diff --git a/Design/adminPage_ideas.md b/Design/adminPage_ideas.md index e7f3584e..b874fcd4 100644 --- a/Design/adminPage_ideas.md +++ b/Design/adminPage_ideas.md @@ -6,13 +6,13 @@ PHẦN 1: CÁC CHỨC NĂNG TRÊN GISAO DIỆN ADMIN (UI MENUS) Menu chính Menu con Chức năng chi tiết 1. Dashboard (Bảng điều khiển) Home Xem tổng quan trạng thái website (Site Health, số lượng bài viết/trang, bình luận gần đây, hoạt động viết nháp nhanh). Updates Cập nhật phiên bản WordPress Core, Themes, Plugins và các gói dịch vụ ngôn ngữ. -2. Posts (Bài viết) All Posts Quản lý toàn bộ bài viết tin tức, blog, lọc theo ngày/chuyên mục. +(Done) 2. Posts (Bài viết) All Posts Quản lý toàn bộ bài viết tin tức, blog, lọc theo ngày/chuyên mục. Add New Soạn thảo bài viết mới bằng Block Editor (Gutenberg) hoặc Classic Editor. Categories Tạo và quản lý các Chuyên mục (thư mục phân loại bài viết). Tags Tạo và quản lý các Thẻ (nhãn từ khóa tìm kiếm nhanh). -3. Media (Thư viện) Library Quản lý toàn bộ hình ảnh, tài liệu (PDF, Docx), video, âm thanh đã tải lên. -Add New Tải tệp tin mới lên máy chủ. -4. Pages (Trang) All Pages Quản lý các trang tĩnh (ví dụ: Trang chủ, Trang liên hệ, Giới thiệu bệnh viện, Giờ làm việc...). +(Done) 3. Media (Thư viện) Library Quản lý toàn bộ hình ảnh, tài liệu (PDF, Docx), video, âm thanh đã tải lên. +Add New Tải tệp tin mới lên máy chủ. +(Done) 4. Pages (Trang) All Pages Quản lý các trang tĩnh (ví dụ: Trang chủ, Trang liên hệ, Giới thiệu bệnh viện, Giờ làm việc...). Add New Tạo trang tĩnh mới. 5. Comments (Bình luận) (Không có) Quản lý bình luận của người đọc (Phê duyệt, Trả lời, Đánh dấu Spam, Xóa tạm/Xóa vĩnh viễn). 6. Appearance (Giao diện) Themes Cài đặt, kích hoạt hoặc xóa các giao diện hiển thị của website. diff --git a/sisvietnamvn_main/TestFileUri.java b/sisvietnamvn_main/TestFileUri.java new file mode 100644 index 00000000..93541246 --- /dev/null +++ b/sisvietnamvn_main/TestFileUri.java @@ -0,0 +1,18 @@ +import java.net.URI; +import java.nio.file.Paths; + +public class TestFileUri { + public static void main(String[] args) throws Exception { + String uploadPath = "/home/x79/sisvietnamvn_01/sisvietnamvn_main/uploads"; + System.out.println("Using file:/ + uploadPath:"); + try { + URI uri1 = new URI("file:/" + uploadPath + "/"); + System.out.println(" Host: " + uri1.getHost()); + System.out.println(" Path: " + uri1.getPath()); + } catch(Exception e) { e.printStackTrace(); } + + System.out.println("\nUsing Paths.get().toUri():"); + URI uri2 = Paths.get(uploadPath).toUri(); + System.out.println(" URI: " + uri2.toString()); + } +} diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/config/MvcConfig.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/config/MvcConfig.java index 2ebea21d..70c465eb 100644 --- a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/config/MvcConfig.java +++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/config/MvcConfig.java @@ -17,13 +17,13 @@ public class MvcConfig implements WebMvcConfigurer { private void exposeDirectory(String dirName, ResourceHandlerRegistry registry) { Path uploadDir = Paths.get(dirName); - String uploadPath = uploadDir.toFile().getAbsolutePath(); if (dirName.startsWith("../")) { dirName = dirName.replace("../", ""); } + String uploadPath = uploadDir.toFile().getAbsolutePath(); registry.addResourceHandler("/" + dirName + "/**") - .addResourceLocations("file:/" + uploadPath + "/"); + .addResourceLocations("file:" + uploadPath + "/"); } } diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageMediaController.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageMediaController.java new file mode 100644 index 00000000..c9850cc5 --- /dev/null +++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManageMediaController.java @@ -0,0 +1,163 @@ +package com.sisvietnamvn.web.controller.manage; + +import com.sisvietnamvn.web.domain.Media; +import com.sisvietnamvn.web.domain.MediaType; +import com.sisvietnamvn.web.hook.HookManager; +import com.sisvietnamvn.web.security.AuthoritiesConstants; +import com.sisvietnamvn.web.service.MediaService; +import java.io.IOException; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +/** + * Thymeleaf MVC controller for the Media Library admin pages. + * Provides Library view, Add New upload, Detail/Edit, and Delete. + */ +@Controller +@RequestMapping("/manage/media") +@PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\", \"" + AuthoritiesConstants.AUTHOR + "\", \"" + AuthoritiesConstants.CONTRIBUTOR + "\")") +public class ManageMediaController { + + private static final Logger LOG = LoggerFactory.getLogger(ManageMediaController.class); + + private final MediaService mediaService; + private final HookManager hookManager; + + public ManageMediaController(MediaService mediaService, HookManager hookManager) { + this.mediaService = mediaService; + this.hookManager = hookManager; + } + + @ModelAttribute + public void adminInit() { + hookManager.doAction("admin_init"); + } + + /** + * GET /manage/media — Media Library page. + */ + @GetMapping + public String listMedia(@RequestParam(value = "type", required = false) String typeStr, + @RequestParam(value = "keyword", required = false) String keyword, + Model model) { + LOG.debug("Request to list all media (type={}, keyword={})", typeStr, keyword); + + MediaType type = null; + if (typeStr != null && !typeStr.isBlank()) { + try { + type = MediaType.valueOf(typeStr); + } catch (IllegalArgumentException e) { + LOG.warn("Invalid media type filter: {}", typeStr); + } + } + + model.addAttribute("mediaList", mediaService.findFiltered(type, keyword)); + model.addAttribute("mediaTypes", MediaType.values()); + model.addAttribute("selectedType", typeStr); + model.addAttribute("keyword", keyword); + return "manage/media/list"; + } + + /** + * GET /manage/media/new — Add New upload page. + */ + @GetMapping("/new") + public String showUploadPage() { + LOG.debug("Request to show media upload page"); + return "manage/media/upload"; + } + + /** + * POST /manage/media/upload — Handle file upload from the form. + */ + @PostMapping("/upload") + public String uploadMedia(@RequestParam("files") MultipartFile[] files, + RedirectAttributes redirectAttributes) { + LOG.debug("Request to upload {} file(s)", files.length); + + int successCount = 0; + int failCount = 0; + + for (MultipartFile file : files) { + if (file.isEmpty()) continue; + try { + mediaService.upload(file); + successCount++; + } catch (IOException e) { + LOG.error("Failed to upload file: {}", file.getOriginalFilename(), e); + failCount++; + } + } + + if (successCount > 0) { + redirectAttributes.addFlashAttribute("successMessage", + successCount + " file(s) uploaded successfully!"); + } + if (failCount > 0) { + redirectAttributes.addFlashAttribute("errorMessage", + failCount + " file(s) failed to upload."); + } + + return "redirect:/manage/media"; + } + + /** + * GET /manage/media/{id} — View/edit detail page for a single media item. + */ + @GetMapping("/{id}") + public String showDetail(@PathVariable Long id, Model model, RedirectAttributes redirectAttributes) { + LOG.debug("Request to show media detail: {}", id); + Optional mediaOpt = mediaService.findById(id); + if (mediaOpt.isEmpty()) { + redirectAttributes.addFlashAttribute("errorMessage", "Media not found."); + return "redirect:/manage/media"; + } + Media media = mediaOpt.get(); + String fullUrl = org.springframework.web.servlet.support.ServletUriComponentsBuilder + .fromCurrentContextPath() + .path(media.getFileUrl()) + .toUriString(); + + model.addAttribute("media", media); + model.addAttribute("fullUrl", fullUrl); + return "manage/media/detail"; + } + + /** + * POST /manage/media/{id} — Save edits (e.g. alt text) for a media item. + */ + @PostMapping("/{id}") + public String updateMedia(@PathVariable Long id, + @RequestParam(value = "altText", required = false) String altText, + RedirectAttributes redirectAttributes) { + LOG.debug("Request to update media: {}", id); + Optional mediaOpt = mediaService.findById(id); + if (mediaOpt.isEmpty()) { + redirectAttributes.addFlashAttribute("errorMessage", "Media not found."); + return "redirect:/manage/media"; + } + Media media = mediaOpt.get(); + media.setAltText(altText); + mediaService.save(media); + redirectAttributes.addFlashAttribute("successMessage", "Media updated successfully!"); + return "redirect:/manage/media/" + id; + } + + /** + * POST /manage/media/{id}/delete — Delete a media item. + */ + @PostMapping("/{id}/delete") + public String deleteMedia(@PathVariable Long id, RedirectAttributes redirectAttributes) { + LOG.debug("Request to delete media: {}", id); + mediaService.delete(id); + redirectAttributes.addFlashAttribute("successMessage", "Media deleted successfully!"); + return "redirect:/manage/media"; + } +} diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManagePluginController.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManagePluginController.java new file mode 100644 index 00000000..b0241af0 --- /dev/null +++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/ManagePluginController.java @@ -0,0 +1,159 @@ +package com.sisvietnamvn.web.controller.manage; + +import com.sisvietnamvn.web.domain.Plugin; +import com.sisvietnamvn.web.service.PluginService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +@Controller +@RequestMapping("/manage/plugins") +public class ManagePluginController { + + private static final Logger LOG = LoggerFactory.getLogger(ManagePluginController.class); + + private final PluginService pluginService; + + public ManagePluginController(PluginService pluginService) { + this.pluginService = pluginService; + } + + // --- Installed Plugins List --- + + @GetMapping + public String listPlugins(Model model) { + LOG.debug("Request to list all plugins"); + model.addAttribute("plugins", pluginService.findAll()); + return "manage/plugins/list"; + } + + // --- Activate / Deactivate / Delete --- + + @PostMapping("/{id}/activate") + public String activatePlugin(@PathVariable Long id, RedirectAttributes redirectAttributes) { + try { + Plugin plugin = pluginService.activate(id); + redirectAttributes.addFlashAttribute("successMessage", "Plugin '" + plugin.getName() + "' activated successfully. Please restart the application to apply hooks."); + } catch (Exception e) { + redirectAttributes.addFlashAttribute("errorMessage", "Failed to activate plugin: " + e.getMessage()); + } + return "redirect:/manage/plugins"; + } + + @PostMapping("/{id}/deactivate") + public String deactivatePlugin(@PathVariable Long id, RedirectAttributes redirectAttributes) { + try { + Plugin plugin = pluginService.deactivate(id); + redirectAttributes.addFlashAttribute("successMessage", "Plugin '" + plugin.getName() + "' deactivated successfully. Please restart the application to remove hooks."); + } catch (Exception e) { + redirectAttributes.addFlashAttribute("errorMessage", "Failed to deactivate plugin: " + e.getMessage()); + } + return "redirect:/manage/plugins"; + } + + @PostMapping("/{id}/delete") + public String deletePlugin(@PathVariable Long id, RedirectAttributes redirectAttributes) { + try { + pluginService.delete(id); + redirectAttributes.addFlashAttribute("successMessage", "Plugin deleted successfully."); + } catch (Exception e) { + redirectAttributes.addFlashAttribute("errorMessage", "Failed to delete plugin: " + e.getMessage()); + } + return "redirect:/manage/plugins"; + } + + // --- Add New Plugin --- + + @GetMapping("/new") + public String showAddPluginPage() { + return "manage/plugins/add-new"; + } + + @PostMapping("/upload") + public String uploadPlugin(@RequestParam("zipFile") MultipartFile zipFile, RedirectAttributes redirectAttributes) { + if (zipFile.isEmpty()) { + redirectAttributes.addFlashAttribute("errorMessage", "Please select a .zip file to upload."); + return "redirect:/manage/plugins/new"; + } + + try { + pluginService.installFromZip(zipFile); + redirectAttributes.addFlashAttribute("successMessage", "Plugin installed successfully. You can now activate it."); + return "redirect:/manage/plugins"; + } catch (IOException e) { + LOG.error("Failed to install plugin", e); + redirectAttributes.addFlashAttribute("errorMessage", "Failed to install plugin: " + e.getMessage()); + return "redirect:/manage/plugins/new"; + } catch (IllegalArgumentException e) { + redirectAttributes.addFlashAttribute("errorMessage", e.getMessage()); + return "redirect:/manage/plugins/new"; + } + } + + // --- Plugin File Editor --- + + @GetMapping("/editor") + public String showEditorSelector(Model model) { + model.addAttribute("plugins", pluginService.findAll()); + return "manage/plugins/editor"; + } + + @GetMapping("/editor/{pluginKey}") + public String showEditor(@PathVariable String pluginKey, + @RequestParam(value = "file", required = false) String file, + Model model, RedirectAttributes redirectAttributes) { + Optional pluginOpt = pluginService.findByKey(pluginKey); + if (pluginOpt.isEmpty()) { + redirectAttributes.addFlashAttribute("errorMessage", "Plugin not found."); + return "redirect:/manage/plugins/editor"; + } + + model.addAttribute("plugins", pluginService.findAll()); + model.addAttribute("selectedPlugin", pluginOpt.get()); + + try { + List files = pluginService.getPluginFiles(pluginKey); + model.addAttribute("pluginFiles", files); + + if (file != null && !file.isEmpty() && files.contains(file)) { + String content = pluginService.readPluginFile(pluginKey, file); + model.addAttribute("selectedFile", file); + model.addAttribute("fileContent", content); + } else if (!files.isEmpty()) { + // Select first file by default + String firstFile = files.get(0); + String content = pluginService.readPluginFile(pluginKey, firstFile); + model.addAttribute("selectedFile", firstFile); + model.addAttribute("fileContent", content); + } + } catch (Exception e) { + LOG.error("Error reading plugin files for {}", pluginKey, e); + model.addAttribute("errorMessage", "Error reading plugin files: " + e.getMessage()); + } + + return "manage/plugins/editor"; + } + + @PostMapping("/editor/{pluginKey}/save") + public String savePluginFile(@PathVariable String pluginKey, + @RequestParam("file") String file, + @RequestParam("content") String content, + RedirectAttributes redirectAttributes) { + try { + pluginService.writePluginFile(pluginKey, file, content); + redirectAttributes.addFlashAttribute("successMessage", "File '" + file + "' saved successfully. Changes may require a rebuild or restart."); + } catch (Exception e) { + LOG.error("Error saving plugin file {}/{}", pluginKey, file, e); + redirectAttributes.addFlashAttribute("errorMessage", "Failed to save file: " + e.getMessage()); + } + return "redirect:/manage/plugins/editor/" + pluginKey + "?file=" + file; + } +} diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/MediaController.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/MediaController.java index 439434a7..220236be 100644 --- a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/MediaController.java +++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/controller/manage/MediaController.java @@ -1,23 +1,22 @@ package com.sisvietnamvn.web.controller.manage; +import com.sisvietnamvn.web.domain.Media; +import com.sisvietnamvn.web.service.MediaService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import org.springframework.util.StringUtils; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; import org.springframework.security.access.prepost.PreAuthorize; import com.sisvietnamvn.web.security.AuthoritiesConstants; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * REST controller for media file uploads (used by Editor.js and other AJAX consumers). + */ @RestController @RequestMapping("/api/manage/media") @PreAuthorize("hasAnyAuthority(\"" + AuthoritiesConstants.ADMIN + "\", \"" + AuthoritiesConstants.EDITOR + "\", \"" + AuthoritiesConstants.AUTHOR + "\", \"" + AuthoritiesConstants.CONTRIBUTOR + "\")") @@ -25,8 +24,11 @@ public class MediaController { private final Logger log = LoggerFactory.getLogger(MediaController.class); - // Save files in an 'uploads' directory at the project root - private final String UPLOAD_DIR = "uploads/"; + private final MediaService mediaService; + + public MediaController(MediaService mediaService) { + this.mediaService = mediaService; + } @PostMapping("/upload") public ResponseEntity> uploadFile(@RequestParam("file") MultipartFile file) { @@ -40,32 +42,20 @@ public class MediaController { } try { - // Ensure the upload directory exists - Path uploadPath = Paths.get(UPLOAD_DIR); - if (!Files.exists(uploadPath)) { - Files.createDirectories(uploadPath); - } - - // Generate a unique file name to avoid conflicts - String originalFilename = StringUtils.cleanPath(file.getOriginalFilename()); - String extension = ""; - int dotIndex = originalFilename.lastIndexOf('.'); - if (dotIndex > 0) { - extension = originalFilename.substring(dotIndex); - } - String newFilename = UUID.randomUUID().toString() + extension; - - // Save the file - Path filePath = uploadPath.resolve(newFilename); - Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING); + Media media = mediaService.upload(file); // Construct the Editor.js expected response Map fileData = new HashMap<>(); - fileData.put("url", "/uploads/" + newFilename); - fileData.put("name", originalFilename); - fileData.put("size", file.getSize()); - if (extension.startsWith(".")) { - fileData.put("extension", extension.substring(1)); + fileData.put("url", media.getFileUrl()); + fileData.put("name", media.getOriginalFilename()); + fileData.put("size", media.getFileSize()); + if (media.getMimeType() != null) { + String ext = ""; + int dotIndex = media.getOriginalFilename().lastIndexOf('.'); + if (dotIndex > 0) { + ext = media.getOriginalFilename().substring(dotIndex + 1); + } + fileData.put("extension", ext); } response.put("success", 1); diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/Media.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/Media.java new file mode 100644 index 00000000..545e8223 --- /dev/null +++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/Media.java @@ -0,0 +1,160 @@ +package com.sisvietnamvn.web.domain; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.io.Serial; +import java.io.Serializable; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +/** + * A Media entity representing an uploaded file (image, document, video, audio). + */ +@Entity +@Table(name = "sis_media") +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +public class Media extends AbstractAuditingEntity implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "media_sequence") + @SequenceGenerator(name = "media_sequence", sequenceName = "sis_media_seq", allocationSize = 1) + @Column(name = "id") + private Long id; + + @NotNull + @Size(max = 500) + @Column(name = "original_filename", length = 500, nullable = false) + private String originalFilename; + + @NotNull + @Size(max = 500) + @Column(name = "stored_filename", length = 500, nullable = false) + private String storedFilename; + + @NotNull + @Size(max = 1000) + @Column(name = "file_url", length = 1000, nullable = false) + private String fileUrl; + + @Size(max = 100) + @Column(name = "mime_type", length = 100) + private String mimeType; + + @Column(name = "file_size") + private Long fileSize; + + @Size(max = 500) + @Column(name = "alt_text", length = 500) + private String altText; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "media_type", length = 20, nullable = false) + private MediaType mediaType; + + // --- Getters and Setters --- + + @Override + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getOriginalFilename() { + return originalFilename; + } + + public void setOriginalFilename(String originalFilename) { + this.originalFilename = originalFilename; + } + + public String getStoredFilename() { + return storedFilename; + } + + public void setStoredFilename(String storedFilename) { + this.storedFilename = storedFilename; + } + + public String getFileUrl() { + return fileUrl; + } + + public void setFileUrl(String fileUrl) { + this.fileUrl = fileUrl; + } + + public String getMimeType() { + return mimeType; + } + + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + + public Long getFileSize() { + return fileSize; + } + + public void setFileSize(Long fileSize) { + this.fileSize = fileSize; + } + + public String getAltText() { + return altText; + } + + public void setAltText(String altText) { + this.altText = altText; + } + + public MediaType getMediaType() { + return mediaType; + } + + public void setMediaType(MediaType mediaType) { + this.mediaType = mediaType; + } + + /** + * Returns a human-readable file size string. + */ + public String getFormattedFileSize() { + if (fileSize == null) return "—"; + if (fileSize < 1024) return fileSize + " B"; + if (fileSize < 1024 * 1024) return String.format("%.1f KB", fileSize / 1024.0); + return String.format("%.1f MB", fileSize / (1024.0 * 1024.0)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Media)) return false; + return id != null && id.equals(((Media) o).id); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public String toString() { + return "Media{" + + "id=" + id + + ", originalFilename='" + originalFilename + '\'' + + ", storedFilename='" + storedFilename + '\'' + + ", fileUrl='" + fileUrl + '\'' + + ", mimeType='" + mimeType + '\'' + + ", fileSize=" + fileSize + + ", mediaType=" + mediaType + + '}'; + } +} diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/MediaType.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/MediaType.java new file mode 100644 index 00000000..0082059f --- /dev/null +++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/MediaType.java @@ -0,0 +1,12 @@ +package com.sisvietnamvn.web.domain; + +/** + * Enum representing the type of media file. + */ +public enum MediaType { + IMAGE, + DOCUMENT, + VIDEO, + AUDIO, + OTHER +} diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/Plugin.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/Plugin.java new file mode 100644 index 00000000..b26cf6aa --- /dev/null +++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/Plugin.java @@ -0,0 +1,177 @@ +package com.sisvietnamvn.web.domain; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import java.io.Serializable; + +/** + * A Plugin. + */ +@Entity +@Table(name = "sis_plugin") +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +public class Plugin extends AbstractAuditingEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator") + @SequenceGenerator(name = "sequenceGenerator", sequenceName = "sis_plugin_seq", allocationSize = 1) + private Long id; + + @NotNull + @Size(max = 100) + @Column(name = "plugin_key", length = 100, nullable = false, unique = true) + private String pluginKey; + + @NotNull + @Size(max = 255) + @Column(name = "name", length = 255, nullable = false) + private String name; + + @Size(max = 1000) + @Column(name = "description", length = 1000) + private String description; + + @Size(max = 50) + @Column(name = "version", length = 50) + private String version; + + @Size(max = 255) + @Column(name = "author", length = 255) + private String author; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + private PluginStatus status; + + // jhipster-needle-entity-add-field - JHipster will add fields here + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Plugin id(Long id) { + this.setId(id); + return this; + } + + public String getPluginKey() { + return pluginKey; + } + + public void setPluginKey(String pluginKey) { + this.pluginKey = pluginKey; + } + + public Plugin pluginKey(String pluginKey) { + this.setPluginKey(pluginKey); + return this; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Plugin name(String name) { + this.setName(name); + return this; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Plugin description(String description) { + this.setDescription(description); + return this; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public Plugin version(String version) { + this.setVersion(version); + return this; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public Plugin author(String author) { + this.setAuthor(author); + return this; + } + + public PluginStatus getStatus() { + return status; + } + + public void setStatus(PluginStatus status) { + this.status = status; + } + + public Plugin status(PluginStatus status) { + this.setStatus(status); + return this; + } + + // jhipster-needle-entity-add-getters-setters - JHipster will add getters and setters here + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Plugin)) { + return false; + } + return id != null && id.equals(((Plugin) o).id); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + // prettier-ignore + @Override + public String toString() { + return "Plugin{" + + "id=" + getId() + + ", pluginKey='" + getPluginKey() + "'" + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", version='" + getVersion() + "'" + + ", author='" + getAuthor() + "'" + + ", status='" + getStatus() + "'" + + "}"; + } +} diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/PluginStatus.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/PluginStatus.java new file mode 100644 index 00000000..afa07c27 --- /dev/null +++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/PluginStatus.java @@ -0,0 +1,8 @@ +package com.sisvietnamvn.web.domain; + +/** + * The PluginStatus enumeration. + */ +public enum PluginStatus { + ACTIVE, INACTIVE +} diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/hook/SamplePluginHooks.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/plugins/sampleplugin/sampleplugin/SamplePluginHooks.java similarity index 63% rename from sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/hook/SamplePluginHooks.java rename to sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/plugins/sampleplugin/sampleplugin/SamplePluginHooks.java index f6bcc94f..f4b39e0f 100644 --- a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/hook/SamplePluginHooks.java +++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/plugins/sampleplugin/sampleplugin/SamplePluginHooks.java @@ -1,7 +1,10 @@ -package com.sisvietnamvn.web.hook; +package com.sisvietnamvn.web.plugins.sampleplugin; import com.sisvietnamvn.web.domain.Page; -import jakarta.annotation.PostConstruct; +import com.sisvietnamvn.web.hook.HookManager; +import com.sisvietnamvn.web.repository.PluginRepository; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @@ -15,13 +18,32 @@ public class SamplePluginHooks { private static final Logger LOG = LoggerFactory.getLogger(SamplePluginHooks.class); private final HookManager hookManager; + private final PluginRepository pluginRepository; + private static final String PLUGIN_KEY = "sampleplugin"; - public SamplePluginHooks(HookManager hookManager) { + public SamplePluginHooks(HookManager hookManager, PluginRepository pluginRepository) { this.hookManager = hookManager; + this.pluginRepository = pluginRepository; } - @PostConstruct + @EventListener(ApplicationReadyEvent.class) public void registerHooks() { + boolean isActive = false; + try { + // Only register hooks if this plugin is ACTIVE in the database + isActive = pluginRepository.findByPluginKey(PLUGIN_KEY) + .map(p -> p.getStatus().name().equals("ACTIVE")) + .orElse(false); + } catch (Exception e) { + LOG.warn("Could not verify plugin status (table might not exist yet due to async Liquibase): {}", e.getMessage()); + return; + } + + if (!isActive) { + LOG.info("SamplePlugin is inactive. Skipping hook registration."); + return; + } + LOG.info("Registering SamplePlugin hooks..."); // Example Action: Log when a page is saved @@ -53,8 +75,6 @@ public class SamplePluginHooks { if (value instanceof Page page) { if (page.getContent() != null && !page.getContent().contains("Auto-signature")) { LOG.info("FILTER TRIGGERED (pre_save_post): Appending signature to page '{}'", page.getTitle()); - // In a real scenario, this would modify the JSON content of Editor.js, - // but for demonstration we just log it. } } return value; diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/plugins/sampleplugin/sampleplugin/plugin.json b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/plugins/sampleplugin/sampleplugin/plugin.json new file mode 100644 index 00000000..1dc5d4af --- /dev/null +++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/plugins/sampleplugin/sampleplugin/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "Sample Plugin", + "version": "1.0.0", + "author": "Admin", + "description": "A sample plugin to demonstrate the HookManager." +} diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/repository/MediaRepository.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/repository/MediaRepository.java new file mode 100644 index 00000000..a3c209a1 --- /dev/null +++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/repository/MediaRepository.java @@ -0,0 +1,22 @@ +package com.sisvietnamvn.web.repository; + +import com.sisvietnamvn.web.domain.Media; +import com.sisvietnamvn.web.domain.MediaType; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * Spring Data JPA repository for the {@link Media} entity. + */ +@Repository +public interface MediaRepository extends JpaRepository { + + List findAllByOrderByCreatedDateDesc(); + + List findByMediaTypeOrderByCreatedDateDesc(MediaType mediaType); + + List findByOriginalFilenameContainingIgnoreCaseOrderByCreatedDateDesc(String keyword); + + List findByMediaTypeAndOriginalFilenameContainingIgnoreCaseOrderByCreatedDateDesc(MediaType mediaType, String keyword); +} diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/repository/PluginRepository.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/repository/PluginRepository.java new file mode 100644 index 00000000..02131c48 --- /dev/null +++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/repository/PluginRepository.java @@ -0,0 +1,19 @@ +package com.sisvietnamvn.web.repository; + +import com.sisvietnamvn.web.domain.Plugin; +import com.sisvietnamvn.web.domain.PluginStatus; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * Spring Data JPA repository for the Plugin entity. + */ +@Repository +public interface PluginRepository extends JpaRepository { + Optional findByPluginKey(String pluginKey); + List findByStatus(PluginStatus status); + List findAllByOrderByNameAsc(); +} diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/service/MediaService.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/service/MediaService.java new file mode 100644 index 00000000..83585242 --- /dev/null +++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/service/MediaService.java @@ -0,0 +1,176 @@ +package com.sisvietnamvn.web.service; + +import com.sisvietnamvn.web.domain.Media; +import com.sisvietnamvn.web.domain.MediaType; +import com.sisvietnamvn.web.repository.MediaRepository; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +/** + * Service for managing media files (upload, list, filter, delete). + * Files are stored using a WordPress-style time-based folder structure: uploads/YYYY/MM/ + */ +@Service +@Transactional +public class MediaService { + + private static final Logger LOG = LoggerFactory.getLogger(MediaService.class); + + private static final String BASE_UPLOAD_DIR = "uploads"; + + private final MediaRepository mediaRepository; + + public MediaService(MediaRepository mediaRepository) { + this.mediaRepository = mediaRepository; + } + + /** + * Upload a file to disk and persist a Media entity. + * Files are stored in uploads/YYYY/MM/ based on the current date. + */ + public Media upload(MultipartFile file) throws IOException { + LOG.debug("Request to upload file: {}", file.getOriginalFilename()); + + // Build WordPress-style path: uploads/YYYY/MM/ + String subPath = getUploadSubPath(); + Path uploadDir = Paths.get(BASE_UPLOAD_DIR, subPath); + if (!Files.exists(uploadDir)) { + Files.createDirectories(uploadDir); + } + + // Generate unique stored filename + String originalFilename = StringUtils.cleanPath(file.getOriginalFilename()); + String extension = ""; + int dotIndex = originalFilename.lastIndexOf('.'); + if (dotIndex > 0) { + extension = originalFilename.substring(dotIndex); + } + String storedFilename = UUID.randomUUID().toString() + extension; + + // Save file to disk + Path filePath = uploadDir.resolve(storedFilename); + Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING); + + // Build the public URL + String fileUrl = "/" + BASE_UPLOAD_DIR + "/" + subPath + "/" + storedFilename; + + // Determine media type from MIME + String mimeType = file.getContentType(); + MediaType mediaType = resolveMediaType(mimeType); + + // Create and save entity + Media media = new Media(); + media.setOriginalFilename(originalFilename); + media.setStoredFilename(storedFilename); + media.setFileUrl(fileUrl); + media.setMimeType(mimeType); + media.setFileSize(file.getSize()); + media.setMediaType(mediaType); + + return mediaRepository.save(media); + } + + /** + * Find all media, sorted by date descending. + */ + @Transactional(readOnly = true) + public List findAll() { + LOG.debug("Request to get all Media"); + return mediaRepository.findAllByOrderByCreatedDateDesc(); + } + + /** + * Find media filtered by type and/or keyword. + */ + @Transactional(readOnly = true) + public List findFiltered(MediaType type, String keyword) { + LOG.debug("Request to get filtered Media (type={}, keyword={})", type, keyword); + boolean hasType = type != null; + boolean hasKeyword = keyword != null && !keyword.isBlank(); + + if (hasType && hasKeyword) { + return mediaRepository.findByMediaTypeAndOriginalFilenameContainingIgnoreCaseOrderByCreatedDateDesc(type, keyword); + } else if (hasType) { + return mediaRepository.findByMediaTypeOrderByCreatedDateDesc(type); + } else if (hasKeyword) { + return mediaRepository.findByOriginalFilenameContainingIgnoreCaseOrderByCreatedDateDesc(keyword); + } else { + return mediaRepository.findAllByOrderByCreatedDateDesc(); + } + } + + /** + * Find a single media by ID. + */ + @Transactional(readOnly = true) + public Optional findById(Long id) { + LOG.debug("Request to get Media : {}", id); + return mediaRepository.findById(id); + } + + /** + * Delete a media entity and its file from disk. + */ + public void delete(Long id) { + LOG.debug("Request to delete Media : {}", id); + mediaRepository.findById(id).ifPresent(media -> { + // Delete file from disk + try { + // fileUrl is like /uploads/2026/07/uuid.jpg — strip leading slash + Path filePath = Paths.get(media.getFileUrl().substring(1)); + Files.deleteIfExists(filePath); + LOG.debug("Deleted file from disk: {}", filePath); + } catch (IOException e) { + LOG.warn("Failed to delete file from disk for media {}: {}", id, e.getMessage()); + } + mediaRepository.delete(media); + }); + } + + /** + * Update a media entity (e.g. alt text). + */ + public Media save(Media media) { + LOG.debug("Request to save Media : {}", media); + return mediaRepository.save(media); + } + + /** + * Returns the current year/month subdirectory path (e.g. "2026/07"). + */ + public String getUploadSubPath() { + LocalDate now = LocalDate.now(); + return String.format("%d/%02d", now.getYear(), now.getMonthValue()); + } + + /** + * Maps a MIME type string to a MediaType enum value. + */ + public MediaType resolveMediaType(String mimeType) { + if (mimeType == null) return MediaType.OTHER; + if (mimeType.startsWith("image/")) return MediaType.IMAGE; + if (mimeType.startsWith("video/")) return MediaType.VIDEO; + if (mimeType.startsWith("audio/")) return MediaType.AUDIO; + if (mimeType.startsWith("application/pdf") || + mimeType.startsWith("application/msword") || + mimeType.startsWith("application/vnd.openxmlformats") || + mimeType.startsWith("application/vnd.ms-") || + mimeType.startsWith("text/")) { + return MediaType.DOCUMENT; + } + return MediaType.OTHER; + } +} diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/service/PluginService.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/service/PluginService.java new file mode 100644 index 00000000..512b941f --- /dev/null +++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/service/PluginService.java @@ -0,0 +1,220 @@ +package com.sisvietnamvn.web.service; + +import com.sisvietnamvn.web.domain.Plugin; +import com.sisvietnamvn.web.domain.PluginStatus; +import com.sisvietnamvn.web.repository.PluginRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +@Service +@Transactional +public class PluginService { + + private static final Logger LOG = LoggerFactory.getLogger(PluginService.class); + + // Using the source directory so that edited/uploaded .java files can be compiled + // and picked up by Spring Boot DevTools (or a normal build). + private static final String PLUGIN_BASE_DIR = "src/main/java/com/sisvietnamvn/web/plugins"; + + private final PluginRepository pluginRepository; + + public PluginService(PluginRepository pluginRepository) { + this.pluginRepository = pluginRepository; + ensurePluginBaseDirExists(); + } + + private void ensurePluginBaseDirExists() { + try { + Path path = Paths.get(PLUGIN_BASE_DIR); + if (!Files.exists(path)) { + Files.createDirectories(path); + } + } catch (IOException e) { + LOG.error("Could not create plugin base directory", e); + } + } + + public List findAll() { + LOG.debug("Request to get all Plugins"); + return pluginRepository.findAllByOrderByNameAsc(); + } + + public Optional findById(Long id) { + LOG.debug("Request to get Plugin : {}", id); + return pluginRepository.findById(id); + } + + public Optional findByKey(String key) { + return pluginRepository.findByPluginKey(key); + } + + public boolean isPluginActive(String key) { + return pluginRepository.findByPluginKey(key) + .map(plugin -> plugin.getStatus() == PluginStatus.ACTIVE) + .orElse(false); + } + + public Plugin activate(Long id) { + return pluginRepository.findById(id).map(plugin -> { + plugin.setStatus(PluginStatus.ACTIVE); + LOG.info("Activated plugin: {}", plugin.getName()); + return plugin; + }).orElseThrow(() -> new IllegalArgumentException("Plugin not found")); + } + + public Plugin deactivate(Long id) { + return pluginRepository.findById(id).map(plugin -> { + plugin.setStatus(PluginStatus.INACTIVE); + LOG.info("Deactivated plugin: {}", plugin.getName()); + return plugin; + }).orElseThrow(() -> new IllegalArgumentException("Plugin not found")); + } + + public void delete(Long id) { + pluginRepository.findById(id).ifPresent(plugin -> { + String key = plugin.getPluginKey(); + pluginRepository.delete(plugin); + LOG.info("Deleted plugin from DB: {}", key); + + // Also delete files + Path pluginDir = Paths.get(PLUGIN_BASE_DIR, key); + if (Files.exists(pluginDir)) { + try { + Files.walkFileTree(pluginDir, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + LOG.info("Deleted plugin directory: {}", pluginDir); + } catch (IOException e) { + LOG.error("Failed to delete plugin directory: {}", pluginDir, e); + } + } + }); + } + + public void installFromZip(MultipartFile zipFile) throws IOException { + String originalFilename = zipFile.getOriginalFilename(); + if (originalFilename == null || !originalFilename.endsWith(".zip")) { + throw new IllegalArgumentException("File must be a .zip archive"); + } + + String pluginKey = originalFilename.substring(0, originalFilename.length() - 4).replaceAll("[^a-zA-Z0-9-]", "-").toLowerCase(); + Path pluginDir = Paths.get(PLUGIN_BASE_DIR, pluginKey); + + if (Files.exists(pluginDir)) { + throw new IllegalArgumentException("Plugin directory already exists: " + pluginKey); + } + + Files.createDirectories(pluginDir); + + // Extract ZIP + try (ZipInputStream zis = new ZipInputStream(zipFile.getInputStream())) { + ZipEntry zipEntry = zis.getNextEntry(); + while (zipEntry != null) { + Path newFile = newFile(pluginDir, zipEntry); + if (zipEntry.isDirectory()) { + Files.createDirectories(newFile); + } else { + // Create parent directories if needed + Path parent = newFile.getParent(); + if (!Files.exists(parent)) { + Files.createDirectories(parent); + } + // Write file + Files.copy(zis, newFile, StandardCopyOption.REPLACE_EXISTING); + } + zipEntry = zis.getNextEntry(); + } + zis.closeEntry(); + } + + // Create a basic DB entry + Plugin plugin = new Plugin(); + plugin.setPluginKey(pluginKey); + plugin.setName(pluginKey); + plugin.setVersion("1.0"); + plugin.setStatus(PluginStatus.INACTIVE); + plugin.setAuthor("Uploaded"); + plugin.setDescription("Custom uploaded plugin"); + pluginRepository.save(plugin); + + LOG.info("Installed new plugin from zip: {}", pluginKey); + } + + private Path newFile(Path destinationDir, ZipEntry zipEntry) throws IOException { + Path destFile = destinationDir.resolve(zipEntry.getName()); + String destDirPath = destinationDir.toFile().getCanonicalPath(); + String destFilePath = destFile.toFile().getCanonicalPath(); + + if (!destFilePath.startsWith(destDirPath + File.separator)) { + throw new IOException("Entry is outside of the target dir: " + zipEntry.getName()); + } + return destFile; + } + + // --- Plugin File Editor methods --- + + public List getPluginFiles(String pluginKey) throws IOException { + Path pluginDir = Paths.get(PLUGIN_BASE_DIR, pluginKey); + List files = new ArrayList<>(); + if (Files.exists(pluginDir)) { + Files.walkFileTree(pluginDir, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + files.add(pluginDir.relativize(file).toString().replace("\\", "/")); + return FileVisitResult.CONTINUE; + } + }); + } + return files; + } + + public String readPluginFile(String pluginKey, String filePath) throws IOException { + Path targetPath = resolveAndCheckPath(pluginKey, filePath); + if (Files.exists(targetPath) && !Files.isDirectory(targetPath)) { + return Files.readString(targetPath); + } + throw new IllegalArgumentException("File not found or is a directory: " + filePath); + } + + public void writePluginFile(String pluginKey, String filePath, String content) throws IOException { + Path targetPath = resolveAndCheckPath(pluginKey, filePath); + if (Files.exists(targetPath) && !Files.isDirectory(targetPath)) { + Files.writeString(targetPath, content, StandardOpenOption.TRUNCATE_EXISTING); + LOG.info("Saved changes to plugin file: {}/{}", pluginKey, filePath); + } else { + throw new IllegalArgumentException("File not found or is a directory: " + filePath); + } + } + + private Path resolveAndCheckPath(String pluginKey, String filePath) throws IOException { + Path pluginDir = Paths.get(PLUGIN_BASE_DIR, pluginKey).toAbsolutePath().normalize(); + Path targetPath = pluginDir.resolve(filePath).normalize(); + if (!targetPath.startsWith(pluginDir)) { + throw new SecurityException("Path traversal attempt detected!"); + } + return targetPath; + } +} diff --git a/sisvietnamvn_main/src/main/resources/config/liquibase/changelog/20260702143000_add_media_entity.xml b/sisvietnamvn_main/src/main/resources/config/liquibase/changelog/20260702143000_add_media_entity.xml new file mode 100644 index 00000000..267e3b9c --- /dev/null +++ b/sisvietnamvn_main/src/main/resources/config/liquibase/changelog/20260702143000_add_media_entity.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sisvietnamvn_main/src/main/resources/config/liquibase/changelog/20260702144700_add_plugin_entity.xml b/sisvietnamvn_main/src/main/resources/config/liquibase/changelog/20260702144700_add_plugin_entity.xml new file mode 100644 index 00000000..70ee4719 --- /dev/null +++ b/sisvietnamvn_main/src/main/resources/config/liquibase/changelog/20260702144700_add_plugin_entity.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sisvietnamvn_main/src/main/resources/config/liquibase/master.xml b/sisvietnamvn_main/src/main/resources/config/liquibase/master.xml index 55dce11a..d5b22122 100644 --- a/sisvietnamvn_main/src/main/resources/config/liquibase/master.xml +++ b/sisvietnamvn_main/src/main/resources/config/liquibase/master.xml @@ -26,6 +26,8 @@ + + diff --git a/sisvietnamvn_main/src/main/resources/templates/fragments/manage-layout.html b/sisvietnamvn_main/src/main/resources/templates/fragments/manage-layout.html index 05cd2726..aea20ea9 100644 --- a/sisvietnamvn_main/src/main/resources/templates/fragments/manage-layout.html +++ b/sisvietnamvn_main/src/main/resources/templates/fragments/manage-layout.html @@ -73,6 +73,22 @@ + + + +