001/*
002 *  Copyright 2022 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.workspaces;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.time.ZonedDateTime;
021import java.util.Collections;
022import java.util.List;
023import java.util.Map;
024import java.util.stream.Collectors;
025
026import org.apache.avalon.framework.component.Component;
027import org.apache.avalon.framework.context.Context;
028import org.apache.avalon.framework.context.ContextException;
029import org.apache.avalon.framework.context.Contextualizable;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.cocoon.components.ContextHelper;
034import org.apache.cocoon.environment.Request;
035import org.apache.cocoon.servlet.multipart.Part;
036
037import org.ametys.cms.data.Binary;
038import org.ametys.cms.data.RichText;
039import org.ametys.cms.repository.AttachableAmetysObject;
040import org.ametys.cms.repository.CommentableAmetysObject;
041import org.ametys.cms.repository.ReactionableObject.ReactionType;
042import org.ametys.cms.repository.comment.AbstractComment;
043import org.ametys.cms.repository.comment.RichTextComment;
044import org.ametys.cms.transformation.RichTextTransformer;
045import org.ametys.cms.transformation.docbook.DocbookTransformer;
046import org.ametys.core.observation.ObservationManager;
047import org.ametys.core.right.RightManager;
048import org.ametys.core.right.RightManager.RightResult;
049import org.ametys.core.user.CurrentUserProvider;
050import org.ametys.core.user.User;
051import org.ametys.core.user.UserIdentity;
052import org.ametys.core.user.UserManager;
053import org.ametys.plugins.repository.AmetysObject;
054import org.ametys.plugins.repository.AmetysObjectResolver;
055import org.ametys.plugins.repository.AmetysRepositoryException;
056import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
057import org.ametys.plugins.repository.tag.TaggableAmetysObject;
058import org.ametys.plugins.workflow.support.WorkflowHelper;
059import org.ametys.plugins.workflow.support.WorkflowProvider;
060import org.ametys.plugins.workspaces.project.ProjectManager;
061import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint;
062import org.ametys.plugins.workspaces.project.objects.Project;
063import org.ametys.plugins.workspaces.tags.ProjectTagsDAO;
064import org.ametys.runtime.plugin.component.AbstractLogEnabled;
065
066/**
067 * Abstract class for workspace modules DAO's
068 *
069 */
070public abstract class AbstractWorkspaceDAO extends AbstractLogEnabled implements Serviceable, Component, Contextualizable
071{
072
073    /** Ametys resolver */
074    protected AmetysObjectResolver _resolver;
075
076    /** Observer manager. */
077    protected ObservationManager _observationManager;
078
079    /** The current user provider. */
080    protected CurrentUserProvider _currentUserProvider;
081
082    /** The rights manager */
083    protected RightManager _rightManager;
084
085    /** User manager */
086    protected UserManager _userManager;
087
088    /** The workflow provider */
089    protected WorkflowProvider _workflowProvider;
090
091    /** The workflow helper */
092    protected WorkflowHelper _workflowHelper;
093
094    /** Workspaces project manager */
095    protected ProjectManager _projectManager;
096
097    /** The avalon context */
098    protected Context _context;
099
100    /** The workspace module EP */
101    protected WorkspaceModuleExtensionPoint _workspaceModuleEP;
102
103    /** The project tags DAO */
104    protected ProjectTagsDAO _projectTagsDAO;
105
106    /** The Workspaces helper */
107    protected WorkspacesHelper _workspaceHelper;
108    /** Rich text transformer */
109    protected RichTextTransformer _richTextTransformer;
110
111    public void contextualize(Context context) throws ContextException
112    {
113        _context = context;
114    }
115
116    public void service(ServiceManager manager) throws ServiceException
117    {
118        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
119        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
120        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
121        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
122        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
123        _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE);
124        _workflowHelper = (WorkflowHelper) manager.lookup(WorkflowHelper.ROLE);
125        _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE);
126        _workspaceModuleEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE);
127        _projectTagsDAO = (ProjectTagsDAO) manager.lookup(ProjectTagsDAO.ROLE);
128        _workspaceHelper = (WorkspacesHelper) manager.lookup(WorkspacesHelper.ROLE);
129        _richTextTransformer = (RichTextTransformer) manager.lookup(DocbookTransformer.ROLE);
130    }
131
132    /**
133     * Get the project name
134     * @return the project name
135     */
136    protected String _getProjectName()
137    {
138        Request request = ContextHelper.getRequest(_context);
139        return (String) request.getAttribute("projectName");
140    }
141
142    /**
143     * Get the project
144     * @return the project
145     */
146    protected Project _getProject()
147    {
148        return _projectManager.getProject(_getProjectName());
149    }
150
151    /**
152     * Get the sitemap language
153     * @return the sitemap language
154     */
155    protected String _getSitemapLanguage()
156    {
157        Request request = ContextHelper.getRequest(_context);
158        return (String) request.getAttribute("sitemapLanguage");
159    }
160
161    /**
162     * Get the site name
163     * @return the site name
164     */
165    protected String _getSiteName()
166    {
167        Request request = ContextHelper.getRequest(_context);
168        return (String) request.getAttribute("siteName");
169    }
170
171    /**
172     * Check user rights
173     * @param objectToCheck the object to check
174     * @param rightId the right id
175     * @throws IllegalAccessException if a right error occurred
176     */
177    protected void _checkUserRights(AmetysObject objectToCheck, String rightId) throws IllegalAccessException
178    {
179        if (_rightManager.hasRight(_currentUserProvider.getUser(), rightId, objectToCheck) != RightResult.RIGHT_ALLOW)
180        {
181            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to do operation without convenient right [" + rightId + "]");
182        }
183    }
184
185    /**
186     * Check user reading rights
187     * @param objectToCheck the object to check
188     * @throws IllegalAccessException if a right error occurred
189     */
190    protected void _checkUserReadingRights(AmetysObject objectToCheck) throws IllegalAccessException
191    {
192        if (!_rightManager.currentUserHasReadAccess(objectToCheck))
193        {
194            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to do read operation without convenient right");
195        }
196    }
197
198    /**
199     * Handle tags for the edition of a TaggableAmetysObject
200     * @param taggableAmetysObject the object to edit
201     * @param tags the tags
202     * @return the created tags
203     */
204    protected List<Map<String, Object>> _handleTags(TaggableAmetysObject taggableAmetysObject, List<Object> tags)
205    {
206        return _workspaceHelper.handleTags(taggableAmetysObject, tags);
207    }
208
209    /**
210     * Edit the attachments of an AttachableAmetysObject
211     * @param attachableAmetysObject the ametys object to edit
212     * @param newFiles list of new files
213     * @param newFileNames list of new files names
214     * @param deleteFiles list of names of old files to delete
215     */
216    protected void _setAttachments(AttachableAmetysObject attachableAmetysObject, List<Part> newFiles, List<String> newFileNames, List<String> deleteFiles)
217    {
218        if (!newFiles.isEmpty() || !deleteFiles.isEmpty())
219        {
220            List<Binary> attachments = attachableAmetysObject.getAttachments()
221                .stream()
222                .filter(b -> !deleteFiles.contains(b.getName()))
223                .collect(Collectors.toList());
224
225            List<String> fileNames = attachments.stream()
226                    .map(Binary::getFilename)
227                    .collect(Collectors.toList());
228
229            int i = 0;
230            for (Part newPart : newFiles)
231            {
232                String newName = newFileNames.get(i);
233                fileNames.add(newName);
234                Binary newBinary = _partToBinary(newPart, newName);
235                if (newBinary != null)
236                {
237                    attachments.add(newBinary);
238                }
239                i++;
240            }
241            attachableAmetysObject.setAttachments(attachments);
242        }
243    }
244
245    private Binary _partToBinary(Part part, String name)
246    {
247        if (part.isRejected())
248        {
249            getLogger().error("Part {} will not be uploaded because it's rejected", part.getFileName());
250            return null;
251        }
252
253        try (InputStream is = part.getInputStream())
254        {
255            Binary binary = new Binary();
256
257            binary.setFilename(name);
258            binary.setInputStream(is);
259            binary.setLastModificationDate(ZonedDateTime.now());
260            binary.setMimeType(part.getMimeType());
261
262            return binary;
263        }
264        catch (Exception e)
265        {
266            getLogger().error("An error occurred getting binary from part {}", part.getFileName(), e);
267        }
268
269        return null;
270    }
271
272    /**
273     * Comment a commentableAmetysObject
274     * @param <T> type of the value to retrieve
275     * @param commentableAmetysObject the commentableAmetysObject
276     * @param commentText the comment text
277     * @param moduleRoot the module root
278     * @return The commentableAmetysObject
279     */
280    public <T extends AbstractComment> T createComment(CommentableAmetysObject<T>  commentableAmetysObject, String commentText, ModifiableTraversableAmetysObject moduleRoot)
281    {
282        UserIdentity userIdentity = _currentUserProvider.getUser();
283
284        T comment = commentableAmetysObject.createComment();
285
286        _setComment(comment, userIdentity, commentText);
287
288        moduleRoot.saveChanges();
289
290        return comment;
291    }
292
293    /**
294     * Edit a commentableAmetysObject comment
295     * @param commentableAmetysObject the commentableAmetysObject
296     * @param commentId the comment Id
297     * @param commentText the comment text
298     * @param moduleRoot the module root
299     * @return The commentableAmetysObject
300     * @throws IllegalAccessException If an error occurs when checking the rights
301     */
302    public CommentableAmetysObject editComment(CommentableAmetysObject commentableAmetysObject, String commentId, String commentText, ModifiableTraversableAmetysObject moduleRoot) throws IllegalAccessException
303    {
304        UserIdentity userIdentity = _currentUserProvider.getUser();
305        User user = _userManager.getUser(userIdentity);
306        AbstractComment comment = commentableAmetysObject.getComment(commentId);
307        String authorEmail = comment.getAuthorEmail();
308        if (!authorEmail.equals(user.getEmail()))
309        {
310            throw new IllegalAccessException("User '" + userIdentity + "' tried to edit an other user's comment");
311        }
312
313        if (comment.getContent().equals(commentText))
314        {
315            return commentableAmetysObject;
316        }
317
318        comment.setContent(commentText);
319        if (comment instanceof RichTextComment richTextComment)
320        {
321            _setRichTextContent(commentText, richTextComment);
322        }
323        comment.setEdited(true);
324
325        moduleRoot.saveChanges();
326        return commentableAmetysObject;
327    }
328
329    private void _setRichTextContent(String commentText, RichTextComment richTextComment)
330    {
331        RichText richText = richTextComment.getRichTextContent();
332        if (richText == null)
333        {
334            richText = new RichText();
335        }
336
337        try
338        {
339            _richTextTransformer.transform(commentText, richText);
340        }
341        catch (AmetysRepositoryException | IOException e)
342        {
343            throw new AmetysRepositoryException("Unable to transform the text " + commentText + " into a rich text for comment " + richTextComment.getId(), e);
344        }
345
346        richTextComment.setRichTextContent(richText);
347    }
348
349    /**
350     * Edit a commentableAmetysObject comment
351     * @param commentableAmetysObject the commentableAmetysObject
352     * @param commentId the comment Id
353     * @param moduleRoot the module root
354     * @return The commentableAmetysObject
355     */
356    public CommentableAmetysObject deleteComment(CommentableAmetysObject commentableAmetysObject, String commentId, ModifiableTraversableAmetysObject moduleRoot)
357    {
358        AbstractComment comment = commentableAmetysObject.getComment(commentId);
359
360        if (comment.isSubComment())
361        {
362            AbstractComment parentComment = comment.getCommentParent();
363            List<AbstractComment> subComments = parentComment.getSubComment(true, true);
364            boolean hasAfterSubComments = _hasAfterSubComments(comment, subComments);
365            if (comment.hasSubComments() || hasAfterSubComments)
366            {
367                comment.setDeleted(true);
368                comment.setAccepted(false);
369            }
370            else
371            {
372                comment.remove();
373            }
374
375            // Sort comment by creation date (Recent creation date in first)
376            List<AbstractComment> currentSubComments = parentComment.getSubComment(true, true);
377            Collections.sort(currentSubComments, (c1, c2) ->
378            {
379                return c2.getCreationDate().compareTo(c1.getCreationDate());
380            });
381
382            // Remove already deleted sub comment if no recent sub comment is present
383            for (AbstractComment subCom : currentSubComments)
384            {
385                if (subCom.isDeleted() && !subCom.hasSubComments())
386                {
387                    subCom.remove();
388                }
389                else
390                {
391                    break;
392                }
393            }
394
395            // Remove parent comment
396            if (parentComment.isDeleted())
397            {
398                deleteComment(commentableAmetysObject, parentComment.getId(), moduleRoot);
399            }
400        }
401        else
402        {
403            List<AbstractComment> subComment = comment.getSubComment(true, true);
404            boolean hasSubComments = subComment.stream()
405                .filter(c -> !c.isDeleted()) // Ignore alreay deleted sub comment
406                .findAny()
407                .isPresent();
408            if (hasSubComments)
409            {
410                comment.setDeleted(true);
411                comment.setAccepted(false);
412            }
413            else
414            {
415                comment.remove();
416            }
417        }
418
419        moduleRoot.saveChanges();
420
421        return commentableAmetysObject;
422    }
423
424    /**
425     * Check if a given comment should be deleted or removed
426     * @param comment the comment
427     * @param subComments the subcomments
428     * @return true if the comment should be deleted, false if the comment should be removed instead
429     */
430    protected boolean _hasAfterSubComments(AbstractComment comment, List<AbstractComment> subComments)
431    {
432        boolean hasAfterSubComments = subComments.stream()
433            .filter(c -> !c.getId().equals(comment.getId())) //Don't take current sub comment
434            .filter(c -> !c.isDeleted()) // Ignore alreay deleted sub comment
435            .filter(c -> c.getCreationDate().isAfter(comment.getCreationDate())) // Just take sub comment after current comment
436            .findAny()
437            .isPresent();
438        return hasAfterSubComments;
439    }
440
441    /**
442     * Answer to a commentableAmetysObject comment
443     * @param <T> type of the value to retrieve
444     * @param commentableAmetysObject the commentableAmetysObject
445     * @param commentId the parent comment Id
446     * @param commentText the comment text
447     * @param moduleRoot the module root
448     * @return The commentableAmetysObject
449     */
450    public <T extends AbstractComment> T answerComment(CommentableAmetysObject<T> commentableAmetysObject, String commentId, String commentText, ModifiableTraversableAmetysObject moduleRoot)
451    {
452        AbstractComment comment = commentableAmetysObject.getComment(commentId);
453
454        UserIdentity userIdentity = _currentUserProvider.getUser();
455        T subComment = comment.createSubComment();
456
457        _setComment(subComment, userIdentity, commentText);
458
459        moduleRoot.saveChanges();
460        return subComment;
461    }
462
463    private void _setComment(AbstractComment comment, UserIdentity userIdentity, String commentText)
464    {
465        User user = _userManager.getUser(userIdentity);
466        comment.setAuthorName(user.getFullName());
467        comment.setAuthorEmail(user.getEmail());
468        comment.setAuthor(userIdentity);
469        comment.setEmailHiddenStatus(true);
470        comment.setContent(commentText);
471        if (comment instanceof RichTextComment richTextComment)
472        {
473            _setRichTextContent(commentText, richTextComment);
474        }
475        comment.setValidated(true);
476    }
477
478    /**
479     * Answer to a commentableAmetysObject comment
480     * @param commentableAmetysObject the commentableAmetysObject
481     * @param commentId the parent comment Id
482     * @param liked true if the comment is liked, otherwise the comment is unliked
483     * @param moduleRoot the module root
484     * @return The commentableAmetysObject
485     */
486    public CommentableAmetysObject likeOrUnlikeComment(CommentableAmetysObject commentableAmetysObject, String commentId, Boolean liked, ModifiableTraversableAmetysObject moduleRoot)
487    {
488        AbstractComment comment = commentableAmetysObject.getComment(commentId);
489
490        UserIdentity user = _currentUserProvider.getUser();
491        if (Boolean.FALSE.equals(liked)
492            || liked == null && comment.getReactionUsers(ReactionType.LIKE).contains(user))
493        {
494            comment.removeReaction(user, ReactionType.LIKE);
495        }
496        else
497        {
498            comment.addReaction(user, ReactionType.LIKE);
499        }
500
501        moduleRoot.saveChanges();
502        return commentableAmetysObject;
503    }
504}