diff --git a/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs b/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs index 4f9a953212..e912f7281c 100644 --- a/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs +++ b/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Packaging; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; +using Umbraco.Core.Telemetry; namespace Umbraco.Core.Composing.CompositionExtensions { @@ -79,6 +80,8 @@ namespace Umbraco.Core.Composing.CompositionExtensions factory.GetInstance(), factory.GetInstance(), new DirectoryInfo(IOHelper.GetRootDirectorySafe()))); + composition.RegisterUnique(); + return composition; } diff --git a/src/Umbraco.Core/Manifest/ManifestParser.cs b/src/Umbraco.Core/Manifest/ManifestParser.cs index 9bbb0875d8..a9ce06e8da 100644 --- a/src/Umbraco.Core/Manifest/ManifestParser.cs +++ b/src/Umbraco.Core/Manifest/ManifestParser.cs @@ -67,7 +67,7 @@ namespace Umbraco.Core.Manifest /// /// Gets all manifests. /// - private IEnumerable GetManifests() + internal IEnumerable GetManifests() { var manifests = new List(); diff --git a/src/Umbraco.Core/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs index e50eb69467..cadd661e28 100644 --- a/src/Umbraco.Core/Manifest/PackageManifest.cs +++ b/src/Umbraco.Core/Manifest/PackageManifest.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using Newtonsoft.Json; using Umbraco.Core.PropertyEditors; @@ -9,6 +10,28 @@ namespace Umbraco.Core.Manifest /// public class PackageManifest { + private string _packageName; + + [JsonProperty("name")] + public string PackageName + { + get + { + if (string.IsNullOrWhiteSpace(_packageName) is false) + { + return _packageName; + } + + if (string.IsNullOrWhiteSpace(Source) is false) + { + _packageName = Path.GetFileName(Path.GetDirectoryName(Source)); + } + + return _packageName; + } + set => _packageName = value; + } + /// /// Gets the source path of the manifest. /// @@ -66,5 +89,17 @@ namespace Umbraco.Core.Manifest /// [JsonProperty("sections")] public ManifestSection[] Sections { get; set; } = Array.Empty(); + + /// + /// Gets or sets the version of the package + /// + [JsonProperty("version")] + public string Version { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether telemetry is allowed + /// + [JsonProperty("allowPackageTelemetry")] + public bool AllowPackageTelemetry { get; set; } = true; } } diff --git a/src/Umbraco.Core/Telemetry/ITelemetryService.cs b/src/Umbraco.Core/Telemetry/ITelemetryService.cs new file mode 100644 index 0000000000..f4ca3736f6 --- /dev/null +++ b/src/Umbraco.Core/Telemetry/ITelemetryService.cs @@ -0,0 +1,15 @@ +using Umbraco.Core.Telemetry.Models; + +namespace Umbraco.Core.Telemetry +{ + /// + /// Service which gathers the data for telemetry reporting + /// + public interface ITelemetryService + { + /// + /// Try and get the + /// + bool TryGetTelemetryReportData(out TelemetryReportData telemetryReportData); + } +} diff --git a/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs b/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs new file mode 100644 index 0000000000..a86c4c4fa2 --- /dev/null +++ b/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs @@ -0,0 +1,28 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Core.Telemetry.Models +{ + /// + /// Serializable class containing information about an installed package. + /// + [Serializable] + [DataContract(Name = "packageTelemetry")] + public class PackageTelemetry + { + /// + /// Gets or sets the name of the installed package. + /// + [DataMember(Name = "name")] + public string Name { get; set; } + + /// + /// Gets or sets the version of the installed package. + /// + /// + /// This may be an empty string if no version is specified, or if package telemetry has been restricted. + /// + [DataMember(Name = "version")] + public string Version { get; set; } + } +} diff --git a/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs b/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs new file mode 100644 index 0000000000..560bd1dcfe --- /dev/null +++ b/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Umbraco.Core.Telemetry.Models +{ + /// + /// Serializable class containing telemetry information. + /// + [DataContract] + public class TelemetryReportData + { + /// + /// Gets or sets a random GUID to prevent an instance posting multiple times pr. day. + /// + [DataMember(Name = "id")] + public Guid Id { get; set; } + + /// + /// Gets or sets the Umbraco CMS version. + /// + [DataMember(Name = "version")] + public string Version { get; set; } + + /// + /// Gets or sets an enumerable containing information about packages. + /// + /// + /// Contains only the name and version of the packages, unless no version is specified. + /// + [DataMember(Name = "packages")] + public IEnumerable Packages { get; set; } + } +} diff --git a/src/Umbraco.Core/Telemetry/TelemetryService.cs b/src/Umbraco.Core/Telemetry/TelemetryService.cs new file mode 100644 index 0000000000..a1b1f39ecd --- /dev/null +++ b/src/Umbraco.Core/Telemetry/TelemetryService.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Manifest; +using Umbraco.Core.Telemetry.Models; + +namespace Umbraco.Core.Telemetry +{ + /// + internal class TelemetryService : ITelemetryService + { + private readonly IUmbracoSettingsSection _settings; + private readonly ManifestParser _manifestParser; + + /// + /// Initializes a new instance of the class. + /// + public TelemetryService( + ManifestParser manifestParser, + IUmbracoSettingsSection settings) + { + _manifestParser = manifestParser; + _settings = settings; + } + + /// + public bool TryGetTelemetryReportData(out TelemetryReportData telemetryReportData) + { + if (TryGetTelemetryId(out Guid telemetryId) is false) + { + telemetryReportData = null; + return false; + } + + telemetryReportData = new TelemetryReportData + { + Id = telemetryId, + Version = UmbracoVersion.SemanticVersion.ToSemanticString(), + Packages = GetPackageTelemetry() + }; + return true; + } + + private bool TryGetTelemetryId(out Guid telemetryId) + { + // Parse telemetry string as a GUID & verify its a GUID and not some random string + // since users may have messed with or decided to empty the app setting or put in something random + if (Guid.TryParse(_settings.BackOffice.Id, out var parsedTelemetryId) is false) + { + telemetryId = Guid.Empty; + return false; + } + + telemetryId = parsedTelemetryId; + return true; + } + + private IEnumerable GetPackageTelemetry() + { + List packages = new (); + var manifests = _manifestParser.GetManifests(); + + foreach (var manifest in manifests) + { + if (manifest.AllowPackageTelemetry is false) + { + continue; + } + + packages.Add(new PackageTelemetry + { + Name = manifest.PackageName, + Version = manifest.Version ?? string.Empty + }); + } + + return packages; + } + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 632031a2e6..6729930174 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -396,6 +396,10 @@ + + + + diff --git a/src/Umbraco.Tests/Manifest/ManifestParserTests.cs b/src/Umbraco.Tests/Manifest/ManifestParserTests.cs index 6b140e3757..26c031c7aa 100644 --- a/src/Umbraco.Tests/Manifest/ManifestParserTests.cs +++ b/src/Umbraco.Tests/Manifest/ManifestParserTests.cs @@ -443,5 +443,27 @@ javascript: ['~/test.js',/*** some note about stuff asd09823-4**09234*/ '~/test2 Assert.AreEqual("Content", manifest.Sections[0].Name); Assert.AreEqual("World", manifest.Sections[1].Name); } + + [Test] + public void CanParseManifest_Version() + { + const string json = @"{""name"": ""VersionPackage"", ""version"": ""1.0.0""}"; + PackageManifest manifest = _parser.ParseManifest(json); + + Assert.Multiple(() => + { + Assert.AreEqual("VersionPackage", manifest.PackageName); + Assert.AreEqual("1.0.0", manifest.Version); + }); + } + + [Test] + public void CanParseManifest_TrackingAllowed() + { + const string json = @"{""allowPackageTelemetry"": false }"; + PackageManifest manifest = _parser.ParseManifest(json); + + Assert.IsFalse(manifest.AllowPackageTelemetry); + } } } diff --git a/src/Umbraco.Web/Telemetry/ReportSiteTask.cs b/src/Umbraco.Web/Telemetry/ReportSiteTask.cs index 24ac7cbf3a..78d4b24ab6 100644 --- a/src/Umbraco.Web/Telemetry/ReportSiteTask.cs +++ b/src/Umbraco.Web/Telemetry/ReportSiteTask.cs @@ -1,14 +1,11 @@ using Newtonsoft.Json; using System; using System.Net.Http; -using System.Runtime.Serialization; using System.Text; using System.Threading; using System.Threading.Tasks; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; +using Umbraco.Core.Telemetry; using Umbraco.Web.Scheduling; namespace Umbraco.Web.Telemetry @@ -17,14 +14,19 @@ namespace Umbraco.Web.Telemetry { private readonly IProfilingLogger _logger; private static HttpClient _httpClient; - private readonly IUmbracoSettingsSection _settings; + private readonly ITelemetryService _telemetryService; - public ReportSiteTask(IBackgroundTaskRunner runner, int delayBeforeWeStart, int howOftenWeRepeat, IProfilingLogger logger, IUmbracoSettingsSection settings) + public ReportSiteTask( + IBackgroundTaskRunner runner, + int delayBeforeWeStart, + int howOftenWeRepeat, + IProfilingLogger logger, + ITelemetryService telemetryService) : base(runner, delayBeforeWeStart, howOftenWeRepeat) { _logger = logger; _httpClient = new HttpClient(); - _settings = settings; + _telemetryService = telemetryService; } /// @@ -34,12 +36,9 @@ namespace Umbraco.Web.Telemetry /// A value indicating whether to repeat the task. public override async Task PerformRunAsync(CancellationToken token) { - // Try & get a value stored in umbracoSettings.config on the backoffice XML element ID attribute - var backofficeIdentifierRaw = _settings.BackOffice.Id; - // Parse as a GUID & verify its a GUID and not some random string // In case of users may have messed or decided to empty the file contents or put in something random - if (Guid.TryParse(backofficeIdentifierRaw, out var telemetrySiteIdentifier) == false) + if (_telemetryService.TryGetTelemetryReportData(out var telemetryReportData) is false) { // Some users may have decided to mess with the XML attribute and put in something else // Stop repeating this task (no need to keep checking) @@ -61,8 +60,7 @@ namespace Umbraco.Web.Telemetry using (var request = new HttpRequestMessage(HttpMethod.Post, "installs/")) { - var postData = new TelemetryReportData { Id = telemetrySiteIdentifier, Version = UmbracoVersion.SemanticVersion.ToSemanticString() }; - request.Content = new StringContent(JsonConvert.SerializeObject(postData), Encoding.UTF8, "application/json"); //CONTENT-TYPE header + request.Content = new StringContent(JsonConvert.SerializeObject(telemetryReportData), Encoding.UTF8, "application/json"); //CONTENT-TYPE header // Set a low timeout - no need to use a larger default timeout for this POST request _httpClient.Timeout = new TimeSpan(0, 0, 1); @@ -86,15 +84,5 @@ namespace Umbraco.Web.Telemetry } public override bool IsAsync => true; - - [DataContract] - private class TelemetryReportData - { - [DataMember(Name = "id")] - public Guid Id { get; set; } - - [DataMember(Name = "version")] - public string Version { get; set; } - } } } diff --git a/src/Umbraco.Web/Telemetry/TelemetryComponent.cs b/src/Umbraco.Web/Telemetry/TelemetryComponent.cs index 1ae9ad9764..c3d29f72ca 100644 --- a/src/Umbraco.Web/Telemetry/TelemetryComponent.cs +++ b/src/Umbraco.Web/Telemetry/TelemetryComponent.cs @@ -1,6 +1,7 @@ using Umbraco.Core.Composing; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; +using Umbraco.Core.Telemetry; using Umbraco.Web.Scheduling; namespace Umbraco.Web.Telemetry @@ -8,13 +9,13 @@ namespace Umbraco.Web.Telemetry public class TelemetryComponent : IComponent { private readonly IProfilingLogger _logger; - private readonly IUmbracoSettingsSection _settings; + private readonly ITelemetryService _telemetryService; private BackgroundTaskRunner _telemetryReporterRunner; - public TelemetryComponent(IProfilingLogger logger, IUmbracoSettingsSection settings) + public TelemetryComponent(IProfilingLogger logger, IUmbracoSettingsSection settings, ITelemetryService telemetryService) { _logger = logger; - _settings = settings; + _telemetryService = telemetryService; } public void Initialize() @@ -26,7 +27,7 @@ namespace Umbraco.Web.Telemetry const int howOftenWeRepeat = 60 * 1000 * 60 * 24; // 60 * 1000 * 60 * 24 = 24hrs (86400000) // As soon as we add our task to the runner it will start to run (after its delay period) - var task = new ReportSiteTask(_telemetryReporterRunner, delayBeforeWeStart, howOftenWeRepeat, _logger, _settings); + var task = new ReportSiteTask(_telemetryReporterRunner, delayBeforeWeStart, howOftenWeRepeat, _logger, _telemetryService); _telemetryReporterRunner.TryAdd(task); }