diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cd41e5dccbe2..46437439faea 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -53,6 +53,7 @@ test/Identity.IntegrationTest @bitwarden/team-auth-dev test/Identity.Test @bitwarden/team-auth-dev # Autofill team +**/Autofill @bitwarden/team-autofill-dev src/Core/Utilities/StaticStore.cs @bitwarden/team-autofill-dev src/Core/Enums/GlobalEquivalentDomainsType.cs @bitwarden/team-autofill-dev diff --git a/src/Admin/Controllers/AutofillTriageController.cs b/src/Admin/Controllers/AutofillTriageController.cs new file mode 100644 index 000000000000..3a2bcf9225dc --- /dev/null +++ b/src/Admin/Controllers/AutofillTriageController.cs @@ -0,0 +1,54 @@ +#nullable enable + +using Bit.Admin.Enums; +using Bit.Admin.Models; +using Bit.Admin.Utilities; +using Bit.Core.Autofill.Repositories; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Admin.Controllers; + +[Authorize] +public class AutofillTriageController : Controller +{ + private readonly IAutofillTriageReportRepository _repo; + + public AutofillTriageController(IAutofillTriageReportRepository repo) + => _repo = repo; + + [RequirePermission(Permission.User_List_View)] + public async Task Index(int page = 1, int count = 25) + { + var skip = (page - 1) * count; + var reports = await _repo.GetActiveAsync(skip, count); + var model = new AutofillTriageModel + { + Items = reports.ToList(), + Page = page, + Count = count, + }; + return View(model); + } + + [RequirePermission(Permission.User_List_View)] + public async Task Details(Guid id) + { + var report = await _repo.GetByIdAsync(id); + if (report is null) + { + return NotFound(); + } + + return View(report); + } + + [HttpPost] + [ValidateAntiForgeryToken] + [RequirePermission(Permission.User_List_View)] + public async Task Archive(Guid id) + { + await _repo.ArchiveAsync(id); + return RedirectToAction(nameof(Index)); + } +} diff --git a/src/Admin/Models/AutofillTriageFieldResultModel.cs b/src/Admin/Models/AutofillTriageFieldResultModel.cs new file mode 100644 index 000000000000..9f9efc9a71b3 --- /dev/null +++ b/src/Admin/Models/AutofillTriageFieldResultModel.cs @@ -0,0 +1,21 @@ +namespace Bit.Admin.Models; + +public class AutofillTriageFieldResultModel +{ + public string? HtmlId { get; set; } + public string? HtmlName { get; set; } + public string? HtmlType { get; set; } + public string? Placeholder { get; set; } + public string? AriaLabel { get; set; } + public string? Autocomplete { get; set; } + public string? FormIndex { get; set; } + public bool Eligible { get; set; } + public string? QualifiedAs { get; set; } + public List Conditions { get; set; } = []; +} + +public class AutofillTriageConditionResultModel +{ + public string? Description { get; set; } + public bool Passed { get; set; } +} diff --git a/src/Admin/Models/AutofillTriageModel.cs b/src/Admin/Models/AutofillTriageModel.cs new file mode 100644 index 000000000000..6c6fecd0348b --- /dev/null +++ b/src/Admin/Models/AutofillTriageModel.cs @@ -0,0 +1,7 @@ +using Bit.Core.Autofill.Entities; + +namespace Bit.Admin.Models; + +public class AutofillTriageModel : PagedModel +{ +} diff --git a/src/Admin/Views/AutofillTriage/Details.cshtml b/src/Admin/Views/AutofillTriage/Details.cshtml new file mode 100644 index 000000000000..3406e6fe0258 --- /dev/null +++ b/src/Admin/Views/AutofillTriage/Details.cshtml @@ -0,0 +1,173 @@ +@using System.Net +@using System.Text.Json +@using Bit.Admin.Models +@model Bit.Core.Autofill.Entities.AutofillTriageReport +@{ + ViewData["Title"] = "Autofill Triage Report"; + var fields = new List(); + string? parseError = null; + try + { + fields = JsonSerializer.Deserialize>(Model.ReportData, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? []; + } + catch + { + parseError = "Could not parse report data. The stored JSON may be malformed."; + } +} + +

