Skip to content

Add report issue workflow to backend and toolbar UI#35717

Open
fmontes wants to merge 5 commits into
mainfrom
fmontes/report-bug-button
Open

Add report issue workflow to backend and toolbar UI#35717
fmontes wants to merge 5 commits into
mainfrom
fmontes/report-bug-button

Conversation

@fmontes
Copy link
Copy Markdown
Member

@fmontes fmontes commented May 15, 2026

Proposed Changes

  • add a new /api/v1/report-issue backend endpoint that validates report submissions, forwards them to the workflow action, and handles proxy and upstream failures explicitly
  • add the toolbar user-menu entry and report issue dialog in dotCMS UI, including multipart submission, client metadata capture, PrimeNG file upload, and inline validation/error handling
  • add tests for the backend resource and UI service/component flows, and add the required UI translations and OpenAPI documentation

Checklist

  • Tests
  • Translations
  • Security Implications Contemplated (report submission stays server-side and relies on the configured user proxy instead of exposing a token in the UI)

Additional Info

This diff includes the follow-up typing cleanup for the UI report-issue service response and the signal-based/report-component refactors that are already on this branch.

Screenshots

Original Updated
N/A N/A

fmontes and others added 4 commits May 14, 2026 18:25
Convert @Input/@output to model()/output(), replace visible setter
side-effect with effect(), and promote screenshotFile to a signal for
OnPush correctness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move dot-report-issue out of dot-toolbar/components since it is no
longer toolbar-specific. Update relative imports accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

❌ Issue Linking Required

This PR could not be linked to an issue. All PRs must be linked to an issue for tracking purposes.

How to fix this:

