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