Autofill Triage Report

+← Back to list + +
+
+
+
Created At
+
@Model.CreationDate.ToString("f")
+ +
Page URL
+
+ @{ var decodedPageUrl = WebUtility.HtmlDecode(Model.PageUrl); } + @decodedPageUrl +
+ + @if (Model.TargetElementRef != null) + { +
Targeted Field
+
+ @WebUtility.HtmlDecode(Model.TargetElementRef) + (field the user right-clicked) +
+ } + +
Extension Version
+
@Model.ExtensionVersion
+ +
User Message
+
@(Model.UserMessage != null ? WebUtility.HtmlDecode(Model.UserMessage) : "(none)")
+
+
+
+ + @Html.AntiForgeryToken() +
+
+
+
+ +@if (parseError != null) +{ +
@parseError
+} + +

Field Analysis (@fields.Count fields)

+ +@for (var i = 0; i < fields.Count; i++) +{ + var field = fields[i]; + var fieldLabel = field.HtmlId ?? field.HtmlName ?? field.Placeholder ?? "(unnamed)"; + var collapseId = $"field-{i}"; + var isTarget = field.HtmlId == Model.TargetElementRef || field.HtmlName == Model.TargetElementRef; +
+ +
+
+
+ @if (field.HtmlId != null) + { +
ID
+
@field.HtmlId
+ } + @if (field.HtmlName != null) + { +
Name
+
@field.HtmlName
+ } + @if (field.HtmlType != null) + { +
Type
+
@field.HtmlType
+ } + @if (field.Placeholder != null) + { +
Placeholder
+
@field.Placeholder
+ } + @if (field.AriaLabel != null) + { +
ARIA Label
+
@field.AriaLabel
+ } + @if (field.Autocomplete != null) + { +
Autocomplete
+
@field.Autocomplete
+ } + @if (field.FormIndex != null) + { +
Form Index
+
@field.FormIndex
+ } +
+ + @if (field.Conditions.Any()) + { +
+
Conditions
+
    + @foreach (var condition in field.Conditions) + { +
  • + @if (condition.Passed) + { + + } + else + { + + } + @condition.Description +
  • + } +
+ } +
+
+
+} + +@section Scripts { + +} diff --git a/src/Admin/Views/AutofillTriage/Index.cshtml b/src/Admin/Views/AutofillTriage/Index.cshtml new file mode 100644 index 000000000000..835dbd1676eb --- /dev/null +++ b/src/Admin/Views/AutofillTriage/Index.cshtml @@ -0,0 +1,93 @@ +@using System.Net +@model AutofillTriageModel +@{ + ViewData["Title"] = "Autofill Triage"; +} + +

Autofill Triage Reports

+ +
+ + + + + + + + + + + @if (!Model.Items.Any()) + { + + + + } + else + { + @foreach (var item in Model.Items) + { + var decodedUrl = WebUtility.HtmlDecode(item.PageUrl); + var truncatedUrl = decodedUrl.Length > 80 ? decodedUrl.Substring(0, 80) + "..." : decodedUrl; + var rawMessage = item.UserMessage != null ? WebUtility.HtmlDecode(item.UserMessage) : null; + var truncatedMessage = rawMessage != null && rawMessage.Length > 60 ? rawMessage.Substring(0, 60) + "..." : rawMessage; + + + + + + + } + } + +
Created AtPage URLUser Message
No reports to show.
+ + @item.CreationDate.ToString("g") + + @truncatedUrl@(truncatedMessage ?? "—") + View Details +
+ + @Html.AntiForgeryToken() +
+
+
+ + diff --git a/src/Admin/Views/Shared/_Layout.cshtml b/src/Admin/Views/Shared/_Layout.cshtml index 9810d061e50b..25786422df3f 100644 --- a/src/Admin/Views/Shared/_Layout.cshtml +++ b/src/Admin/Views/Shared/_Layout.cshtml @@ -74,6 +74,14 @@ } + @if (canViewUsers && FeatureService.IsEnabled(FeatureFlagKeys.EnableAutofillIssueReporting)) + { + + } @if (canViewTools) {