Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
54 changes: 54 additions & 0 deletions src/Admin/Controllers/AutofillTriageController.cs
Original file line number Diff line number Diff line change
@@ -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
Comment thread
kdenney marked this conversation as resolved.
{
private readonly IAutofillTriageReportRepository _repo;

public AutofillTriageController(IAutofillTriageReportRepository repo)
=> _repo = repo;

[RequirePermission(Permission.User_List_View)]
public async Task<IActionResult> Index(int page = 1, int count = 25)
{
var skip = (page - 1) * count;
Comment thread
kdenney marked this conversation as resolved.
Comment thread
kdenney marked this conversation as resolved.
Comment thread
kdenney marked this conversation as resolved.
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<IActionResult> 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<IActionResult> Archive(Guid id)
{
Comment thread
kdenney marked this conversation as resolved.
await _repo.ArchiveAsync(id);
return RedirectToAction(nameof(Index));
}
}
21 changes: 21 additions & 0 deletions src/Admin/Models/AutofillTriageFieldResultModel.cs
Original file line number Diff line number Diff line change
@@ -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<AutofillTriageConditionResultModel> Conditions { get; set; } = [];
}

public class AutofillTriageConditionResultModel
{
public string? Description { get; set; }
public bool Passed { get; set; }
}
7 changes: 7 additions & 0 deletions src/Admin/Models/AutofillTriageModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using Bit.Core.Autofill.Entities;

namespace Bit.Admin.Models;

public class AutofillTriageModel : PagedModel<AutofillTriageReport>
{
}
173 changes: 173 additions & 0 deletions src/Admin/Views/AutofillTriage/Details.cshtml
Original file line number Diff line number Diff line change
@@ -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<AutofillTriageFieldResultModel>();
string? parseError = null;
try
{
fields = JsonSerializer.Deserialize<List<AutofillTriageFieldResultModel>>(Model.ReportData,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? [];
}
catch
{
parseError = "Could not parse report data. The stored JSON may be malformed.";
}
}

<h1>Autofill Triage Report</h1>
<a asp-action="Index" class="btn btn-secondary mb-3">&larr; Back to list</a>

<div class="card mb-4">
<div class="card-body">
<dl class="row">
<dt class="col-sm-3">Created At</dt>
<dd class="col-sm-9">@Model.CreationDate.ToString("f")</dd>

<dt class="col-sm-3">Page URL</dt>
<dd class="col-sm-9">
@{ var decodedPageUrl = WebUtility.HtmlDecode(Model.PageUrl); }
<a href="@decodedPageUrl" target="_blank" rel="noreferrer noopener">@decodedPageUrl</a>
</dd>

@if (Model.TargetElementRef != null)
{
<dt class="col-sm-3">Targeted Field</dt>
<dd class="col-sm-9">
<code class="text-danger">@WebUtility.HtmlDecode(Model.TargetElementRef)</code>
<small class="text-muted">(field the user right-clicked)</small>
</dd>
}

<dt class="col-sm-3">Extension Version</dt>
<dd class="col-sm-9">@Model.ExtensionVersion</dd>

<dt class="col-sm-3">User Message</dt>
<dd class="col-sm-9">@(Model.UserMessage != null ? WebUtility.HtmlDecode(Model.UserMessage) : "(none)")</dd>
</dl>
<div class="mt-3 pt-3 border-top">
<form method="post" asp-action="Archive" asp-route-id="@Model.Id" style="display:inline">
<button type="submit" class="btn btn-outline-danger"
onclick="return confirm('Archive this report?')">
Archive
</button>
@Html.AntiForgeryToken()
</form>
</div>
</div>
</div>

@if (parseError != null)
{
<div class="alert alert-warning">@parseError</div>
}

<h2>Field Analysis (@fields.Count fields)</h2>

@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;
<div class="card mb-3 @(isTarget ? "border-primary" : "")">
<div class="card-header d-flex justify-content-between align-items-center"
data-bs-toggle="collapse" data-bs-target="#@collapseId"
role="button" aria-expanded="false" aria-controls="@collapseId"
style="cursor: pointer; user-select: none;">

Check warning on line 79 in src/Admin/Views/AutofillTriage/Details.cshtml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a 'onKeyPress|onKeyDown|onKeyUp' attribute to this <div> tag.

See more on https://sonarcloud.io/project/issues?id=bitwarden_server&issues=AZ0xii_JPU4l3pg_onlz&open=AZ0xii_JPU4l3pg_onlz&pullRequest=7337

Check warning on line 79 in src/Admin/Views/AutofillTriage/Details.cshtml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use <button> or <input> instead of the button role to ensure accessibility across all devices.