Option 1: Add keyword to PR body (Recommended - auto-removes this comment)
Edit this PR description and add one of these lines:

  • This PR fixes #123 or Fixes: #123

  • This PR closes #123 or Closes: #123

  • This PR resolves #123 or Resolves: #123

  • Other supported keywords: fix, fixed, close, closed, resolve, resolved
    Option 2: Link via GitHub UI (Note: won't clear the failed check)

  1. Go to the PR → Development section (right sidebar)

  2. Click "Link issue" and select an existing issue

  3. Push a new commit or re-run the workflow to clear the failed check
    Option 3: Use branch naming
    Create a new branch with one of these patterns:

  • 123-feature-description (number at start)

  • issue-123-feature-description (issue-number at start)

  • feature-issue-123 (issue-number anywhere)

Why is this required?

Issue linking ensures proper tracking, documentation, and helps maintain project history. It connects your code changes to the problem they solve.---

This comment was automatically generated by the issue linking workflow

@github-actions github-actions Bot added Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code labels May 15, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 15, 2026

Claude finished @fmontes's task in 4m 5s —— View job


Review — PR #35717

High-priority

  1. PII forwarded externally by default to a hardcoded URL. ReportIssueResource.DEFAULT_WORKFLOW_URL is https://corpsites-headless.dotcms.cloud/.... buildMetadata adds user.userId, user.email, user.fullName, remoteAddress, server hostname, requestUrl, referer, userAgent. Any backend user submitting (no role gate) pushes this to a dotCMS-owned third party with no operator opt-in, no consent dialog, and no way to disable forwarding without changing the property. For an on-prem customer this can be a GDPR/data-residency problem — the customer's user identities and IPs leave their boundary by default. Recommend: (a) make forwarding opt-in via config, (b) document data sent in the UI before submit, (c) consider hashing/omitting email/IP unless explicitly enabled.

  2. No rate limiting / no description size limit. Any backend user can hit /api/v1/report-issue repeatedly. The description text field is read via field.getValue() (ReportIssueResource.java:195) with no length cap — a multi-MB string passes through the screenshot size guard entirely. Combined with point 1, this is a meaningful abuse vector against the centralized upstream. Add a per-user rate limit and a description max length (e.g., 5–10 KB).

  3. Login-As impersonation attributes the report to the impersonated user, not the impersonator. initDataObject.getUser() returns the effective (impersonated) user during Login-As (ReportIssueResource.java:134). The PR also removed the visible: !auth.isLoginAs gate on the separator and did not gate the new "report-an-issue" menu item with !auth.isLoginAs (dot-toolbar-user.store.ts:142-147), so a Login-As session can submit. Result: the bug is filed under the impersonated user's email/name with no audit trail of the real submitter. Either hide the item in Login-As mode (consistent with my-account/login-as) or capture both real + impersonated user in metadata.

Medium

  1. isClosing reentry guard is racy. dot-report-issue.component.ts:102-115: sets isClosing = true, calls visible.set(false), then queueMicrotask clears it. PrimeNG (onHide) fires asynchronously after the dialog animation, well past the next microtask — so the guard does not actually block the second handleClose() triggered by onHide. Result: shutdown emits twice and resetForm runs twice. Not catastrophic, but ineffective code. Use a one-shot flag cleared inside onHide, or simply skip work if !this.visible().

  2. Dialog missing explicit closable / closeOnEscape. dot-report-issue.component.html:1-10. core-web/CLAUDE.md requires "All dialogs must have closable: true and closeOnEscape: true". Add them explicitly.

  3. HTTP method inconsistency to upstream. HttpReportIssueForwarder uses POST for the JSON path (ReportIssueResource.java:657) and PUT for the multipart path (ReportIssueResource.java:668) to the same upstream URL. Relies on the workflow fire endpoint accepting both — worth confirming the upstream actually does, and aligning if not (or at least leaving a comment explaining why they differ).

  4. hasSubmitted is set to true even when validation blocks the call (dot-report-issue.component.ts:145-150). Combined with the submit button being enabled while invalid (spec line 80), submit click → triggers the path-1 validator error display. Fine UX, but the test should keep submit enabled before a submit attempt when the form is invalid is fragile — if a future contributor adds [disabled]="form.invalid" they will silently break the validation-trigger flow.

Low / nits

  1. ReportIssueResource.getRequiredTextField (line 193) takes a fieldName parameter but hardcodes the error message to "Description is required.". If reused for another field it would lie. Inline it or interpolate fieldName.

  2. title.substring(0, 120) (line 301) can split a surrogate pair if a code-point boundary lands at index 120. Use title.codePoints() truncation or just accept it — low impact.

  3. getReportedPath() injects a user-controlled URL fragment (from client metadata.url) into the upstream issue title via normalizeReportedPath (line 304-319). No injection into dotCMS itself, but the title appears in upstream notifications/emails — keep in mind the upstream needs to render this safely.

  4. escapeHeaderValue (line 701) handles \ and " but not CRLF. FileUtil.sanitizeFileName already strips CR/LF from filenames (FileUtil.java:223 illegalChars includes 10 and 13), so this is currently safe — but the escape function on its own is incomplete and could foot-gun a future caller. A one-line comment or a CR/LF assertion would harden it.

  5. onScreenshotClear() (component.ts:140-142) is a one-line passthrough to removeScreenshot() — fold the two if the only caller is the template (onClear).

  6. DotReportIssuePayload.metadata: Record<string, string> (service.ts:12) — fine, but the backend type is Map<String, Object>. If you ever pass nested objects (e.g., viewport: {w, h}) the type will lie. Either widen to Record<string, unknown> or keep flat-string only on the backend too.

  7. The effect(() => { if (this.visible()) this.resetForm(); }) (component.ts:95-99) runs resetForm on transition to true and on every re-trigger; relying on model() semantics works today but is a subtle coupling. A linkedSignal/explicit handler in the open path would be clearer.

  8. Test expect(...).toHaveText(...) is called on a possibly-null query result with ?. (dot-report-issue.component.spec.ts:215) — if the element is missing, the assertion silently no-ops. Use expect(spectator.query(...)).toHaveText(...) (no ?.) so a missing element fails the test.

Not a problem (sanity-checked)

  • Screenshot size guard memory bound (one chunk past maxBytes) — fine.
  • Backend correctly catches IOException / InterruptedException and restores interrupt status.
  • WORKFLOW_URL_PROPERTY is server-side, no SSRF.
  • Allowed mime-type set + sanitized filename = safe filename header.

Summary

Functionally the change looks coherent and tested. The two things I would not ship without addressing are (1) external PII forwarding by default + no rate/size limit (#1, #2) and (3) Login-As attribution. The frontend close-guard race (#4) and missing dialog flags (#5) are easy follow-ups.
branch

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an end-to-end “Report an Issue” workflow for the dotCMS Admin UI by introducing a new backend proxy endpoint that forwards issue reports (optionally with a screenshot) to an upstream workflow action, and wiring a new toolbar user-menu entry + dialog that submits multipart data and surfaces validation/errors.

Changes:

  • Backend: Introduces POST /api/v1/report-issue JAX-RS resource, request validation, upstream forwarding (JSON or multipart), and response mapping; adds unit tests.
  • Frontend: Adds “Report an Issue” menu item and a new dialog component to collect description + optional screenshot and submit via a new API service.
  • Docs/i18n: Adds OpenAPI path entry and new UI translation keys.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
dotCMS/src/main/java/com/dotcms/rest/api/v1/reportissue/ReportIssueResource.java New REST endpoint + upstream forwarder and payload/validation logic for report submissions.
dotCMS/src/test/java/com/dotcms/rest/api/v1/reportissue/ReportIssueResourceTest.java Unit tests covering validation, forwarding behavior, and authorization header handling.
dotCMS/src/main/webapp/WEB-INF/openapi/openapi.yaml Documents the new /v1/report-issue endpoint in OpenAPI.
dotCMS/src/main/webapp/WEB-INF/messages/Language.properties Adds i18n strings for the new “Report an Issue” UI flow.
core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/store/dot-toolbar-user.store.ts Adds state + menu action to open the report-issue dialog.
core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/store/dot-toolbar-user.store.spec.ts Tests new menu item and showReportIssue state updates.
core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.ts Includes the new report-issue dialog component in toolbar user component imports.
core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.html Renders the report-issue dialog when store state indicates it should be visible.
core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.spec.ts Updates test setup to provide services required by the newly imported dialog component.
core-web/apps/dotcms-ui/src/app/view/components/dot-report-issue/dot-report-issue.component.ts New dialog component with reactive form validation, screenshot handling, and submission.
core-web/apps/dotcms-ui/src/app/view/components/dot-report-issue/dot-report-issue.component.html Dialog UI markup, inputs, file upload, and submit/cancel actions.
core-web/apps/dotcms-ui/src/app/view/components/dot-report-issue/dot-report-issue.component.spec.ts Component tests for validation, submission flow, and error handling.
core-web/apps/dotcms-ui/src/app/providers.ts Registers the new report-issue API service in app-wide providers.
core-web/apps/dotcms-ui/src/app/api/services/dot-report-issue.service.ts New Angular service to submit multipart form data to /api/v1/report-issue.
core-web/apps/dotcms-ui/src/app/api/services/dot-report-issue.service.spec.ts Service tests ensuring multipart fields are sent (with/without screenshot).
Comments suppressed due to low confidence (4)

dotCMS/src/main/java/com/dotcms/rest/api/v1/reportissue/ReportIssueResource.java:449

  • Client-supplied metadata is merged into the server-built metadata via metadata.putAll(clientMetadata), which allows a malicious client to override trusted server fields (e.g., submittedAt, user, remoteAddress, serverName) and spoof report context. Consider either nesting client metadata under a dedicated key (e.g., metadata.client) or whitelisting/merging in a way that prevents overriding server-owned keys.
        if (user != null) {
            final Map<String, Object> userMetadata = new LinkedHashMap<>();
            userMetadata.put("userId", Try.of(user::getUserId).getOrNull());
            userMetadata.put("email", Try.of(user::getEmailAddress).getOrNull());
            userMetadata.put("fullName", Try.of(user::getFullName).getOrNull());
            metadata.put("user", userMetadata);
        }

        if (clientMetadata != null && !clientMetadata.isEmpty()) {
            metadata.putAll(clientMetadata);
        }

dotCMS/src/main/java/com/dotcms/rest/api/v1/reportissue/ReportIssueResource.java:274

  • readBytesWithLimit reads the screenshot InputStream but never closes it. Other multipart handlers in this codebase use try-with-resources around part.getEntityAs(InputStream.class) to ensure streams are closed; consider doing the same here to avoid leaking request resources under load.
    private static byte[] readBytesWithLimit(final InputStream inputStream, final long maxBytes)
            throws IOException {
        if (inputStream == null) {
            throw new ReportIssueValidationException("Screenshot is invalid.");
        }

        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        final byte[] buffer = new byte[8192];
        long totalBytes = 0L;
        int bytesRead;

        while ((bytesRead = inputStream.read(buffer)) != -1) {
            totalBytes += bytesRead;
            if (totalBytes > maxBytes) {
                throw new ReportIssueValidationException("Screenshot exceeds the maximum allowed size.");
            }
            outputStream.write(buffer, 0, bytesRead);
        }

        return outputStream.toByteArray();

dotCMS/src/main/java/com/dotcms/rest/api/v1/reportissue/ReportIssueResource.java:118

  • The OpenAPI annotations for 400/502 use the generic ResponseEntityView schema, and the 200 response has no schema. In this codebase, other endpoints typically reference a specific response view type (e.g., TempFilesView) to keep the contract explicit; consider introducing a dedicated response view for this endpoint (success + error) and referencing it in @Schema to avoid an untyped/ambiguous API contract.
    @Operation(
            operationId = "reportIssue",
            summary = "Report a dotCMS UI issue",
            description = "Creates a Bug contentlet in the configured upstream reporting dotCMS instance.",
            tags = {"Workflow"},
            responses = {
                    @ApiResponse(responseCode = "200", description = "Issue reported",
                            content = @Content(mediaType = "application/json")),
                    @ApiResponse(responseCode = "400", description = "Invalid report payload",
                            content = @Content(mediaType = "application/json",
                                    schema = @Schema(implementation = ResponseEntityView.class))),
                    @ApiResponse(responseCode = "502", description = "Reporting service unavailable or unauthorized",
                            content = @Content(mediaType = "application/json",
                                    schema = @Schema(implementation = ResponseEntityView.class)))

dotCMS/src/main/java/com/dotcms/rest/api/v1/reportissue/ReportIssueResource.java:485

  • mapUpstreamResponse can return upstream 4xx responses directly (e.g., 415) when the upstream includes a body, but the endpoint’s OpenAPI annotations (and openapi.yaml entry) only document 200/400/502. Consider either normalizing these upstream errors into the documented 502/400 responses, or expanding the OpenAPI responses to include the possible passthrough status codes so clients have an accurate contract.
    private Response mapUpstreamResponse(final ReportIssueForwardResponse upstreamResponse) {
        final int statusCode = upstreamResponse.statusCode();

        if (statusCode == Response.Status.UNAUTHORIZED.getStatusCode()
                || statusCode == Response.Status.FORBIDDEN.getStatusCode()) {
            return error(Response.Status.BAD_GATEWAY, ERROR_PROXY_NOT_AUTHORIZED,
                    "Report issue service is not authorized. Check the User Proxy plugin configuration.");
        }

        if (statusCode >= 200 && statusCode < 300) {
            return Response.status(statusCode)
                    .entity(upstreamResponse.body())
                    .type(upstreamResponse.contentType().orElse(MediaType.APPLICATION_JSON))
                    .build();
        }

        if (statusCode >= 400 && statusCode < 500 && UtilMethods.isSet(upstreamResponse.body())) {
            return Response.status(statusCode)
                    .entity(upstreamResponse.body())
                    .type(upstreamResponse.contentType().orElse(MediaType.APPLICATION_JSON))
                    .build();
        }

Comment on lines +63 to +64
static final String DEFAULT_WORKFLOW_URL =
"https://corpsites-headless.dotcms.cloud/api/v1/workflow/actions/default/fire/NEW";
Comment on lines +75 to +83
<button
pButton
type="submit"
[label]="'submit' | dm"
[loading]="isSubmitting()"
[disabled]="isSubmitting()"
data-testid="dot-report-issue-submit-button"
(click)="save()"></button>
</ng-template>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Add Report an Issue workflow to dotCMS admin toolbar

2 participants