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