See more on https://sonarcloud.io/project/issues?id=bitwarden_server&issues=AZ0xii_JPU4l3pg_onl0&open=AZ0xii_JPU4l3pg_onl0&pullRequest=7337
<div class="d-flex align-items-center gap-2">
<strong>@fieldLabel</strong>
<span class="text-muted">|</span>
<span>Identified as:
@if (field.Eligible)
{
<span class="badge bg-success">@field.QualifiedAs</span>
}
else
{
<span class="badge bg-secondary">@field.QualifiedAs</span>
}
</span>
</div>
<span data-for="@collapseId" class="text-muted small">Expand</span>
</div>
<div class="collapse" id="@collapseId">
<div class="card-body">
<dl class="row mb-0">
@if (field.HtmlId != null)
{
<dt class="col-sm-3">ID</dt>
<dd class="col-sm-9"><code>@field.HtmlId</code></dd>
}
@if (field.HtmlName != null)
{
<dt class="col-sm-3">Name</dt>
<dd class="col-sm-9"><code>@field.HtmlName</code></dd>
}
@if (field.HtmlType != null)
{
<dt class="col-sm-3">Type</dt>
<dd class="col-sm-9"><code>@field.HtmlType</code></dd>
}
@if (field.Placeholder != null)
{
<dt class="col-sm-3">Placeholder</dt>
<dd class="col-sm-9">@field.Placeholder</dd>
}
@if (field.AriaLabel != null)
{
<dt class="col-sm-3">ARIA Label</dt>
<dd class="col-sm-9">@field.AriaLabel</dd>
}
@if (field.Autocomplete != null)
{
<dt class="col-sm-3">Autocomplete</dt>
<dd class="col-sm-9"><code>@field.Autocomplete</code></dd>
}
@if (field.FormIndex != null)
{
<dt class="col-sm-3">Form Index</dt>
<dd class="col-sm-9">@field.FormIndex</dd>
}
</dl>

@if (field.Conditions.Any())
{
<hr />
<h6>Conditions</h6>
<ul class="list-unstyled mb-0">
@foreach (var condition in field.Conditions)
{
<li>
@if (condition.Passed)
{
<i class="fa fa-check text-success" aria-hidden="true"></i>
}
else
{
<i class="fa fa-times text-secondary" aria-hidden="true"></i>
}
@condition.Description
</li>
}
</ul>
}
</div>
</div>
</div>
}

@section Scripts {
<script>
document.querySelectorAll('.collapse').forEach(function (el) {
el.addEventListener('show.bs.collapse', function () {
document.querySelector('[data-for="' + el.id + '"]').textContent = 'Collapse';
});
el.addEventListener('hide.bs.collapse', function () {
document.querySelector('[data-for="' + el.id + '"]').textContent = 'Expand';
});
});
</script>
}
93 changes: 93 additions & 0 deletions src/Admin/Views/AutofillTriage/Index.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
@using System.Net
@model AutofillTriageModel
@{
ViewData["Title"] = "Autofill Triage";
}

<h1>Autofill Triage Reports</h1>

<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th style="white-space: nowrap;">Created At</th>
<th>Page URL</th>
<th>User Message</th>
<th></th>
</tr>
</thead>
<tbody>
@if (!Model.Items.Any())
{
<tr>
<td colspan="4">No reports to show.</td>
</tr>
}
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;
<tr>
<td style="white-space: nowrap;">
<a asp-action="Details" asp-route-id="@item.Id" title="@item.CreationDate.ToString()">
@item.CreationDate.ToString("g")
</a>
</td>
<td title="@decodedUrl">@truncatedUrl</td>
<td title="@rawMessage">@(truncatedMessage ?? "—")</td>
<td class="text-end" style="width: 1%; white-space: nowrap;">
<a asp-action="Details" asp-route-id="@item.Id" class="btn btn-sm btn-secondary me-1">View Details</a>
<form method="post" asp-action="Archive" asp-route-id="@item.Id" style="display:inline">
<button type="submit"
class="btn btn-sm btn-outline-danger"
onclick="return confirm('Archive this report?')">
Archive
</button>
@Html.AntiForgeryToken()
</form>
</td>
</tr>
}
}
</tbody>
</table>
</div>

<nav>
<ul class="pagination">
@if (Model.PreviousPage.HasValue)
{
<li class="page-item">
<a class="page-link" asp-action="Index" asp-route-page="@Model.PreviousPage.Value"
asp-route-count="@Model.Count">
Previous
</a>
</li>
}
else
{
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1">Previous</a>
</li>
}
@if (Model.NextPage.HasValue)
{
<li class="page-item">
<a class="page-link" asp-action="Index" asp-route-page="@Model.NextPage.Value"
asp-route-count="@Model.Count">
Next
</a>
</li>
}
else
{
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1">Next</a>
</li>
}
</ul>
</nav>
8 changes: 8 additions & 0 deletions src/Admin/Views/Shared/_Layout.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@
</a>
</li>
}
@if (canViewUsers && FeatureService.IsEnabled(FeatureFlagKeys.EnableAutofillIssueReporting))
{
<li class="nav-item" active-controller="AutofillTriage">
<a class="nav-link" asp-controller="AutofillTriage" asp-action="Index">
Autofill Triage
</a>
</li>
}
@if (canViewTools)
{
<li class="nav-item dropdown" active-controller="tools">
Expand Down
22 changes: 22 additions & 0 deletions src/Api/Autofill/Controllers/AutofillTriageReportController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Bit.Api.Autofill.Models;
using Bit.Core;
using Bit.Core.Autofill.Commands;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Bit.Api.Autofill.Controllers;

[Route("autofill/triage-report")]
[Authorize("Application")]
[RequireFeature(FeatureFlagKeys.EnableAutofillIssueReporting)]
public class AutofillTriageReportController(ICreateAutofillTriageReportCommand createAutofillTriageReportCommand)
: Controller
{
[HttpPost("")]
public async Task<IActionResult> Post([FromBody] AutofillTriageReportRequestModel model)
Comment thread
kdenney marked this conversation as resolved.
{
await createAutofillTriageReportCommand.Run(model.ToEntity());
Comment thread
kdenney marked this conversation as resolved.
return NoContent();
}
}
Loading
Loading