I discovered that they are now putting some prefetch script into the initial payload that contains some useful json. (Actually : this is the json mentioned by b.mcewan in the currently accepted answer).
Since I have this all ready for consumption.... Here is a link to some code that will gather your machine's installed hotfixes and present some detail including the KB title.
Code will run in LINQPad
http://share.linqpad.net/l6tdxc.linq
In case you do not use LP here are the routines. ParseTitle makes use of some autogenerated classes to deserialize the json. You will need to remove the .Dump() extension method calls and Hyperlinq class reference and present the data some other way. (EDIT: more than just the KB article Title is exposed by the ArticleInfo class.... like the details about what the hotfix does, how to get it and install it etc.)
void Main()
{
const string query = "SELECT HotFixID, InstalledOn, InstalledBy, Description, Caption, * FROM Win32_QuickFixEngineering";
var result =
(from ManagementObject quickfix in new ManagementObjectSearcher(query).Get() //.AsParallel()
orderby Convert.ToDateTime(quickfix["InstalledOn"]) descending
let web = new WebClient()
let input = quickfix["Caption"].ToString()
let id = input.Substring(35, input.Length - 35)
let url = $"{input.Replace("microsoft.com/?kbid=", "microsoft.com/en-us/help/")}/kb{id}"
let html = web.DownloadString(url)
where string.IsNullOrEmpty( html ).Equals(false)
let kbInfo = ParseInfo( url, html )
where kbInfo != null
let pub = kbInfo.Details.PublishedOn
let title = kbInfo.Details.Title
let desc = Util.OnDemand( "More....", () =>
Util.RawHtml(string.Join(Environment.NewLine,
kbInfo.Details.Body
.Select(i => $"<span class=typeglyphx>{i.Title}</span>{i.Content.Single()}")))
)
select
new
{
HotFixID = Util.RawHtml($"<span class=typeglyphx>{quickfix["HotFixID"].ToString()}</span>"),
Published = pub.Date,
InstalledOn = quickfix["InstalledOn"].ToString(),
InstallDelay = $"{Convert.ToInt16((Convert.ToDateTime(quickfix["InstalledOn"].ToString()).Date - pub.Date).TotalDays)} days",
InstalledBy = quickfix["InstalledBy"].ToString(),
Description = new Hyperlinq(quickfix["Description"].ToString()),
Title = Util.RawHtml($"<span class=typeglyphx>{title}</span>") ?? $"{url} [Could not obtain KB title]",
Body = desc,
Link = new Hyperlinq(url),
}
).Dump(1);
}
#nullable enable
string? ParseTitle ( string html )
{
var doc = new HtmlDocument();
doc.LoadHtml(html);
var meta = doc.DocumentNode
.SelectNodes("//script");
var searchToken = "microsoft.support.prefetchedArticle = (function() ";
var nuggets = meta
.Where(i => i.OuterHtml.Contains(searchToken))
.Select(i => i.OuterHtml)
.Single();
var start = nuggets.IndexOf(":") + 1;
var length = nuggets.Length - start - 28;
var json = nuggets.Substring(start, length);
string? ret = null;
try
{
var articleInfo = MSKBPreFetched.ArticleInfo.FromJson(json);
ret = articleInfo.Details.Title;
}
catch{ json.DumpTrace("could not deserialize the json for this article"); // LP only}
return ret;
}
#nullable disable
// <auto-generated />
// json2csharp
// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do:
//
// using MSKBPreFetched;
//
// var articleInfo = ArticleInfo.FromJson(jsonString);
namespace MSKBPreFetched
{
using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
public partial class ArticleInfo
{
[JsonProperty("sideNav")]
//[JsonConverter(typeof(ParseStringConverter))]
public string SideNav { get; set; }
[JsonProperty("details")]
public Details Details { get; set; }
[JsonProperty("_ts")]
public long Ts { get; set; }
}
public partial class Details
{
[JsonProperty("subType")]
public string SubType { get; set; }
[JsonProperty("heading")]
public string Heading { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("body")]
public List<Body> Body { get; set; }
[JsonProperty("urltitle")]
public string Urltitle { get; set; }
[JsonProperty("keywords")]
public List<string> Keywords { get; set; }
[JsonProperty("keywordsLower")]
public List<string> KeywordsLower { get; set; }
[JsonProperty("os")]
public List<object> Os { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("id")]
[JsonConverter(typeof(ParseStringConverter))]
public long Id { get; set; }
[JsonProperty("locale")]
public string Locale { get; set; }
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("titleLower")]
public string TitleLower { get; set; }
[JsonProperty("published")]
public bool Published { get; set; }
[JsonProperty("createdOn")]
public DateTimeOffset CreatedOn { get; set; }
[JsonProperty("publishedOn")]
public DateTimeOffset PublishedOn { get; set; }
[JsonProperty("version")]
public long Version { get; set; }
[JsonProperty("eolProject")]
public string EolProject { get; set; }
[JsonProperty("supportAreaPaths")]
public List<Guid> SupportAreaPaths { get; set; }
[JsonProperty("supportAreaPathNodes")]
public List<PrimarySupportAreaPath> SupportAreaPathNodes { get; set; }
[JsonProperty("disableVAPopup")]
public bool DisableVaPopup { get; set; }
[JsonProperty("primarySupportAreaPath")]
public List<PrimarySupportAreaPath> PrimarySupportAreaPath { get; set; }
[JsonProperty("isContentLocaleFallback")]
public bool IsContentLocaleFallback { get; set; }
[JsonProperty("contentLocale")]
public string ContentLocale { get; set; }
}
public partial class Body
{
[JsonProperty("meta")]
public Meta Meta { get; set; }
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("content")]
public List<string> Content { get; set; }
}
public partial class Meta
{
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("products")]
public List<object> Products { get; set; }
[JsonProperty("supportAreaPaths")]
public List<object> SupportAreaPaths { get; set; }
[JsonProperty("isInternalContent")]
public bool IsInternalContent { get; set; }
[JsonProperty("id")]
public string Id { get; set; }
}
public partial class PrimarySupportAreaPath
{
[JsonProperty("id")]
public Guid Id { get; set; }
[JsonProperty("parent", NullValueHandling = NullValueHandling.Ignore)]
public Guid? Parent { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("tree")]
public List<object> Tree { get; set; }
}
public partial class ArticleInfo
{
public static ArticleInfo FromJson(string json) => JsonConvert.DeserializeObject<ArticleInfo>(json, MSKBPreFetched.Converter.Settings);
}
public static class Serialize
{
public static string ToJson(this ArticleInfo self) => JsonConvert.SerializeObject(self, MSKBPreFetched.Converter.Settings);
}
internal static class Converter
{
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
DateParseHandling = DateParseHandling.None,
Converters =
{
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
},
};
}
internal class ParseStringConverter : JsonConverter
{
public override bool CanConvert(Type t) => t == typeof(long) || t == typeof(long?);
public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null) return null;
var value = serializer.Deserialize<string>(reader);
long l;
if (Int64.TryParse(value, out l))
{
return l;
}
throw new Exception("Cannot unmarshal type long");
}
public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer)
{
if (untypedValue == null)
{
serializer.Serialize(writer, null);
return;
}
var value = (long)untypedValue;
serializer.Serialize(writer, value.ToString());
return;
}
public static readonly ParseStringConverter Singleton = new ParseStringConverter();
}
}