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     * @param moduleRoot the module root, used to check tag creation rights if needed
211     * @return the created tags
212     */
213    protected List<Map<String, Object>> _handleTags(TaggableAmetysObject taggableAmetysObject, List<Object> tags, ModifiableTraversableAmetysObject moduleRoot)
214    {
215        return _workspaceHelper.handleTags(taggableAmetysObject, tags, moduleRoot);
216    }
217
218    /**
219     * Edit the attachments of an AttachableAmetysObject
220     * @param attachableAmetysObject the ametys object to edit
221     * @param newFiles list of new files
222     * @param newFileNames list of new files names
223     * @param deleteFiles list of names of old files to delete
224     */
225    protected void _setAttachments(AttachableAmetysObject attachableAmetysObject, List<Part> newFiles, List<String> newFileNames, List<String> deleteFiles)
226    {
227        if (!newFiles.isEmpty() || !deleteFiles.isEmpty())
228        {
229            List<Binary> attachments = attachableAmetysObject.getAttachments()
230                .stream()
231                .filter(b -> !deleteFiles.contains(b.getName()))
232                .collect(Collectors.toList());
233
234            List<String> fileNames = attachments.stream()
235                    .map(Binary::getFilename)
236                    .collect(Collectors.toList());
237
238            int i = 0;
239            for (Part newPart : newFiles)
240            {
241                String newName = newFileNames.get(i);
242                fileNames.add(newName);
243                Binary newBinary = _partToBinary(newPart, newName);
244                if (newBinary != null)
245                {
246                    attachments.add(newBinary);
247                }
248                i++;
249            }
250            attachableAmetysObject.setAttachments(attachments);
251        }
252    }
253
254    private Binary _partToBinary(Part part, String name)
255    {
256        if (part.isRejected())
257        {
258            getLogger().error("Part {} will not be uploaded because it's rejected", part.getFileName());
259            return null;
260        }
261
262        try (InputStream is = part.getInputStream())
263        {
264            Binary binary = new Binary();
265
266            binary.setFilename(name);
267            binary.setInputStream(is);
268            binary.setLastModificationDate(ZonedDateTime.now());
269            binary.setMimeType(part.getMimeType());
270
271            return binary;
272        }
273        catch (Exception e)
274        {
275            getLogger().error("An error occurred getting binary from part {}", part.getFileName(), e);
276        }
277
278        return null;
279    }
280
281    /**
282     * Comment a commentableAmetysObject
283     * @param <T> type of the value to retrieve
284     * @param commentableAmetysObject the commentableAmetysObject
285     * @param commentText the comment text
286     * @param moduleRoot the module root
287     * @return The commentableAmetysObject
288     */
289    public <T extends AbstractComment> T createComment(CommentableAmetysObject<T>  commentableAmetysObject, String commentText, ModifiableTraversableAmetysObject moduleRoot)
290    {
291        UserIdentity userIdentity = _currentUserProvider.getUser();
292
293        T comment = commentableAmetysObject.createComment();
294
295        _setComment(comment, userIdentity, commentText);
296
297        moduleRoot.saveChanges();
298
299        return comment;
300    }
301
302    /**
303     * Edit a commentableAmetysObject comment
304     * @param commentableAmetysObject the commentableAmetysObject
305     * @param commentId the comment Id
306     * @param commentText the comment text
307     * @param moduleRoot the module root
308     * @return The commentableAmetysObject
309     */
310    public CommentableAmetysObject editComment(CommentableAmetysObject commentableAmetysObject, String commentId, String commentText, ModifiableTraversableAmetysObject moduleRoot)
311    {
312        UserIdentity userIdentity = _currentUserProvider.getUser();
313        User user = _userManager.getUser(userIdentity);
314        AbstractComment comment = commentableAmetysObject.getComment(commentId);
315        String authorEmail = comment.getAuthorEmail();
316        if (!authorEmail.equals(user.getEmail()))
317        {
318            throw new AccessDeniedException("User '" + userIdentity + "' tried to edit an other user's comment");
319        }
320
321        if (comment.getContent().equals(commentText))
322        {
323            return commentableAmetysObject;
324        }
325
326        comment.setContent(commentText);
327        if (comment instanceof RichTextComment richTextComment)
328        {
329            _setRichTextContent(commentText, richTextComment);
330        }
331        comment.setEdited(true);
332
333        moduleRoot.saveChanges();
334        return commentableAmetysObject;
335    }
336
337    private void _setRichTextContent(String commentText, RichTextComment richTextComment)
338    {
339        RichText richText = richTextComment.getRichTextContent();
340        if (richText == null)
341        {
342            richText = new RichText();
343        }
344
345        try
346        {
347            _richTextTransformer.transform(commentText, richText);
348        }
349        catch (AmetysRepositoryException | IOException e)
350        {
351            throw new AmetysRepositoryException("Unable to transform the text " + commentText + " into a rich text for comment " + richTextComment.getId(), e);
352        }
353
354        richTextComment.setRichTextContent(richText);
355    }
356
357    /**
358     * Edit a commentableAmetysObject comment
359     * @param commentableAmetysObject the commentableAmetysObject
360     * @param commentId the comment Id
361     * @param moduleRoot the module root
362     * @return The commentableAmetysObject
363     */
364    public CommentableAmetysObject deleteComment(CommentableAmetysObject commentableAmetysObject, String commentId, ModifiableTraversableAmetysObject moduleRoot)
365    {
366        AbstractComment comment = commentableAmetysObject.getComment(commentId);
367
368        if (comment.isSubComment())
369        {
370            AbstractComment parentComment = comment.getCommentParent();
371            List<AbstractComment> subComments = parentComment.getSubComment(true, true);
372            boolean hasAfterSubComments = _hasAfterSubComments(comment, subComments);
373            if (comment.hasSubComments() || hasAfterSubComments)
374            {
375                comment.setDeleted(true);
376                comment.setAccepted(false);
377            }
378            else
379            {
380                comment.remove();
381            }
382
383            // Sort comment by creation date (Recent creation date in first)
384            List<AbstractComment> currentSubComments = parentComment.getSubComment(true, true);
385            Collections.sort(currentSubComments, (c1, c2) ->
386            {
387                return c2.getCreationDate().compareTo(c1.getCreationDate());
388            });
389
390            // Remove already deleted sub comment if no recent sub comment is present
391            for (AbstractComment subCom : currentSubComments)
392            {
393                if (subCom.isDeleted() && !subCom.hasSubComments())
394                {
395                    subCom.remove();
396                }
397                else
398                {
399                    break;
400                }
401            }
402
403            // Remove parent comment
404            if (parentComment.isDeleted())
405            {
406                deleteComment(commentableAmetysObject, parentComment.getId(), moduleRoot);
407            }
408        }
409        else
410        {
411            List<AbstractComment> subComment = comment.getSubComment(true, true);
412            boolean hasSubComments = subComment.stream()
413                .filter(c -> !c.isDeleted()) // Ignore alreay deleted sub comment
414                .findAny()
415                .isPresent();
416            if (hasSubComments)
417            {
418                comment.setDeleted(true);
419                comment.setAccepted(false);
420            }
421            else
422            {
423                comment.remove();
424            }
425        }
426
427        moduleRoot.saveChanges();
428
429        return commentableAmetysObject;
430    }
431
432    /**
433     * Check if a given comment should be deleted or removed
434     * @param comment the comment
435     * @param subComments the subcomments
436     * @return true if the comment should be deleted, false if the comment should be removed instead
437     */
438    protected boolean _hasAfterSubComments(AbstractComment comment, List<AbstractComment> subComments)
439    {
440        boolean hasAfterSubComments = subComments.stream()
441            .filter(c -> !c.getId().equals(comment.getId())) //Don't take current sub comment
442            .filter(c -> !c.isDeleted()) // Ignore alreay deleted sub comment
443            .filter(c -> c.getCreationDate().isAfter(comment.getCreationDate())) // Just take sub comment after current comment
444            .findAny()
445            .isPresent();
446        return hasAfterSubComments;
447    }
448
449    /**
450     * Answer to a commentableAmetysObject comment
451     * @param <T> type of the value to retrieve
452     * @param commentableAmetysObject the commentableAmetysObject
453     * @param commentId the parent comment Id
454     * @param commentText the comment text
455     * @param moduleRoot the module root
456     * @return The commentableAmetysObject
457     */
458    public <T extends AbstractComment> T answerComment(CommentableAmetysObject<T> commentableAmetysObject, String commentId, String commentText, ModifiableTraversableAmetysObject moduleRoot)
459    {
460        AbstractComment comment = commentableAmetysObject.getComment(commentId);
461
462        UserIdentity userIdentity = _currentUserProvider.getUser();
463        T subComment = comment.createSubComment();
464
465        _setComment(subComment, userIdentity, commentText);
466
467        moduleRoot.saveChanges();
468        return subComment;
469    }
470
471    private void _setComment(AbstractComment comment, UserIdentity userIdentity, String commentText)
472    {
473        User user = _userManager.getUser(userIdentity);
474        comment.setAuthorName(user.getFullName());
475        comment.setAuthorEmail(user.getEmail());
476        comment.setAuthor(userIdentity);
477        comment.setEmailHiddenStatus(true);
478        comment.setContent(commentText);
479        if (comment instanceof RichTextComment richTextComment)
480        {
481            _setRichTextContent(commentText, richTextComment);
482        }
483        comment.setValidated(true);
484    }
485
486    /**
487     * Answer to a commentableAmetysObject comment
488     * @param commentableAmetysObject the commentableAmetysObject
489     * @param commentId the parent comment Id
490     * @param liked true if the comment is liked, otherwise the comment is unliked
491     * @param moduleRoot the module root
492     * @return The commentableAmetysObject
493     */
494    public CommentableAmetysObject likeOrUnlikeComment(CommentableAmetysObject commentableAmetysObject, String commentId, Boolean liked, ModifiableTraversableAmetysObject moduleRoot)
495    {
496        AbstractComment comment = commentableAmetysObject.getComment(commentId);
497
498        UserIdentity user = _currentUserProvider.getUser();
499        if (Boolean.FALSE.equals(liked)
500            || liked == null && comment.getReactionUsers(ReactionType.LIKE).contains(user))
501        {
502            comment.removeReaction(user, ReactionType.LIKE);
503        }
504        else
505        {
506            comment.addReaction(user, ReactionType.LIKE);
507        }
508
509        moduleRoot.saveChanges();
510        return commentableAmetysObject;
511    }
512}