diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/taillog/TailLogResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/taillog/TailLogResource.java index 3771b24bc1d9..7644ddb3ebfb 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/taillog/TailLogResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/taillog/TailLogResource.java @@ -5,9 +5,11 @@ import com.dotcms.rest.EmptyHttpResponse; import com.dotcms.rest.InitDataObject; +import com.dotcms.rest.ResponseEntityListStringView; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; import com.dotcms.util.CloseUtils; +import com.dotmarketing.business.Role; import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.servlets.taillog.Tailer; import com.dotmarketing.util.Config; @@ -16,6 +18,13 @@ import com.dotmarketing.util.SecurityLogger; import com.dotmarketing.util.ThreadUtils; import com.dotmarketing.util.UtilMethods; +import com.liferay.portal.model.Portlet; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.commons.io.input.TailerListenerAdapter; import org.glassfish.jersey.media.sse.EventOutput; @@ -24,6 +33,7 @@ import org.glassfish.jersey.server.JSONP; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; @@ -34,9 +44,12 @@ import java.io.File; import java.io.IOException; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * This resource provides the endpoint used by the LogViewer functionality to display backend server logs @@ -51,6 +64,94 @@ public class TailLogResource { //This is in seconds public static final int KEEP_ALIVE_EVENT_INTERVAL = Config.getIntProperty("KEEP_ALIVE_EVENT_INTERVAL",20); + /** + * Lists the names of log files available in the configured log folder, sorted alphabetically. + *
+ * The folder is resolved from the {@code TAIL_LOG_LOG_FOLDER} property (default + * {@code ./dotsecure/logs/}) and files are filtered by the regex configured in + * {@code TAIL_LOG_FILE_REGEX} (default {@code .*\.log$|.*\.out$}, matching the regex used + * by {@link #getLogs}). + *
+ * The returned names are paths relative to the log folder and are intended to be used as + * the {@code fileName} path parameter for {@code GET /api/v1/logs/{fileName}/_tail} and + * the {@code GET /api/v1/maintenance/_downloadLog/{fileName}} download endpoints. + *
+ * Auth note: this listing requires admin + Maintenance portlet access, which
+ * is stricter than {@link #getLogs} (back-end user only). The stricter check preserves the
+ * security perimeter of the legacy JSP scriptlet in {@code tail_log.jsp} that this endpoint
+ * replaces.
+ *
+ * @param request http request
+ * @param response http response
+ * @return a {@link ResponseEntityListStringView} wrapping the sorted list of log file names
+ */
+ @Operation(
+ summary = "List available log files",
+ description = "Returns log file names from the configured log folder (TAIL_LOG_LOG_FOLDER), "
+ + "filtered by TAIL_LOG_FILE_REGEX and sorted alphabetically. The names are used as "
+ + "the {fileName} path parameter for the tail and download endpoints."
+ )
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200",
+ description = "Sorted list of log file names",
+ content = @Content(mediaType = "application/json",
+ schema = @Schema(implementation = ResponseEntityListStringView.class))),
+ @ApiResponse(responseCode = "401",
+ description = "Unauthorized - authentication required",
+ content = @Content(mediaType = "application/json")),
+ @ApiResponse(responseCode = "403",
+ description = "Forbidden - CMS Administrator role and Maintenance portlet access required",
+ content = @Content(mediaType = "application/json"))
+ })
+ @GET
+ @Path("/")
+ @NoCache
+ @Produces({MediaType.APPLICATION_JSON})
+ public final ResponseEntityListStringView listLogFiles(
+ @Parameter(hidden = true) @Context final HttpServletRequest request,
+ @Parameter(hidden = true) @Context final HttpServletResponse response) {
+
+ new WebResource.InitBuilder(new WebResource())
+ .requiredBackendUser(true)
+ .requireAdmin(true)
+ .requestAndResponse(request, response)
+ .rejectWhenNoUser(true)
+ .requiredPortlet(Portlet.MAINTENANCE)
+ .init();
+
+ final String regex = Config.getStringProperty(
+ "TAIL_LOG_FILE_REGEX", ".*\\.log$|.*\\.out$");
+
+ String logPath = Config.getStringProperty(
+ "TAIL_LOG_LOG_FOLDER", "./dotsecure/logs/");
+ logPath = FileUtil.getAbsolutlePath(logPath);
+ if (!logPath.endsWith(File.separator)) {
+ logPath = logPath + File.separator;
+ }
+
+ final File logFolder = new File(logPath);
+ if (!logFolder.isDirectory()) {
+ Logger.debug(this, "Log folder does not exist or is not a directory: " + logPath);
+ return new ResponseEntityListStringView(new ArrayList<>());
+ }
+
+ final Pattern pattern = Pattern.compile(regex);
+ final String basePath = logPath;
+
+ final List The override is fully restored in {@code finally} so the test never pollutes the real
+ * dotsecure/logs directory or leaks state across tests.
+ */
+ @Test
+ public void test_listLogFiles_filtersAndSortsAndStripsBasePath() throws Exception {
+ final String originalLogFolder = Config.getStringProperty(
+ "TAIL_LOG_LOG_FOLDER", "./dotsecure/logs/");
+
+ final File tempLogFolder = Files.createTempDirectory("taillog-list-").toFile();
+ tempLogFolder.deleteOnExit();
+
+ final File firstMatch = plantLogFile(tempLogFolder, "aa-itlist.log");
+ final File secondMatch = plantLogFile(tempLogFolder, "zz-itlist.log");
+ final File notMatching = plantLogFile(tempLogFolder, "aa-itlist.txt");
+
+ try {
+ Config.setProperty("TAIL_LOG_LOG_FOLDER", tempLogFolder.getAbsolutePath());
+
+ final ResponseEntityListStringView result =
+ resource.listLogFiles(createRequestForUser(adminUser), mockResponse);
+
+ assertNotNull(result);
+ final List