diff --git a/notes-service/src/main/java/org/exoplatform/wiki/jpa/JPADataStorage.java b/notes-service/src/main/java/org/exoplatform/wiki/jpa/JPADataStorage.java index dce8081b7..65055efae8 100644 --- a/notes-service/src/main/java/org/exoplatform/wiki/jpa/JPADataStorage.java +++ b/notes-service/src/main/java/org/exoplatform/wiki/jpa/JPADataStorage.java @@ -1591,6 +1591,22 @@ public PageVersion getPageVersionById(long versionId) { return EntityConverter.convertPageVersionEntityToPageVersion(pageVersionDAO.find(versionId)); } + @Override + public PageVersion updatePageVersionContent(long versionId, String content) { + PageVersionEntity pageVersionEntity = pageVersionDAO.find(versionId); + pageVersionEntity.setContent(content); + pageVersionEntity.setUpdatedDate(new Date(System.currentTimeMillis())); + return EntityConverter.convertPageVersionEntityToPageVersion(pageVersionDAO.update(pageVersionEntity)); + } + + @Override + public DraftPage updateDraftContent(long draftId, String content) { + DraftPageEntity draftPageEntity = draftPageDAO.find(draftId); + draftPageEntity.setContent(content); + draftPageEntity.setUpdatedDate(new Date(System.currentTimeMillis())); + return EntityConverter.convertDraftPageEntityToDraftPage(draftPageDAO.update(draftPageEntity)); + } + /** * {@inheritDoc} */ diff --git a/notes-service/src/main/java/org/exoplatform/wiki/service/DataStorage.java b/notes-service/src/main/java/org/exoplatform/wiki/service/DataStorage.java index 5a0ba26cd..e44257f8c 100644 --- a/notes-service/src/main/java/org/exoplatform/wiki/service/DataStorage.java +++ b/notes-service/src/main/java/org/exoplatform/wiki/service/DataStorage.java @@ -319,6 +319,23 @@ public default List getAttachmentsOfPage(Page page, boolean loadCont */ PageVersion getPageVersionById(long versionId); + /** + * Updates page version content by its given id + * + * @param versionId page version id + * @param content page version content + * @return {@link PageVersion} + */ + PageVersion updatePageVersionContent(long versionId, String content) throws WikiException; + + /** + * Updates draft page content by its given id + * + * @param draftId draft page id + * @param content draft page content + * @return {@link PageVersion} + */ + DraftPage updateDraftContent(long draftId, String content) throws WikiException; /** * Gets draft pages of a given wiki diff --git a/notes-service/src/main/java/org/exoplatform/wiki/service/NoteService.java b/notes-service/src/main/java/org/exoplatform/wiki/service/NoteService.java index 65f504fbb..7141993dc 100644 --- a/notes-service/src/main/java/org/exoplatform/wiki/service/NoteService.java +++ b/notes-service/src/main/java/org/exoplatform/wiki/service/NoteService.java @@ -465,6 +465,18 @@ List getBreadCrumb(String noteType, */ void createVersionOfNote(Page note, String userName) throws WikiException; + /** + * Creates a version of a note. This method only tag the current note data as + * a new version, it does not update the note data + * + * @param note The note + * @param objectType The object type to link to the page version's content images + * @param draftId The ID of the latest draft, used as the source ID to move content images to the new version + * @param userName The author name + * @throws WikiException if an error occured + */ + void createVersionOfNote(Page note, String userName, String objectType, String draftId) throws WikiException; + /** * Restores a version of a note * @@ -591,7 +603,6 @@ DraftPage createDraftForExistPage(DraftPage draftNoteToSave, String revision, long currentTimeMillis, String username) throws WikiException; - /** * Creates a draft for a new page * diff --git a/notes-service/src/main/java/org/exoplatform/wiki/service/impl/NoteServiceImpl.java b/notes-service/src/main/java/org/exoplatform/wiki/service/impl/NoteServiceImpl.java index 081e5af4c..d8ce18f0e 100644 --- a/notes-service/src/main/java/org/exoplatform/wiki/service/impl/NoteServiceImpl.java +++ b/notes-service/src/main/java/org/exoplatform/wiki/service/impl/NoteServiceImpl.java @@ -41,9 +41,13 @@ import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; +import org.exoplatform.social.attachment.AttachmentService; +import org.exoplatform.social.attachment.model.UploadedAttachmentDetail; import org.gatein.api.EntityNotFoundException; import com.fasterxml.jackson.core.type.TypeReference; @@ -98,6 +102,7 @@ import org.exoplatform.wiki.service.search.SearchResult; import org.exoplatform.wiki.service.search.SearchResultType; import org.exoplatform.wiki.service.search.WikiSearchData; +import org.exoplatform.wiki.service.plugin.WikiDraftPageAttachmentPlugin; import org.exoplatform.wiki.utils.NoteConstants; import org.exoplatform.wiki.utils.Utils; @@ -109,7 +114,8 @@ import lombok.Getter; import lombok.SneakyThrows; -public class NoteServiceImpl implements NoteService { + + public class NoteServiceImpl implements NoteService { public static final String CACHE_NAME = "wiki.PageRenderingCache"; @@ -180,6 +186,8 @@ public class NoteServiceImpl implements NoteService { private final ImageThumbnailService imageThumbnailService; + private final AttachmentService attachmentService; + private final HTMLUploadImageProcessor htmlUploadImageProcessor; public NoteServiceImpl(DataStorage dataStorage, @@ -192,7 +200,8 @@ public NoteServiceImpl(DataStorage dataStorage, FileService fileService, UploadService uploadService, MetadataService metadataService, - ImageThumbnailService imageThumbnailService) { + ImageThumbnailService imageThumbnailService, + AttachmentService attachmentService) { this.dataStorage = dataStorage; this.wikiService = wikiService; this.identityManager = identityManager; @@ -205,6 +214,7 @@ public NoteServiceImpl(DataStorage dataStorage, this.uploadService = uploadService; this.metadataService = metadataService; this.imageThumbnailService = imageThumbnailService; + this.attachmentService = attachmentService; this.htmlUploadImageProcessor = null; } @@ -219,6 +229,7 @@ public NoteServiceImpl(DataStorage dataStorage, UploadService uploadService, MetadataService metadataService, ImageThumbnailService imageThumbnailService, + AttachmentService attachmentService, HTMLUploadImageProcessor htmlUploadImageProcessor) { this.dataStorage = dataStorage; this.wikiService = wikiService; @@ -232,6 +243,7 @@ public NoteServiceImpl(DataStorage dataStorage, this.uploadService = uploadService; this.metadataService = metadataService; this.imageThumbnailService = imageThumbnailService; + this.attachmentService = attachmentService; this.htmlUploadImageProcessor = htmlUploadImageProcessor; } @@ -260,7 +272,6 @@ public Page createNote(Wiki noteBook, String parentNoteName, Page note, Identity note.setContent(note.getContent()); Page createdPage = createNote(noteBook, parentPage, note); NotePageProperties properties = note.getProperties(); - String draftPageId = String.valueOf(properties != null && properties.isDraft() ? properties.getNoteId() : null); try { if (properties != null) { properties.setNoteId(Long.parseLong(createdPage.getId())); @@ -279,15 +290,6 @@ public Page createNote(Wiki noteBook, String parentNoteName, Page note, Identity createdPage.setCanImport(canImportNotes(note.getAuthor(), space, note)); createdPage.setCanView(canViewNotes(note.getAuthor(), space, note)); } - dataStorage.addPageVersion(createdPage, userIdentity.getUserId()); - PageVersion pageVersion = dataStorage.getPublishedVersionByPageIdAndLang(Long.valueOf(createdPage.getId()), createdPage.getLang()); - createdPage.setLatestVersionId(pageVersion != null ? pageVersion.getId() : null); - if (pageVersion != null && draftPageId != null) { - Map eventData = new HashMap<>(); - eventData.put("draftPageId", draftPageId); - eventData.put("pageVersionId", pageVersion.getId()); - Utils.broadcast(listenerService, "note.page.version.created", this, eventData); - } return createdPage; } else { throw new EntityNotFoundException("Parent note not found"); @@ -657,7 +659,6 @@ public DraftPage getDraftNoteById(String id, String userId) throws WikiException } DraftPage draftPage = dataStorage.getDraftPageById(id); computeDraftProps(draftPage, userId); - return draftPage; } @@ -1020,6 +1021,11 @@ public List getVersionsHistoryOfNote(Page note, String userName) th */ @Override public void createVersionOfNote(Page note, String userName) throws WikiException { + createVersionOfNote(note, userName, null, null); + } + + @Override + public void createVersionOfNote(Page note, String userName, String objectType, String draftId) throws WikiException { PageVersion pageVersion = dataStorage.addPageVersion(note, userName); String pageVersionId = pageVersion.getId(); note.setLatestVersionId(pageVersionId); @@ -1046,8 +1052,20 @@ public void createVersionOfNote(Page note, String userName) throws WikiException NOTE_METADATA_VERSION_PAGE_OBJECT_TYPE, userName); } - DraftPage draftPage = dataStorage.getLatestDraftPageByTargetPageAndLang(Long.valueOf(note.getId()), note.getLang()); + DraftPage draftPage = + StringUtils.isNotEmpty(draftId) ? dataStorage.getDraftPageById(draftId) + : dataStorage.getLatestDraftPageByTargetPageAndLang(Long.valueOf(note.getId()), + note.getLang()); if (draftPage != null) { + if (objectType != null) { + pageVersion.setId(pageVersionId); + pageVersion = processImagesOnPageVersionCreation(pageVersion, + objectType, + WikiDraftPageAttachmentPlugin.OBJECT_TYPE, + draftPage.getId(), + userName); + note.setContent(pageVersion.getContent()); + } Map eventData = new HashMap<>(); eventData.put("draftPageId", draftPage.getId()); eventData.put("pageVersionId", pageVersionId); @@ -1108,7 +1126,7 @@ public DraftPage updateDraftForExistPage(DraftPage draftNoteToUpdate, String username) throws WikiException { // Create suffix for draft name String draftSuffix = getDraftNameSuffix(clientTime); - + Long userIdentityId = Long.parseLong(identityManager.getOrCreateUserIdentity(username).getId()); DraftPage newDraftPage = new DraftPage(); newDraftPage.setId(draftNoteToUpdate.getId()); newDraftPage.setName(targetPage.getName() + "_" + draftSuffix); @@ -1116,7 +1134,7 @@ public DraftPage updateDraftForExistPage(DraftPage draftNoteToUpdate, newDraftPage.setTitle(draftNoteToUpdate.getTitle()); newDraftPage.setTargetPageId(draftNoteToUpdate.getTargetPageId()); newDraftPage.setParentPageId(draftNoteToUpdate.getParentPageId()); - newDraftPage.setContent(draftNoteToUpdate.getContent()); + newDraftPage.setContent(processImagesOnDraftUpdate(draftNoteToUpdate, userIdentityId)); newDraftPage.setLang(draftNoteToUpdate.getLang()); newDraftPage.setSyntax(draftNoteToUpdate.getSyntax()); newDraftPage.setCreatedDate(new Date(clientTime)); @@ -1167,7 +1185,7 @@ public DraftPage updateDraftForNewPage(DraftPage draftNoteToUpdate, long clientT newDraftPage.setTargetPageId(draftNoteToUpdate.getTargetPageId()); newDraftPage.setParentPageId(draftNoteToUpdate.getParentPageId()); newDraftPage.setTargetPageRevision("1"); - newDraftPage.setContent(draftNoteToUpdate.getContent()); + newDraftPage.setContent(processImagesOnDraftUpdate(draftNoteToUpdate, userIdentityId)); newDraftPage.setLang(draftNoteToUpdate.getLang()); newDraftPage.setSyntax(draftNoteToUpdate.getSyntax()); newDraftPage.setCreatedDate(new Date(clientTime)); @@ -1196,13 +1214,15 @@ public DraftPage createDraftForExistPage(DraftPage draftPage, // Create suffix for draft name String draftSuffix = getDraftNameSuffix(clientTime); + org.exoplatform.social.core.identity.model.Identity userIdentity = identityManager.getOrCreateUserIdentity(username); + DraftPage newDraftPage = new DraftPage(); newDraftPage.setName(targetPage.getName() + "_" + draftSuffix); newDraftPage.setNewPage(false); newDraftPage.setTitle(draftPage.getTitle()); + newDraftPage.setContent(draftPage.getContent()); newDraftPage.setTargetPageId(targetPage.getId()); newDraftPage.setParentPageId(draftPage.getParentPageId()); - newDraftPage.setContent(draftPage.getContent()); newDraftPage.setLang(draftPage.getLang()); newDraftPage.setSyntax(draftPage.getSyntax()); newDraftPage.setCreatedDate(new Date(clientTime)); @@ -1226,9 +1246,7 @@ public DraftPage createDraftForExistPage(DraftPage draftPage, featuredImage.setId(0L); } properties.setNoteId(Long.parseLong(newDraftPage.getId())); - properties = saveNoteMetadata(properties, - draftPage.getLang(), - Long.valueOf(identityManager.getOrCreateUserIdentity(username).getId())); + properties = saveNoteMetadata(properties, draftPage.getLang(), Long.valueOf(userIdentity.getId())); } } catch (Exception e) { log.error("Failed to save draft note metadata", e); @@ -1241,6 +1259,16 @@ public DraftPage createDraftForExistPage(DraftPage draftPage, eventData.put("pageVersionId", pageVersion.getId()); eventData.put("draftForExistingPageId", newDraftPage.getId()); Utils.broadcast(listenerService, "note.draft.for.exist.page.created", this, eventData); + processImagesOnDraftCreation(newDraftPage, pageVersion.getId(), Long.parseLong(userIdentity.getId())); + + } else { + // page version is null and the draft has a lang + // is a first draft for new translation + if (draftPage.getLang() != null) { + // fetch the version original (without lang) + PageVersion originalPagVersion = getPublishedVersionByPageIdAndLang(Long.valueOf(newDraftPage.getTargetPageId()), null); + processImagesOnDraftCreation(newDraftPage, originalPagVersion.getId(), Long.valueOf(newDraftPage.getTargetPageId())); + } } return newDraftPage; } @@ -1259,8 +1287,8 @@ public DraftPage createDraftForNewPage(DraftPage draftPage, long clientTime, lon newDraftPage.setTitle(draftPage.getTitle()); newDraftPage.setTargetPageId(draftPage.getTargetPageId()); newDraftPage.setTargetPageRevision("1"); - newDraftPage.setParentPageId(draftPage.getParentPageId()); newDraftPage.setContent(draftPage.getContent()); + newDraftPage.setParentPageId(draftPage.getParentPageId()); newDraftPage.setAuthor(draftPage.getAuthor()); newDraftPage.setLang(draftPage.getLang()); newDraftPage.setSyntax(draftPage.getSyntax()); @@ -1277,6 +1305,7 @@ public DraftPage createDraftForNewPage(DraftPage draftPage, long clientTime, lon log.error("Failed to save draft note metadata", e); } newDraftPage.setProperties(properties); + newDraftPage = processImagesOnDraftCreation(newDraftPage,null, userIdentityId); return newDraftPage; } @@ -2353,4 +2382,167 @@ private void saveImportedFeaturedImage(File featuredImage, Page note, long userI log.error("Error while saving imported featured image"); } } -} + + private PageVersion processImagesOnPageVersionCreation(PageVersion pageVersion, + String objectType, + String sourceObjectType, + String sourceObjectId, + String username) throws WikiException { + Long userIdentityId = Long.valueOf(identityManager.getOrCreateUserIdentity(username).getId()); + attachmentService.moveAttachments(sourceObjectType, sourceObjectId, objectType, pageVersion.getId(), null, userIdentityId); + String newContent = pageVersion.getContent() + .replaceAll("/attachments/" + sourceObjectType + "/" + sourceObjectId, + "/attachments/" + objectType + "/" + pageVersion.getId()); + if (!newContent.equals(pageVersion.getContent())) { + return updatePageVersionContent(Long.parseLong(pageVersion.getId()), newContent); + } + return pageVersion; + } + + private DraftPage processImagesOnDraftCreation(DraftPage draftPage, + String sourceObjectId, + long userIdentityId) throws WikiException { + String newDraftContent = saveUploadedContentImages(draftPage.getContent(), + WikiDraftPageAttachmentPlugin.OBJECT_TYPE, + draftPage.getId(), + userIdentityId); + boolean isDraftForExistingPage = StringUtils.isNotEmpty(sourceObjectId); + if (isDraftForExistingPage) { + String sourceObjectType = Utils.extractSourceObjectTypeFromHtml(draftPage.getContent()); + if (sourceObjectType != null) { + attachmentService.copyAttachments(sourceObjectType, + sourceObjectId, + WikiDraftPageAttachmentPlugin.OBJECT_TYPE, + draftPage.getId(), + null, + userIdentityId); + newDraftContent = newDraftContent.replaceAll("/attachments/" + sourceObjectType + "/" + + sourceObjectId, "/attachments/" + WikiDraftPageAttachmentPlugin.OBJECT_TYPE + "/" + draftPage.getId()); + + } + } + if (!newDraftContent.equals(draftPage.getContent())) { + draftPage.setContent(newDraftContent); + return updateDraftPageContent(Long.parseLong(draftPage.getId()), draftPage.getContent()); + } + return draftPage; + } + + private String processImagesOnDraftUpdate(DraftPage draftPage, long userIdentityId) { + try { + String content = saveUploadedContentImages(draftPage.getContent(), + WikiDraftPageAttachmentPlugin.OBJECT_TYPE, + draftPage.getId(), + userIdentityId); + // update the images list if any image is removed from the content + List existingFileIds = getContentImagesIds(content, WikiDraftPageAttachmentPlugin.OBJECT_TYPE, draftPage.getId()); + updateContentImages(existingFileIds, WikiDraftPageAttachmentPlugin.OBJECT_TYPE, draftPage.getId()); + + if (StringUtils.isNotEmpty(draftPage.getLang())) { + // trait the case of adding new translation, the draft created when the title is + // translated + // and updated when the content translated , so we need to copy attachments + String sourceObjectType = Utils.extractSourceObjectTypeFromHtml(draftPage.getContent()); + if (StringUtils.isNotEmpty(sourceObjectType)) { + String sourceObjectId = getPublishedVersionByPageIdAndLang(Long.parseLong(draftPage.getTargetPageId()), null).getId(); + attachmentService.copyAttachments(sourceObjectType, + sourceObjectId, + WikiDraftPageAttachmentPlugin.OBJECT_TYPE, + draftPage.getId(), + null, + userIdentityId); + + content = content.replaceAll("/attachments/" + sourceObjectType + "/" + sourceObjectId, + "/attachments/" + WikiDraftPageAttachmentPlugin.OBJECT_TYPE + "/" + draftPage.getId()); + } + } + return content; + } catch (Exception exception) { + return draftPage.getContent(); + } + } + + private String saveUploadedContentImages(String content, String objectType, String objectId, long userId) { + if (StringUtils.isEmpty(content)) { + return content; + } + try { + String regex = "]*cke_upload_id=\"([^\"]+)\"[^>]*>"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(content); + // Check if the pattern matches and extract the upload ID + while (matcher.find()) { + String uploadId = matcher.group(1); + UploadResource uploadResource = uploadService.getUploadResource(uploadId); + + if (uploadResource != null) { + UploadedAttachmentDetail uploadedAttachmentDetail = new UploadedAttachmentDetail(uploadResource); + attachmentService.saveAttachment(uploadedAttachmentDetail, objectType, objectId, null, userId); + + String fileId = uploadedAttachmentDetail.getId(); + String newSrc = String.format("src=\"/portal/rest/v1/social/attachments/%s/%s/%s\"", objectType, objectId, fileId); + // Replace the entire img tag with the new src + String newImgTag = matcher.group(0).replaceAll("cke_upload_id=\"[^\"]*\"", newSrc); + content = content.replace(matcher.group(0), newImgTag); + } + } + } catch (Exception e) { + log.error("Error while saving uploaded content images"); + return content; + } + return content; + } + + private void updateContentImages(List fileIds, String objectType, String objectId) { + List existingFiles = attachmentService.getAttachmentFileIds(objectType, objectId); + List removedFiles = existingFiles.stream().filter(item -> !fileIds.contains(item)).toList(); + if (CollectionUtils.isNotEmpty(removedFiles)) { + removedFiles.stream().forEach((item) -> { + try { + List metadataItemToDelete = + metadataService.getMetadataItemsByMetadataNameAndTypeAndObject(item, + AttachmentService.METADATA_TYPE.getName(), + objectType, + objectId, + 0, + 0); + + if (org.apache.commons.collections.CollectionUtils.isNotEmpty(metadataItemToDelete)) { + metadataItemToDelete.forEach(metadataItem -> { + try { + metadataService.deleteMetadataItem(metadataItem.getId(), true); + } catch (ObjectNotFoundException e) { + log.error("Error while updating content images", e); + } + }); + } + + } catch (Exception exception) { + log.error("Error while updating content images", exception); + } + }); + } + } + + private List getContentImagesIds(String content, String objectType, String objectId) { + String existingIdRegex = String.format("src=\"/portal/rest/v1/social/attachments/%s/%s/([^\"]+)\"", objectType, objectId); + Pattern existingPattern = Pattern.compile(existingIdRegex); + Matcher existingMatcher = existingPattern.matcher(content); + + List existingFileIds = new ArrayList<>(); + while (existingMatcher.find()) { + String fileId = existingMatcher.group(1); + existingFileIds.add(fileId); + } + return existingFileIds; + } + + private DraftPage updateDraftPageContent(long draftId, String content) throws WikiException { + return dataStorage.updateDraftContent(draftId, content); + } + + private PageVersion updatePageVersionContent(long versionId, String content) throws WikiException { + return dataStorage.updatePageVersionContent(versionId, content); + } + + } diff --git a/notes-service/src/main/java/org/exoplatform/wiki/service/plugin/WikiDraftPageAttachmentPlugin.java b/notes-service/src/main/java/org/exoplatform/wiki/service/plugin/WikiDraftPageAttachmentPlugin.java new file mode 100644 index 000000000..4481ab5ad --- /dev/null +++ b/notes-service/src/main/java/org/exoplatform/wiki/service/plugin/WikiDraftPageAttachmentPlugin.java @@ -0,0 +1,83 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.exoplatform.wiki.service.plugin; + +import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.services.security.Identity; +import org.exoplatform.social.attachment.AttachmentPlugin; +import org.exoplatform.social.core.manager.IdentityManager; +import org.exoplatform.social.core.space.spi.SpaceService; +import org.exoplatform.wiki.model.DraftPage; +import org.exoplatform.wiki.service.NoteService; +import org.exoplatform.wiki.utils.Utils; + +public class WikiDraftPageAttachmentPlugin extends AttachmentPlugin { + + private final NoteService noteService; + + private final SpaceService spaceService; + + public static final String OBJECT_TYPE = "wikiDraft"; + + public WikiDraftPageAttachmentPlugin(NoteService noteService, SpaceService spaceService, IdentityManager identityManager) { + this.noteService = noteService; + this.spaceService = spaceService; + } + + @Override + public String getObjectType() { + return OBJECT_TYPE; + } + + @Override + public boolean hasAccessPermission(Identity identity, String draftId) { + try { + DraftPage draftPage = noteService.getDraftNoteById(draftId, identity.getUserId()); + return draftPage != null && draftPage.isCanView(); + } catch (Exception e) { + return false; + } + } + + @Override + public boolean hasEditPermission(Identity identity, String draftId) throws ObjectNotFoundException { + try { + DraftPage draftPage = noteService.getDraftNoteById(draftId, identity.getUserId()); + return draftPage != null && draftPage.isCanManage(); + } catch (Exception e) { + return false; + } + } + + @Override + public long getAudienceId(String s) throws ObjectNotFoundException { + return 0; + } + + @Override + public long getSpaceId(String draftId) throws ObjectNotFoundException { + try { + String username = Utils.getCurrentUser(); + DraftPage draftPage = noteService.getDraftNoteById(draftId, username); + return Long.parseLong(spaceService.getSpaceByGroupId(draftPage.getWikiOwner()).getId()); + } catch (Exception exception) { + return 0; + } + } +} diff --git a/notes-service/src/main/java/org/exoplatform/wiki/service/plugin/WikiPageVersionAttachmentPlugin.java b/notes-service/src/main/java/org/exoplatform/wiki/service/plugin/WikiPageVersionAttachmentPlugin.java new file mode 100644 index 000000000..e959bea05 --- /dev/null +++ b/notes-service/src/main/java/org/exoplatform/wiki/service/plugin/WikiPageVersionAttachmentPlugin.java @@ -0,0 +1,83 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.exoplatform.wiki.service.plugin; + +import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.services.security.Identity; +import org.exoplatform.social.attachment.AttachmentPlugin; +import org.exoplatform.social.core.space.spi.SpaceService; +import org.exoplatform.wiki.model.Page; +import org.exoplatform.wiki.model.PageVersion; +import org.exoplatform.wiki.service.NoteService; + +public class WikiPageVersionAttachmentPlugin extends AttachmentPlugin { + + private final NoteService noteService; + + private final SpaceService spaceService; + + public static final String OBJECT_TYPE = "wikiPageVersion"; + + public WikiPageVersionAttachmentPlugin(NoteService noteService, SpaceService spaceService) { + this.noteService = noteService; + this.spaceService = spaceService; + } + + @Override + public String getObjectType() { + return OBJECT_TYPE; + } + + @Override + public boolean hasAccessPermission(Identity identity, String versionId) { + try { + PageVersion pageVersion = noteService.getPageVersionById(Long.parseLong(versionId)); + Page note = noteService.getNoteById(pageVersion.getParent().getId(),identity); + return pageVersion != null && note != null && note.isCanView(); + } catch (Exception e) { + return false; + } + } + + @Override + public boolean hasEditPermission(Identity identity, String versionId) throws ObjectNotFoundException { + try { + PageVersion pageVersion = noteService.getPageVersionById(Long.parseLong(versionId)); + Page note = noteService.getNoteById(pageVersion.getParent().getId(),identity); + return pageVersion != null && note != null && note.isCanManage(); + } catch (Exception e) { + return false; + } + } + + @Override + public long getAudienceId(String s) throws ObjectNotFoundException { + return 0; + } + + @Override + public long getSpaceId(String versionId) throws ObjectNotFoundException { + try { + PageVersion pageVersion = noteService.getPageVersionById(Long.parseLong(versionId)); + return Long.parseLong(spaceService.getSpaceByGroupId(pageVersion.getWikiOwner()).getId()); + } catch (Exception exception) { + return 0; + } + } +} diff --git a/notes-service/src/main/java/org/exoplatform/wiki/service/rest/NotesRestService.java b/notes-service/src/main/java/org/exoplatform/wiki/service/rest/NotesRestService.java index 027989d0d..030dba73f 100644 --- a/notes-service/src/main/java/org/exoplatform/wiki/service/rest/NotesRestService.java +++ b/notes-service/src/main/java/org/exoplatform/wiki/service/rest/NotesRestService.java @@ -57,6 +57,7 @@ import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; +import org.exoplatform.wiki.service.plugin.WikiPageVersionAttachmentPlugin; import org.gatein.api.EntityNotFoundException; import org.json.simple.JSONArray; import org.json.simple.JSONObject; @@ -508,6 +509,7 @@ public Response createNote(@Parameter(description = "note object to be created", } String noteBookType = note.getWikiType(); String noteBookOwner = note.getWikiOwner(); + String draftId = note.getProperties().isDraft() ? String.valueOf(note.getProperties().getNoteId()) : null; try { Identity identity = ConversationState.getCurrent().getIdentity(); if (StringUtils.isNotEmpty(note.getParentPageId())) { @@ -542,6 +544,7 @@ public Response createNote(@Parameter(description = "note object to be created", note.getParentPageName(), io.meeds.notes.rest.utils.EntityBuilder.toPage(note), identity); + noteService.createVersionOfNote(createdNote, currentUser, WikiPageVersionAttachmentPlugin.OBJECT_TYPE, draftId); return Response.ok(createdNote, MediaType.APPLICATION_JSON).cacheControl(cc).build(); } catch (IllegalAccessException e) { log.error("User does not have view permissions on the note {}", note.getName(), e); @@ -568,7 +571,7 @@ public Response saveDraft(@RequestBody(description = "Note draft page object to log.warn("Draft Note's title should not be number"); return Response.status(Response.Status.BAD_REQUEST).entity("{ message: Draft Note's title should not be number}").build(); } - + draftNoteToSave.setContent(Utils.sanitizeSrcImageTags(draftNoteToSave.getContent())); String noteBookType = draftNoteToSave.getWikiType(); String noteBookOwner = draftNoteToSave.getWikiOwner(); Page parentNote = null; @@ -662,6 +665,7 @@ public Response updateNote(@Parameter(description = "NoteBook Type", required = log.warn("Note's title should not be number"); return Response.status(Response.Status.BAD_REQUEST).entity("{ message: Note's title should not be number}").build(); } + note.setContent(Utils.sanitizeSrcImageTags(note.getContent())); try { if (noteBookType.toUpperCase().equals(WikiType.GROUP.name())) { noteBookOwner = formatWikiOwnerToGroupId(noteBookOwner); @@ -690,7 +694,7 @@ public Response updateNote(@Parameter(description = "NoteBook Type", required = note_.setName(newNoteName); } note_ = noteService.updateNote(note_, PageUpdateType.EDIT_PAGE_CONTENT_AND_TITLE, identity); - noteService.createVersionOfNote(note_, identity.getUserId()); + noteService.createVersionOfNote(note_, identity.getUserId(), WikiPageVersionAttachmentPlugin.OBJECT_TYPE, null); } else if (!note_.getTitle().equals(note.getTitle())) { String newNoteName = TitleResolver.getId(note.getTitle(), false); if (!NoteConstants.NOTE_HOME_NAME.equals(note.getName()) && !note.getName().equals(newNoteName)) { @@ -703,7 +707,7 @@ public Response updateNote(@Parameter(description = "NoteBook Type", required = } else if (!note_.getContent().equals(note.getContent())) { note_.setContent(note.getContent()); note_ = noteService.updateNote(note_, PageUpdateType.EDIT_PAGE_CONTENT, identity); - noteService.createVersionOfNote(note_, identity.getUserId()); + noteService.createVersionOfNote(note_, identity.getUserId(), WikiPageVersionAttachmentPlugin.OBJECT_TYPE, null); } return Response.ok(note_, MediaType.APPLICATION_JSON).cacheControl(cc).build(); } catch (IllegalAccessException e) { @@ -732,6 +736,7 @@ public Response updateNoteById(@Parameter(description = "Note id", required = tr log.warn("Note's title should not be number"); return Response.status(Response.Status.BAD_REQUEST).entity("{ message: Note's title should not be number}").build(); } + note.setContent(Utils.sanitizeSrcImageTags(note.getContent())); try { Identity identity = ConversationState.getCurrent().getIdentity(); Page note_ = noteService.getNoteById(noteId, identity); @@ -770,7 +775,7 @@ public Response updateNoteById(@Parameter(description = "Note id", required = tr note_.setContent(note.getContent()); note_.setProperties(notePageProperties); } - noteService.createVersionOfNote(note_, identity.getUserId()); + noteService.createVersionOfNote(note_, identity.getUserId(), WikiPageVersionAttachmentPlugin.OBJECT_TYPE, null); if (!Utils.ANONYM_IDENTITY.equals(identity.getUserId())) { WikiPageParams noteParams = new WikiPageParams(note_.getWikiType(), note_.getWikiOwner(), newNoteName); noteService.removeDraftOfNote(noteParams, note.getLang()); @@ -791,7 +796,7 @@ public Response updateNoteById(@Parameter(description = "Note id", required = tr note_.setTitle(note.getTitle()); note_.setProperties(notePageProperties); } - noteService.createVersionOfNote(note_, identity.getUserId()); + noteService.createVersionOfNote(note_, identity.getUserId(), WikiPageVersionAttachmentPlugin.OBJECT_TYPE, null); if (!Utils.ANONYM_IDENTITY.equals(identity.getUserId())) { WikiPageParams noteParams = new WikiPageParams(note_.getWikiType(), note_.getWikiOwner(), newNoteName); noteService.removeDraftOfNote(noteParams, note.getLang()); @@ -807,7 +812,7 @@ public Response updateNoteById(@Parameter(description = "Note id", required = tr note_.setContent(note.getContent()); note_.setProperties(notePageProperties); } - noteService.createVersionOfNote(note_, identity.getUserId()); + noteService.createVersionOfNote(note_, identity.getUserId(), WikiPageVersionAttachmentPlugin.OBJECT_TYPE, null); if (!Utils.ANONYM_IDENTITY.equals(identity.getUserId())) { WikiPageParams noteParams = new WikiPageParams(note_.getWikiType(), note_.getWikiOwner(), newNoteName); noteService.removeDraftOfNote(noteParams, note.getLang()); @@ -831,7 +836,7 @@ public Response updateNoteById(@Parameter(description = "Note id", required = tr note_ = noteService.updateNote(note_, PageUpdateType.PUBLISH, identity); } else if (note.isExtensionDataUpdated()) { note_ = noteService.updateNote(note_, PageUpdateType.EDIT_PAGE_CONTENT_AND_TITLE, identity); - noteService.createVersionOfNote(note_, identity.getUserId()); + noteService.createVersionOfNote(note_, identity.getUserId(), WikiPageVersionAttachmentPlugin.OBJECT_TYPE, null); if (!Utils.ANONYM_IDENTITY.equals(identity.getUserId())) { WikiPageParams noteParams = new WikiPageParams(note_.getWikiType(), note_.getWikiOwner(), newNoteName); noteService.removeDraftOfNote(noteParams, note.getLang()); diff --git a/notes-service/src/main/java/org/exoplatform/wiki/utils/Utils.java b/notes-service/src/main/java/org/exoplatform/wiki/utils/Utils.java index 956a0b71d..c45d80d83 100644 --- a/notes-service/src/main/java/org/exoplatform/wiki/utils/Utils.java +++ b/notes-service/src/main/java/org/exoplatform/wiki/utils/Utils.java @@ -40,14 +40,17 @@ import java.util.Objects; import java.util.ResourceBundle; import java.util.Set; +import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.apache.commons.lang3.StringUtils; +import org.exoplatform.wiki.service.plugin.WikiDraftPageAttachmentPlugin; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; import org.suigeneris.jrcs.diff.DifferentiationFailedException; import org.exoplatform.commons.api.notification.NotificationContext; @@ -793,5 +796,39 @@ public static void sendMentionInNoteNotification(Page note, Page originalNote, S .with(mentionNotificationCtx.makeCommand(PluginKey.key(MentionInNoteNotificationPlugin.ID))) .execute(mentionNotificationCtx); } - + public static String sanitizeSrcImageTags(String content) { + if (StringUtils.isEmpty(content)) { + return content; + } + try { + // Regular expression to find the src attribute with Base64 data in tags + String regex = "(]*?)(\\s+src=\"data:image/[^;]+;base64,[^\"]*\")"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(content); + + // Remove only the src attribute, keeping the tag and other attributes + content = matcher.replaceAll("$1"); + + } catch (Exception exception) { + return content; + } + return content; + } + + public static String extractSourceObjectTypeFromHtml(String htmlContent) { + Document document = Jsoup.parse(htmlContent); + for (Element imgTag : document.select("img")) { + String src = imgTag.attr("src"); + String prefix = "/portal/rest/v1/social/attachments/"; + // Ensure the src starts with the prefix + if (src.startsWith(prefix)) { + String srcPart = src.substring(prefix.length()); + String sourceObject = srcPart.substring(0, srcPart.indexOf('/')); + if (!sourceObject.equals(WikiDraftPageAttachmentPlugin.OBJECT_TYPE)) { + return sourceObject; + } + } + } + return null; + } } diff --git a/notes-service/src/test/java/org/exoplatform/wiki/service/TestNoteService.java b/notes-service/src/test/java/org/exoplatform/wiki/service/TestNoteService.java index 415a2ae91..58ca2a0ed 100644 --- a/notes-service/src/test/java/org/exoplatform/wiki/service/TestNoteService.java +++ b/notes-service/src/test/java/org/exoplatform/wiki/service/TestNoteService.java @@ -65,6 +65,8 @@ import io.meeds.notes.model.NoteFeaturedImage; import io.meeds.notes.model.NotePageProperties; +import org.exoplatform.wiki.service.plugin.WikiDraftPageAttachmentPlugin; +import org.exoplatform.wiki.service.plugin.WikiPageVersionAttachmentPlugin; public class TestNoteService extends BaseTest { private WikiService wService; @@ -1035,4 +1037,50 @@ public void testSaveHideAuthorAndHideReactionProperties() throws Exception { assertTrue(note.getProperties().isHideAuthor()); assertTrue(properties.isHideReaction()); } + + public void testProcessingNoteContentImages() throws Exception { + this.bindMockedUploadService(); + + // processing draft content images + DraftPage draftPage = new DraftPage(); + draftPage.setTitle("test"); + draftPage.setContent("content include image "); + draftPage = noteService.createDraftForNewPage(draftPage, new Date().getTime(), 1L); + assertNotNull(draftPage); + assertNotNull(draftPage.getContent()); + + String imageTagSuffix = " UTILS = mockStatic(org.exoplatform.wiki.utils.Utils.class); + + private WikiDraftPageAttachmentPlugin plugin; + + @Before + public void setUp() { + plugin = new WikiDraftPageAttachmentPlugin(noteService, spaceService, null); + } + + @AfterClass + public static void afterRunBare() { + UTILS.close(); + } + + @Test + public void testGetObjectType() { + Assert.assertEquals("wikiDraft", plugin.getObjectType()); + } + + @Test + public void testHasAccessPermission() throws Exception { + org.exoplatform.services.security.Identity userIdentity = mock(org.exoplatform.services.security.Identity.class); + DraftPage draftPage = mock(DraftPage.class); + + when(userIdentity.getUserId()).thenReturn("user123"); + when(noteService.getDraftNoteById("draft123", "user123")).thenReturn(draftPage); + when(draftPage.isCanView()).thenReturn(true); + + assertTrue(plugin.hasAccessPermission(userIdentity, "draft123")); + } + + @Test + public void testHasEditPermission() throws Exception { + org.exoplatform.services.security.Identity userIdentity = mock(org.exoplatform.services.security.Identity.class); + DraftPage draftPage = mock(DraftPage.class); + + when(userIdentity.getUserId()).thenReturn("user123"); + when(noteService.getDraftNoteById("draft123", "user123")).thenReturn(draftPage); + when(draftPage.isCanManage()).thenReturn(true); + + assertTrue(plugin.hasEditPermission(userIdentity, "draft123")); + } + + @Test + public void getSpaceId() throws Exception { + + UTILS.when(Utils::getCurrentUser).thenReturn("user123"); + DraftPage draftPage = mock(DraftPage.class); + when(draftPage.getWikiOwner()).thenReturn("spaces/test"); + Space space = mock(Space.class); + when(space.getId()).thenReturn("1"); + when(noteService.getDraftNoteById("draft123", "user123")).thenReturn(draftPage); + when(spaceService.getSpaceByGroupId(draftPage.getWikiOwner())).thenReturn(space); + assertNotNull(plugin.getSpaceId("draft123")); + } + +} diff --git a/notes-service/src/test/java/org/exoplatform/wiki/service/plugin/WikiPageVersionAttachmentPluginTest.java b/notes-service/src/test/java/org/exoplatform/wiki/service/plugin/WikiPageVersionAttachmentPluginTest.java new file mode 100644 index 000000000..fe6aefeba --- /dev/null +++ b/notes-service/src/test/java/org/exoplatform/wiki/service/plugin/WikiPageVersionAttachmentPluginTest.java @@ -0,0 +1,101 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.exoplatform.wiki.service.plugin; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.exoplatform.social.core.space.model.Space; +import org.exoplatform.social.core.space.spi.SpaceService; +import org.exoplatform.wiki.model.Page; +import org.exoplatform.wiki.model.PageVersion; +import org.exoplatform.wiki.service.NoteService; + +@RunWith(MockitoJUnitRunner.class) +public class WikiPageVersionAttachmentPluginTest { + + @Mock + private NoteService noteService; + + @Mock + private SpaceService spaceService; + + private WikiPageVersionAttachmentPlugin plugin; + + @Before + public void setUp() { + plugin = new WikiPageVersionAttachmentPlugin(noteService, spaceService); + } + + @Test + public void testGetObjectType() { + Assert.assertEquals(WikiPageVersionAttachmentPlugin.OBJECT_TYPE, plugin.getObjectType()); + } + + @Test + public void testHasAccessPermission() throws Exception { + org.exoplatform.services.security.Identity userIdentity = mock(org.exoplatform.services.security.Identity.class); + PageVersion pageVersion = mock(PageVersion.class); + Page page = mock(Page.class); + + when(noteService.getPageVersionById(1L)).thenReturn(pageVersion); + when(pageVersion.getParent()).thenReturn(page); + when(page.getId()).thenReturn("1"); + when(noteService.getNoteById("1", userIdentity)).thenReturn(page); + when(page.isCanView()).thenReturn(true); + assertTrue(plugin.hasAccessPermission(userIdentity, "1")); + } + + @Test + public void testHasEditPermission() throws Exception { + org.exoplatform.services.security.Identity userIdentity = mock(org.exoplatform.services.security.Identity.class); + PageVersion pageVersion = mock(PageVersion.class); + Page page = mock(Page.class); + + when(noteService.getPageVersionById(1L)).thenReturn(pageVersion); + when(pageVersion.getParent()).thenReturn(page); + when(page.getId()).thenReturn("1"); + when(noteService.getNoteById("1", userIdentity)).thenReturn(page); + when(page.isCanManage()).thenReturn(true); + assertTrue(plugin.hasEditPermission(userIdentity, "1")); + } + + @Test + public void getSpaceId() throws Exception { + PageVersion pageVersion = mock(PageVersion.class); + when(pageVersion.getWikiOwner()).thenReturn("spaces/test2"); + when(noteService.getPageVersionById(1L)).thenReturn(pageVersion); + Space space = mock(Space.class); + when(space.getId()).thenReturn("1"); + when(spaceService.getSpaceByGroupId(pageVersion.getWikiOwner())).thenReturn(space); + assertNotNull(plugin.getSpaceId("1")); + } + +} diff --git a/notes-webapp/src/main/webapp/WEB-INF/conf/configuration.xml b/notes-webapp/src/main/webapp/WEB-INF/conf/configuration.xml index 1d6256a8a..e91ca3f43 100644 --- a/notes-webapp/src/main/webapp/WEB-INF/conf/configuration.xml +++ b/notes-webapp/src/main/webapp/WEB-INF/conf/configuration.xml @@ -31,5 +31,7 @@ war:/conf/wiki/ckeditor-configuration.xml war:/conf/wiki/metadata-configuration.xml war:/conf/wiki/notification-configuration.xml + war:/conf/wiki/feature-flags-configuration.xml + war:/conf/wiki/component-plugin-configuration.xml diff --git a/notes-webapp/src/main/webapp/WEB-INF/conf/wiki/ckeditor/config.js b/notes-webapp/src/main/webapp/WEB-INF/conf/wiki/ckeditor/config.js index 2fa75bd03..2a93be722 100644 --- a/notes-webapp/src/main/webapp/WEB-INF/conf/wiki/ckeditor/config.js +++ b/notes-webapp/src/main/webapp/WEB-INF/conf/wiki/ckeditor/config.js @@ -20,12 +20,15 @@ CKEDITOR.editorConfig = function (config) { } CKEDITOR.plugins.addExternal('toc','/notes/javascript/eXo/wiki/ckeditor/plugins/toc/','plugin.js'); CKEDITOR.plugins.addExternal('linkBalloon', '/social/js/ckeditorPlugins/linkBalloon/', 'plugin.js'); + if (eXo.env.portal.insertImageOptionEnabled) { + CKEDITOR.plugins.addExternal('insertImage','/notes/javascript/eXo/wiki/ckeditor/plugins/insertImage/','plugin.js'); + } - const blocksToolbarGroup = [ + let blocksToolbarGroup = [ 'Blockquote', 'tagSuggester', 'emoji', - 'selectImage', + `${eXo.env.portal.insertImageOptionEnabled && 'insertImage' || 'selectImage'}`, 'Table', 'EmbedSemantic', 'CodeSnippet', @@ -76,19 +79,21 @@ CKEDITOR.editorConfig = function (config) { if (!webPageNote) { mobileToolbar[mobileToolbar.findIndex(item => item.name ==='blocks')].items.push('attachFile'); } - let extraPlugins = `a11ychecker,balloonpanel,indent,indentblock,indentlist,codesnippet,sharedspace,copyformatting,table,tabletools,embedsemantic,autolink,colordialog${!webPageNote && ',tagSuggester' || ''},emoji,link,font,justify,widget,${!webPageNote && ',insertOptions' || ''},contextmenu,tabletools,tableresize,toc,linkBalloon,suggester`; + let extraPlugins = `a11ychecker,balloonpanel,indent,indentblock,indentlist,codesnippet,sharedspace,copyformatting,table,tabletools,embedsemantic,autolink,colordialog${!webPageNote && ',tagSuggester' || ''},emoji,link,font,justify,widget,${!webPageNote && ',insertOptions' || ''},contextmenu,tabletools,tableresize,toc,linkBalloon,suggester, ${eXo.env.portal.insertImageOptionEnabled && 'image2, insertImage' || ''}`; let removePlugins = `image,confirmBeforeReload,maximize,resize,autoembed${webPageNote && ',tagSuggester' || ''}`; require(['SHARED/extensionRegistry'], function(extensionRegistry) { - const ckEditorExtensions = extensionRegistry.loadExtensions('WYSIWYGPlugins', 'image'); - if (ckEditorExtensions?.length) { - const ckEditorExtraPlugins = ckEditorExtensions.map(ckEditorExtension => ckEditorExtension.extraPlugin).join(','); - const ckEditorRemovePlugins = ckEditorExtensions.map(ckEditorExtension => ckEditorExtension.removePlugin).join(','); - if (ckEditorExtraPlugins) { - extraPlugins = `${extraPlugins},${ckEditorExtraPlugins}`; - } - if (ckEditorRemovePlugins) { - removePlugins = `${removePlugins},${ckEditorRemovePlugins}`; + if (!eXo.env.portal.insertImageOptionEnabled) { + const ckEditorExtensions = extensionRegistry.loadExtensions('WYSIWYGPlugins', 'image'); + if (ckEditorExtensions?.length) { + const ckEditorExtraPlugins = ckEditorExtensions.map(ckEditorExtension => ckEditorExtension.extraPlugin).join(','); + const ckEditorRemovePlugins = ckEditorExtensions.map(ckEditorExtension => ckEditorExtension.removePlugin).join(','); + if (ckEditorExtraPlugins) { + extraPlugins = `${extraPlugins},${ckEditorExtraPlugins}`; + } + if (ckEditorRemovePlugins) { + removePlugins = `${removePlugins},${ckEditorRemovePlugins}`; + } } } const notesEditorExtensions = extensionRegistry.loadExtensions('NotesEditor', 'ckeditor-extensions'); diff --git a/notes-webapp/src/main/webapp/WEB-INF/conf/wiki/component-plugin-configuration.xml b/notes-webapp/src/main/webapp/WEB-INF/conf/wiki/component-plugin-configuration.xml new file mode 100644 index 000000000..41b7e01dc --- /dev/null +++ b/notes-webapp/src/main/webapp/WEB-INF/conf/wiki/component-plugin-configuration.xml @@ -0,0 +1,38 @@ + + + + + + org.exoplatform.social.attachment.AttachmentService + + WikiDraftPageAttachmentPlugin + addPlugin + org.exoplatform.wiki.service.plugin.WikiDraftPageAttachmentPlugin + + + WikiPageVersionPlugin + addPlugin + org.exoplatform.wiki.service.plugin.WikiPageVersionAttachmentPlugin + + + \ No newline at end of file diff --git a/notes-webapp/src/main/webapp/WEB-INF/conf/wiki/feature-flags-configuration.xml b/notes-webapp/src/main/webapp/WEB-INF/conf/wiki/feature-flags-configuration.xml new file mode 100644 index 000000000..95d2df46c --- /dev/null +++ b/notes-webapp/src/main/webapp/WEB-INF/conf/wiki/feature-flags-configuration.xml @@ -0,0 +1,51 @@ + + + + + NotesEditorInsertImageOptionFeatureProperties + org.exoplatform.container.ExtendedPropertyConfigurator + + + NotesEditorInsertImageOptionFeatureProperties + Insert image Feature enablement flag + + + + + + org.exoplatform.groovyscript.text.TemplateService + + UIPortalApplication-head + addTemplateExtension + org.exoplatform.groovyscript.text.TemplateExtensionPlugin + + + templates + The list of templates to include in HTML Page Header with UIPortalApplication.gtmpl + war:/groovy/webui/workspace/UINotesHeadTemplate.gtmpl + + + + + \ No newline at end of file diff --git a/notes-webapp/src/main/webapp/groovy/webui/workspace/UINotesHeadTemplate.gtmpl b/notes-webapp/src/main/webapp/groovy/webui/workspace/UINotesHeadTemplate.gtmpl new file mode 100644 index 000000000..7c9bb98b1 --- /dev/null +++ b/notes-webapp/src/main/webapp/groovy/webui/workspace/UINotesHeadTemplate.gtmpl @@ -0,0 +1,10 @@ +<% + import org.exoplatform.commons.api.settings.ExoFeatureService; + def rcontext = _ctx.getRequestContext(); + ExoFeatureService featureService = uicomponent.getApplicationComponent(ExoFeatureService.class); + def userName = rcontext.getRemoteUser(); +%> + + \ No newline at end of file diff --git a/notes-webapp/src/main/webapp/javascript/eXo/wiki/ckeditor/plugins/insertImage/icons/insertImage.png b/notes-webapp/src/main/webapp/javascript/eXo/wiki/ckeditor/plugins/insertImage/icons/insertImage.png new file mode 100644 index 000000000..fcf61b5f2 Binary files /dev/null and b/notes-webapp/src/main/webapp/javascript/eXo/wiki/ckeditor/plugins/insertImage/icons/insertImage.png differ diff --git a/notes-webapp/src/main/webapp/javascript/eXo/wiki/ckeditor/plugins/insertImage/lang/en.js b/notes-webapp/src/main/webapp/javascript/eXo/wiki/ckeditor/plugins/insertImage/lang/en.js new file mode 100644 index 000000000..303b87606 --- /dev/null +++ b/notes-webapp/src/main/webapp/javascript/eXo/wiki/ckeditor/plugins/insertImage/lang/en.js @@ -0,0 +1,5 @@ +CKEDITOR.plugins.setLang('insertImage', 'en', + { + buttonTooltip: 'Insert image', + imageError: 'An error occurred while loading the image. Please try again' + }); \ No newline at end of file diff --git a/notes-webapp/src/main/webapp/javascript/eXo/wiki/ckeditor/plugins/insertImage/lang/fr.js b/notes-webapp/src/main/webapp/javascript/eXo/wiki/ckeditor/plugins/insertImage/lang/fr.js new file mode 100644 index 000000000..31c870c4c --- /dev/null +++ b/notes-webapp/src/main/webapp/javascript/eXo/wiki/ckeditor/plugins/insertImage/lang/fr.js @@ -0,0 +1,5 @@ +CKEDITOR.plugins.setLang('insertImage', 'fr', + { + buttonTooltip: 'Insérer une image', + imageError: 'Une erreur est survenue lors du chargement de l\u2019image. Veuillez essayer de nouveau.' + }); \ No newline at end of file diff --git a/notes-webapp/src/main/webapp/javascript/eXo/wiki/ckeditor/plugins/insertImage/plugin.js b/notes-webapp/src/main/webapp/javascript/eXo/wiki/ckeditor/plugins/insertImage/plugin.js new file mode 100644 index 000000000..6d4688427 --- /dev/null +++ b/notes-webapp/src/main/webapp/javascript/eXo/wiki/ckeditor/plugins/insertImage/plugin.js @@ -0,0 +1,168 @@ +'use strict'; +(function () { + CKEDITOR.plugins.add('insertImage', { + requires: 'uploadwidget,autogrow', + + onLoad: function () { + CKEDITOR.addCss( + '.cke_upload_uploading {' + + 'opacity: 0.3' + + '}' + + '.cke_widget_image {' + + ' max-width: 100%;' + + ' margin: 10px 5px 10px 0 !important' + + '}' + + '.cke_widget_image img {' + + ' max-width: 100%;' + + ' text-align: center' + + '}' + ); + }, + lang: ['en', 'fr'], + icons: 'insertImage', + + init: function (editor) { + editor.ui.addButton('insertImage', { + label: editor.lang.insertImage.buttonTooltip, + command: 'insertImage', + toolbar: 'insert' + }); + + // add insert image command + editor.addCommand('insertImage', { + exec: function () { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/*'; + input.click(); + + input.onchange = function () { + const file = input.files[0]; + if (file) { + handleFileUpload(file, false); + } + }; + } + }); + + const uploadUrl = editor.config.uploadUrl; + let uploadId = generateRandomId(); + editor.config.uploadUrl = uploadUrl + uploadId; + + // handel files comes from dataTransfer + const fileTools = CKEDITOR.fileTools; + + function handleFileUpload(file, moveSelectionPosition) { + if (editor.getData().trim() === '') { + editor.setData(' '); + } + const loader = editor.uploadRepository.create(file); + const reader = new FileReader(); + + reader.onload = function (e) { + const dataUrl = e.target.result; + + // Create a temporary document to safely insert the image + const tempDoc = document.implementation.createHTMLDocument(''); + const temp = new CKEDITOR.dom.element(tempDoc.body); + temp.data('cke-editable', 1); + + temp.appendHtml(``); + + const img = temp.find('img').getItem(0); + loader.data = dataUrl; + loader.upload(editor.config.uploadUrl + uploadId); // Ensure unique upload URL + + // Insert the image and trigger autogrow + editor.insertHtml(img.getOuterHtml()); + editor.fire('change'); + + if (moveSelectionPosition) { + const range = editor.getSelection().getRanges()[0]; + range.moveToPosition(range.endContainer, CKEDITOR.POSITION_AFTER_END); + editor.getSelection().selectRanges([range]); + } + editor.execCommand('autogrow'); + + // Bind notifications for the upload process + fileTools.bindNotifications(editor, loader); + + loader.on('uploaded', function () { + // Clean up the uploaded image once done + cleanWidget(dataUrl); + }); + }; + + reader.readAsDataURL(file); + } + + // handel temp upload + editor.on('fileUploadRequest', function (evt) { + evt.stop(); + const fileLoader = evt.data.fileLoader; + const formData = new FormData(); + const xhr = fileLoader.xhr; + + fileLoader.uploadId = uploadId; + fileLoader.thumbnailURL = evt.data.fileLoader.data; + fileLoader.uploadUrl = editor.config.uploadUrl; + + xhr.open('POST', fileLoader.uploadUrl, true); + formData.append('upload', fileLoader.file, fileLoader.fileName); + fileLoader.xhr.send(formData); + + uploadId = generateRandomId(); + editor.config.uploadUrl = uploadUrl + uploadId; + },); + editor.on('fileUploadResponse', function (evt) { + evt.stop(); + const data = evt.data; + const xhr = data.fileLoader.xhr; + const status = xhr.status; + + if (status === 200) { + data.url = data.fileLoader.thumbnailURL; + } else { + data.message = editor.lang.imageError; + evt.cancel(); + return abortUpload(data.fileLoader.uploadId); + } + }); + + editor.on('paste', function (evt) { + // For performance reason do not parse data if it does not contain img. + const files = Array.from(evt.data.dataTransfer._.files); + if (files.length === 0) { + return; + } + files.forEach((file) => { + handleFileUpload(file, true); + }); + evt.stop(); + }); + + function cleanWidget(dataUrl) { + const insertedImage = editor.document.findOne(`img[src="${dataUrl}"]`); + if (insertedImage) { + insertedImage.removeClass('cke_upload_uploading'); + insertedImage.removeAttribute('data-cke-saved-src'); + insertedImage.removeAttribute('data-cke-widget-data'); + } + } + } + }); +})(); + +function generateRandomId() { + const MAX_RANDOM_NUMBER = 100000; + const random = Math.round(Math.random() * MAX_RANDOM_NUMBER); + const now = Date.now(); + return `${random}-${now}`; +} + +function abortUpload(uploadId) { + return fetch(`${eXo.env.portal.context}/upload?uploadId=${uploadId}&action=abort`, { + method: 'POST', + credentials: 'include' + }); +} \ No newline at end of file diff --git a/notes-webapp/src/main/webapp/vue-app/notes-editor/components/NotesEditorDashboard.vue b/notes-webapp/src/main/webapp/vue-app/notes-editor/components/NotesEditorDashboard.vue index 12a437822..d009aaaea 100644 --- a/notes-webapp/src/main/webapp/vue-app/notes-editor/components/NotesEditorDashboard.vue +++ b/notes-webapp/src/main/webapp/vue-app/notes-editor/components/NotesEditorDashboard.vue @@ -64,6 +64,7 @@