Skip to content

maintenance portlet | Implement search/replace, drop old versions, and delete pushed assets APIs #35191#35231

Open
hassandotcms wants to merge 6 commits intomainfrom
35199-maintenance-search-replace-old-versions-pushed-assets
Open

maintenance portlet | Implement search/replace, drop old versions, and delete pushed assets APIs #35191#35231
hassandotcms wants to merge 6 commits intomainfrom
35199-maintenance-search-replace-old-versions-pushed-assets

Conversation

@hassandotcms
Copy link
Copy Markdown
Member

Summary

Add 3 new REST endpoints to MaintenanceResource, replacing legacy DWR/Struts maintenance tools with modern REST APIs for the Maintenance portlet Tools tab.

New endpoints:

  • POST /api/v1/maintenance/_searchAndReplace — Database-wide find/replace across text content in contentlets, containers, templates, fields, and links. Only affects working/live versions. Flushes all caches after completion.
  • DELETE /api/v1/maintenance/_oldVersions?date=yyyy-MM-dd — Deletes all versions of versionable objects (contentlets, containers, templates, links, workflow history) older than the specified date. Uses ISO date format instead of legacy MM/dd/yyyy.
  • DELETE /api/v1/maintenance/_pushedAssets — Clears all push publishing history records, making all assets appear as "never pushed" to all endpoints.

Implementation details:

  • SearchAndReplaceForm extends Validated with @JsonCreator, validates searchString is non-empty, allows empty replaceString (delete occurrences)
  • Typed @Value.Immutable views (SearchAndReplaceResultView, DropOldVersionsResultView) with full Swagger/OpenAPI annotations
  • SecurityLogger entries on all destructive operations
  • Reuses existing assertBackendUser() pattern (requireAdmin + requiredPortlet MAINTENANCE)
  • ResponseEntityStringView reused for simple _pushedAssets response

Test plan

  • Integration tests pass: ./mvnw verify -pl :dotcms-integration -Dcoreit.test.skip=false -Dit.test=MaintenanceResourceIntegrationTest
  • POST /_searchAndReplace with valid search/replace strings returns {success: true, hasErrors: false}
  • POST /_searchAndReplace with empty searchString returns 400
  • POST /_searchAndReplace with empty replaceString succeeds (deletes occurrences)
  • DELETE /_oldVersions?date=2000-01-01 returns {deletedCount: N, success: true}
  • DELETE /_oldVersions?date=01/01/2000 returns 400 (wrong format)
  • DELETE /_oldVersions without date param returns 400
  • DELETE /_pushedAssets returns "success"
  • Non-admin users get 403 on all endpoints
  • SecurityLogger entries appear for all operations

Closes #35199, Closes #35201, Closes #35204

…s, and delete pushed assets #35191

Add three new endpoints to MaintenanceResource:
- POST /_searchAndReplace: database-wide find/replace across content tables
- DELETE /_oldVersions: drop versionable objects older than a given date
- DELETE /_pushedAssets: clear all push publishing history

Includes Validated forms, @Value.Immutable response views, full Swagger
annotations, and SecurityLogger entries for all destructive operations.
…ions, and pushed assets #35191

Tests cover:
- _searchAndReplace: admin success, non-admin rejection, null/empty form validation,
  empty replaceString allowed
- _oldVersions: admin success, non-admin rejection, missing date, invalid date formats
- _pushedAssets: admin success, non-admin rejection
@claude
Copy link
Copy Markdown
Contributor

claude bot commented Apr 7, 2026

Claude finished @hassandotcms's task in 5m 39s —— View job


Code Review

  • Read all changed files
  • Analyzed implementation

Issues found — several worth addressing before merge.


1. DBSearchAndReplace has no early return on invalid input

MaintenanceUtil.java:449-456 — the guard clauses log "Returning because..." but contain no return statement. Execution falls through unconditionally into the SQL. If textToReplaceWith is null, the replace(col, ?, ?) calls receive null, which in PostgreSQL causes the entire column value to be set to NULL — silent data destruction.

The REST endpoint is currently protected by @NotNull on replaceString, but that is a fragile implicit dependency on form validation to keep a broken utility method from corrupting data. Either fix DBSearchAndReplace to actually return early, or add an explicit null guard in the endpoint. Fix this →


2. dropOldVersions returns HTTP 200 when the operation fails

MaintenanceResource.java:654-660 — when CMSMaintenanceFactory.deleteOldAssetVersions() catches an exception it rolls back and returns -1. The endpoint wraps this as {"deletedCount": -1, "success": false} with a 200 OK. Callers have to inspect the body to detect failure rather than relying on the HTTP status code. The same applies to searchAndReplace when hasErrors is true.


3. searchString is written to the SecurityLogger

MaintenanceResource.java:570-573 logs the full form.getSearchString() value. If an admin is using search-and-replace to clean up a leaked API key or credential from content, that value gets persisted in the security audit log — the one place it should not land. Log the operation and user identity, not the search term.


4. Redundant dual-field error encoding in SearchAndReplaceResultView

AbstractSearchAndReplaceResultView has both success() and hasErrors() where success = !hasErrors always. This is the same bit expressed twice. Drop one — hasErrors is more informative since it directly signals partial failure. Fix this →


5. Inconsistent exception types for validation errors

A null replaceString (key absent from JSON body) triggers ValidationException via @NotNull in super.checkValid(). An empty searchString triggers BadRequestException from the manual check in checkValid(). These produce different response shapes for what are both 400-level input errors. Callers cannot rely on a single error format.


Minor

  • No test for the -1 / success:false path in dropOldVersions — only the happy path is covered.
  • No length limit on searchString/replaceString — a very large string builds a 25-column UPDATE with 50 bind parameters. No application-level guard exists.
  • DBSearchAndReplace with empty replaceString is fine — SQL replace(col, search, '') correctly deletes occurrences, and the test at line 95-106 covers it.

@hassandotcms hassandotcms changed the base branch from 35191-feature-maintenance-tools-rest-apis to main April 7, 2026 13:08
@github-actions github-actions bot added the Area : Backend PR changes Java/Maven backend code label Apr 7, 2026
@hassandotcms hassandotcms force-pushed the 35199-maintenance-search-replace-old-versions-pushed-assets branch from b797ebe to 65d3572 Compare April 7, 2026 14:53
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

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[TASK] Implement delete pushed assets endpoint [TASK] Implement drop old versions endpoint [TASK] Implement search and replace endpoint

1 participant