001/*
002 *  Copyright 2014 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 */
016
017package org.ametys.cms.repository.comment.ui;
018
019import java.io.IOException;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.cocoon.ProcessingException;
028import org.apache.commons.lang.StringUtils;
029
030import org.ametys.cms.ObservationConstants;
031import org.ametys.cms.content.ContentHelper;
032import org.ametys.cms.repository.Content;
033import org.ametys.cms.repository.comment.Comment;
034import org.ametys.cms.repository.comment.CommentableContent;
035import org.ametys.core.observation.Event;
036import org.ametys.core.observation.ObservationManager;
037import org.ametys.core.right.RightManager.RightResult;
038import org.ametys.core.ui.Callable;
039import org.ametys.core.ui.StaticClientSideElement;
040import org.ametys.core.user.CurrentUserProvider;
041import org.ametys.core.user.UserIdentity;
042import org.ametys.core.util.DateUtils;
043import org.ametys.plugins.repository.AmetysObjectResolver;
044import org.ametys.plugins.repository.UnknownAmetysObjectException;
045
046/**
047 * This client site elements creates a button representing the validation state of a content's comment
048 */
049public class CommentClientSideElement extends StaticClientSideElement
050{
051    /** The constant for the name of the author from parameters */
052    public static final String PARAMETER_AUTHOR_NAME = "author-name";
053    /** The constant for the email of the author from parameters */
054    public static final String PARAMETER_AUTHOR_EMAIL = "author-email";
055    /** The constant for the hidden status of the email of the author from parameters */
056    public static final String PARAMETER_AUTHOR_EMAILHIDDEN = "author-emailhidden";
057    /** The constant for the url of the author from parameters */
058    public static final String PARAMETER_AUTHOR_URL = "author-url";
059    /** The constant for the text from parameters */
060    public static final String PARAMETER_TEXT = "text";
061    /** The constant for the content id from parameters */
062    public static final String PARAMETER_CONTENT_ID = "contentId";
063    /** The constant for the comment id from parameters */
064    public static final String PARAMETER_COMMENT_ID = "commentId";
065    /** The Ametys object resolver */
066    protected AmetysObjectResolver _resolver;
067    /** The current user provider */
068    protected CurrentUserProvider _userProvider;
069    /** The observation manager */
070    protected ObservationManager _observationManager;
071    /** The content helper */
072    private ContentHelper _contentHelper;
073
074    @Override
075    public void service(ServiceManager smanager) throws ServiceException
076    {
077        super.service(smanager);
078        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
079        _userProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
080        _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE);
081        _contentHelper = (ContentHelper) smanager.lookup(ContentHelper.ROLE);
082    }
083    
084    /**
085     * Return the list of comments with its validation state
086     * @param parameters The contents and their comments
087     * @return The comments and their validated status
088     */
089    @Callable
090    public Map<String, Object> getComments(Map<String, Object> parameters)
091    {
092        Map<String, Object> results = new HashMap<>();
093        results.put("comments", new ArrayList<Map<String, Object>>());
094        
095        for (String contentId : parameters.keySet())
096        {
097            try
098            {
099                Content content = _resolver.resolveById(contentId);
100                if (content instanceof CommentableContent)
101                {
102                    CommentableContent cContent = (CommentableContent) content;
103                    
104                    @SuppressWarnings("unchecked")
105                    List<String> commentIds = (List<String>) parameters.get(contentId);
106                    
107                    for (String commentId : commentIds)
108                    {
109                        Comment comment = cContent.getComment(commentId);
110                        @SuppressWarnings("unchecked")
111                        List<Map<String, Object>> comments = (List<Map<String, Object>>) results.get("comments");
112                        comments.add(getCommentParameters(cContent, comment));
113                    }   
114                }
115                else
116                {
117                    getLogger().warn("Ignoring request to get comments status on non commentable content " + contentId);
118                }            
119            }
120            catch (UnknownAmetysObjectException e)
121            {
122                getLogger().warn("Ignoring request to get comments status on inexisting content " + contentId, e);
123            }
124        }
125        
126        return results;
127    }
128    
129    /**
130     * Get a comment properties
131     * @param contentId the id of the content
132     * @param commentId the id of the comment. Can be null
133     * @return results the server's response in JSON.
134     * @throws IOException If an error occurred
135     * @throws ProcessingException If an error occurred
136     */
137    @Callable
138    public Map<String, Object> getComment(String contentId, String commentId) throws IOException, ProcessingException
139    {
140        Map<String, Object> results = new HashMap<> ();
141               
142        UserIdentity user = _userProvider.getUser();
143        
144        Map<String, Object> comments = new HashMap<> ();
145        
146        Content content = _resolver.resolveById(contentId);
147        if (StringUtils.isNotEmpty(commentId))
148        {
149            if (content instanceof CommentableContent)
150            {
151                if (_rightManager.hasRight(user, "CMS_Rights_CommentModerate", content) == RightResult.RIGHT_ALLOW)
152                {
153                    CommentableContent commentableContent = (CommentableContent) content;
154                    Comment comment = commentableContent.getComment(commentId);
155                    comments = _jsonifyComment(comment, content);
156                }
157            }
158        }
159        
160        results.put("comments", comments);
161        return results;
162    }
163
164    /**
165     * Edit a comment if connected user has sufficient rights
166     * @param parameters the JS parameters. Necessarily contains the content and comment id, and the values to edit
167     * @return An empty map
168     */
169    @Callable 
170    public Map<String, Object> editComment(Map<String, Object> parameters)
171    {
172        String contentId = (String) parameters.get(PARAMETER_CONTENT_ID);
173        String commentId = (String) parameters.get(PARAMETER_COMMENT_ID);
174        
175        try
176        {
177            Content content = _resolver.resolveById(contentId);
178
179            if (!hasRight("CMS_Rights_CommentModerate", content))
180            {
181                String errorMessage = "User " + getCurrentUser() + " try to edit a comment on content of id '" + contentId + "' with no sufficient rights"; 
182                getLogger().error(errorMessage);
183                throw new IllegalStateException(errorMessage);
184            }
185            
186            if (!(content instanceof CommentableContent))
187            {
188                String errorMessage = "Can not edit comment for non-commentable content of id '" + contentId + "'"; 
189                getLogger().error(errorMessage);
190                throw new IllegalStateException(errorMessage);
191            }
192            
193            CommentableContent cContent = (CommentableContent) content;
194            
195            Comment comment = cContent.getComment(commentId);
196            
197            String oldAuthorName = comment.getAuthorName();
198            String oldAuthorEmail = comment.getAuthorEmail();
199            boolean oldAuthorEmailHidden = comment.isEmailHidden();
200            String oldAuthorURL = comment.getAuthorURL();
201            String oldContent = comment.getContent();
202            
203            boolean needSave = false;
204            
205            String authorName = (String) parameters.get(PARAMETER_AUTHOR_NAME);
206            if (!authorName.equals(oldAuthorName))
207            {
208                comment.setAuthorName(authorName);
209                needSave = true;
210            }
211            
212            String authorEmail = (String) parameters.get(PARAMETER_AUTHOR_EMAIL);
213            if (!authorEmail.equals(oldAuthorEmail))
214            {
215                comment.setAuthorEmail(authorEmail);
216                needSave = true;
217            }
218            
219            boolean authorEmailHidden = (Boolean) parameters.get(PARAMETER_AUTHOR_EMAILHIDDEN);
220            if (authorEmailHidden != oldAuthorEmailHidden)
221            {
222                comment.setEmailHiddenStatus(authorEmailHidden);
223                needSave = true;
224            }
225            
226            String authorUrl = (String) parameters.get(PARAMETER_AUTHOR_URL);
227            if (!authorUrl.equals(oldAuthorURL))
228            {
229                comment.setAuthorURL(authorUrl);
230                needSave = true;
231            }
232            
233            String text = (String) parameters.get(PARAMETER_TEXT);
234            if (!text.equals(oldContent))
235            {
236                comment.setContent(text);
237                needSave = true;
238            }
239            
240            if (needSave)
241            {
242                cContent.saveChanges();
243                
244                Map<String, Object> eventParams = new HashMap<>();
245                eventParams.put(ObservationConstants.ARGS_CONTENT, content);
246                eventParams.put(ObservationConstants.ARGS_COMMENT, comment);
247                eventParams.put("content.comment.old.author", oldAuthorName);
248                eventParams.put("content.comment.old.author.email", oldAuthorEmail);
249                eventParams.put("content.comment.old.author.email.hidden", oldAuthorEmailHidden);
250                eventParams.put("content.comment.old.author.url", oldAuthorURL);
251                eventParams.put("content.comment.old.content", oldContent);
252                
253                _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_COMMENT_MODIFYING, getCurrentUser(), eventParams));
254            }
255        
256            return getCommentParameters(content, comment);
257        }
258        catch (UnknownAmetysObjectException e)
259        {
260            getLogger().error("Unknown content of id '" + contentId + "'", e);
261            throw new IllegalArgumentException("Unknown content of id '" + contentId + "'", e);
262        }
263    }
264    
265    /**
266     * Removes comments
267     * @param contents the contents with comments to remove
268     * @return the JSON result with deleted comments or not
269     */
270    @SuppressWarnings("unchecked")
271    @Callable
272    public Map<String, Object> deleteComments(Map<String, List<String>> contents)
273    {
274        Map<String, Object> results = new HashMap<> ();
275        
276        results.put("deleted-comments", new ArrayList<Map<String, Object>>());
277        results.put("undeleted-comments", new ArrayList<Map<String, Object>>());
278        results.put("uncommentable-contents", new ArrayList<Map<String, Object>>());
279        results.put("noright-contents", new ArrayList<Map<String, Object>>());
280        results.put("unknown-contents", new ArrayList<Map<String, Object>>());
281        
282        for (String contentId : contents.keySet())
283        {
284            try
285            {
286                Content content = _resolver.resolveById(contentId);
287                
288                if (hasRight("CMS_Rights_CommentModerate", content))
289                {
290                    List<String> deleteCommentIds = new ArrayList<>();
291                    // For each associated comment
292                    for (String commentId : contents.get(contentId))
293                    {
294                        if (!_isParentCommentAlreadyDelete(deleteCommentIds, commentId))
295                        {
296                            Map<String, Object> commentParams = new HashMap<>();
297                            commentParams.put("id", commentId);
298                            commentParams.put("contentId", contentId);
299                            commentParams.put("contentTitle", _contentHelper.getTitle(content));
300                            
301                            if (content instanceof CommentableContent)
302                            {
303                                CommentableContent cContent = (CommentableContent) content;
304                                Comment comment = cContent.getComment(commentId);
305                                
306                                Map<String, Object> eventParams = new HashMap<>();
307                                eventParams.put(ObservationConstants.ARGS_CONTENT, content);
308                                eventParams.put(ObservationConstants.ARGS_COMMENT_ID, comment.getId());
309                                eventParams.put(ObservationConstants.ARGS_COMMENT_AUTHOR, comment.getAuthorName());
310                                eventParams.put(ObservationConstants.ARGS_COMMENT_AUTHOR_EMAIL, comment.getAuthorEmail());
311                                eventParams.put(ObservationConstants.ARGS_COMMENT_VALIDATED, comment.isValidated());
312                                eventParams.put("comment.content", comment.getContent());
313                                
314                                _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_COMMENT_DELETING, getCurrentUser(), eventParams));
315                                
316                                comment.remove();
317                                cContent.saveChanges();
318        
319                                List<Map<String, Object>> deletedComments = (List<Map<String, Object>>) results.get("deleted-comments");
320                                deletedComments.add(commentParams);
321                                deleteCommentIds.add(commentId);
322                                
323                                _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_COMMENT_DELETED, getCurrentUser(), eventParams));
324                            }
325                            else
326                            {
327                                getLogger().error("Can not remove a comment on a non commentable content");
328                                
329                                List<Map<String, Object>> uncommentableContents = (List<Map<String, Object>>) results.get("uncommentable-contents");
330                                uncommentableContents.add(commentParams);
331                            }
332                        }
333                    }
334                }
335                else
336                {
337                    // No right
338                    getLogger().error("User '" + getCurrentUser() + "' does not have right to moderate comments on content '" + content.getId() + "'");
339                    
340                    Map<String, Object> contentParams = new HashMap<>();
341                    contentParams.put("contentId", contentId);
342                    contentParams.put("contentTitle", _contentHelper.getTitle(content));
343                    
344                    List<Map<String, Object>> norightContents = (List<Map<String, Object>>) results.get("noright-contents");
345                    norightContents.add(contentParams);
346                }
347            }
348            catch (UnknownAmetysObjectException e)
349            {
350                getLogger().error("Can not remove a comment on a non existing content", e);
351                
352                Map<String, Object> contentParams = new HashMap<>();
353                contentParams.put("contentId", contentId);
354                
355                List<Map<String, Object>> unknownContents = (List<Map<String, Object>>) results.get("unknown-contents");
356                unknownContents.add(contentParams);
357            }
358        }
359
360        return results;
361    }
362    
363    /**
364     * True if the a parent comment of the comment id is already deleted
365     * @param deleteCommentIds the delete comment ids
366     * @param commentId the comment id
367     * @return true if the a parent comment of the comment id is already deleted
368     */
369    protected boolean _isParentCommentAlreadyDelete(List<String> deleteCommentIds, String commentId)
370    {
371        for (String deleteCommentId : deleteCommentIds)
372        {
373            if (commentId.startsWith(deleteCommentId))
374            {
375                return true;
376            }
377        }
378        
379        return false;
380    }
381    
382    /**
383     * Validates comments when it is possible.
384     * @param contents the contents with comments to validate
385     * @return the JSON result with validated comments or not
386     */
387    @SuppressWarnings("unchecked")
388    @Callable
389    public Map<String, Object> validateComments(Map<String, List<String>> contents)
390    {
391        Map<String, Object> results = new HashMap<>();
392
393        results.put("validated-comments", new ArrayList<Map<String, Object>>());
394        results.put("error-comments", new ArrayList<Map<String, Object>>());
395        results.put("uncommentable-contents", new ArrayList<Map<String, Object>>());
396        results.put("noright-contents", new ArrayList<Map<String, Object>>());
397        results.put("unknown-contents", new ArrayList<Map<String, Object>>());
398        
399        for (String contentId : contents.keySet())
400        {
401            try
402            {
403                Content content = _resolver.resolveById(contentId);
404                
405                if (hasRight("CMS_Rights_CommentModerate", content))
406                {
407                    // For each associated comment
408                    for (String commentId : contents.get(contentId))
409                    {
410                        Map<String, Object> commentParams = new HashMap<>();
411                        commentParams.put("id", commentId);
412                        commentParams.put("contentId", contentId);
413                        commentParams.put("contentTitle", _contentHelper.getTitle(content));
414                        
415                        if (content instanceof CommentableContent)
416                        {
417                            CommentableContent cContent = (CommentableContent) content;
418        
419                            Comment comment = cContent.getComment(commentId);
420                            commentParams.put("reportsCount", comment.getReportsCount());
421                            
422                            if (!comment.isValidated())
423                            {
424                                comment.setValidated(true);
425                                cContent.saveChanges();
426                                
427                                List<Map<String, Object>> validatedComments = (List<Map<String, Object>>) results.get("validated-comments");
428                                validatedComments.add(commentParams);
429                                
430                                Map<String, Object> eventParams = new HashMap<>();
431                                eventParams.put(ObservationConstants.ARGS_CONTENT, content);
432                                eventParams.put(ObservationConstants.ARGS_COMMENT, comment);
433                                _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_COMMENT_VALIDATED, getCurrentUser(), eventParams));
434                            }
435                        }
436                        else
437                        {
438                            getLogger().error("Can not validate a comment on a non commentable content");
439                            
440                            List<Map<String, Object>> errorComments = (List<Map<String, Object>>) results.get("error-comments");
441                            errorComments.add(commentParams);
442                        }
443                    }
444                }
445                else
446                {
447                    // No right
448                    getLogger().error("User '" + getCurrentUser() + "' does not have right to validate comments on content '" + content.getId() + "'");
449                    
450                    Map<String, Object> contentParams = new HashMap<>();
451                    contentParams.put("contentId", contentId);
452                    contentParams.put("contentTitle", _contentHelper.getTitle(content));
453                }
454            }
455            catch (UnknownAmetysObjectException e)
456            {
457                getLogger().error("Can not validate a comment on a non existing content", e);
458                
459                Map<String, Object> contentParams = new HashMap<>();
460                contentParams.put("contentId", contentId);
461                
462                List<Map<String, Object>> unknownContents = (List<Map<String, Object>>) results.get("unknown-contents");
463                unknownContents.add(contentParams);
464            }
465        }
466
467        return results;
468    }
469    
470    /**
471     * Invalidates comments when it is possible.
472     * @param contents the contents with comments to invalidate
473     * @return the JSON result with invalidated comments or not
474     */
475    @SuppressWarnings("unchecked")
476    @Callable
477    public Map<String, Object> invalidateComments(Map<String, List<String>> contents)
478    {
479        Map<String, Object> results = new HashMap<>();
480
481        results.put("unvalidated-comments", new ArrayList<Map<String, Object>>());
482        results.put("error-comments", new ArrayList<Map<String, Object>>());
483        results.put("uncommentable-contents", new ArrayList<Map<String, Object>>());
484        results.put("noright-contents", new ArrayList<Map<String, Object>>());
485        results.put("unknown-contents", new ArrayList<Map<String, Object>>());
486        
487        for (String contentId : contents.keySet())
488        {
489            try
490            {
491                Content content = _resolver.resolveById(contentId);
492                
493                if (hasRight("CMS_Rights_CommentModerate", content))
494                {
495                    // For each associated comment
496                    for (String commentId : contents.get(contentId))
497                    {
498                        Map<String, Object> commentParams = new HashMap<>();
499                        commentParams.put("id", commentId);
500                        commentParams.put("contentId", contentId);
501                        commentParams.put("contentTitle", _contentHelper.getTitle(content));
502                        
503                        if (content instanceof CommentableContent)
504                        {
505                            CommentableContent cContent = (CommentableContent) content;
506        
507                            Comment comment = cContent.getComment(commentId);
508                            commentParams.put("reportsCount", comment.getReportsCount());
509                            if (comment.isValidated())
510                            {
511                                comment.setValidated(false);
512                                cContent.saveChanges();
513
514                                List<Map<String, Object>> validatedComments = (List<Map<String, Object>>) results.get("unvalidated-comments");
515                                validatedComments.add(commentParams);
516
517                                Map<String, Object> eventParams = new HashMap<>();
518                                eventParams.put(ObservationConstants.ARGS_CONTENT, content);
519                                eventParams.put(ObservationConstants.ARGS_COMMENT, comment);
520                                _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_COMMENT_UNVALIDATED, getCurrentUser(), eventParams));
521                            }
522                        }
523                        else
524                        {
525                            getLogger().error("Can not validate a comment on a non commentable content");
526                            
527                            List<Map<String, Object>> errorComments = (List<Map<String, Object>>) results.get("error-comments");
528                            errorComments.add(commentParams);
529                        }
530                    }
531                }
532                else
533                {
534                    // No right
535                    getLogger().error("User '" + getCurrentUser() + "' does not have right to validate comments on content '" + content.getId() + "'");
536                    
537                    Map<String, Object> contentParams = new HashMap<>();
538                    contentParams.put("contentId", contentId);
539                    contentParams.put("contentTitle", _contentHelper.getTitle(content));
540                }
541            }
542            catch (UnknownAmetysObjectException e)
543            {
544                getLogger().error("Can not validate a comment on a non existing content", e);
545                
546                Map<String, Object> contentParams = new HashMap<>();
547                contentParams.put("contentId", contentId);
548                
549                List<Map<String, Object>> unknownContents = (List<Map<String, Object>>) results.get("unknown-contents");
550                unknownContents.add(contentParams);
551            }
552        }
553
554        return results;
555    }
556    
557    /**
558     * Jsonify the comment.
559     * @param comment The comment
560     * @param content The content
561     * @return commentMap the comment map
562     */
563    protected Map<String, Object> _jsonifyComment (Comment comment, Content content) 
564    {
565        Map<String, Object> result = new HashMap<> ();
566        
567        Map<String, Object> commentMap = new HashMap<> ();
568            
569        
570        commentMap.put("validated", Boolean.toString(comment.isValidated()));
571        commentMap.put("id", comment.getId());
572        commentMap.put("creationDate", DateUtils.zonedDateTimeToString(comment.getCreationDate()));
573        
574        String authorName = comment.getAuthorName();
575        if (authorName != null)
576        {
577            commentMap.put("author-name", authorName);
578        }
579        
580        String authorEmail = comment.getAuthorEmail();
581        if (authorEmail != null)
582        {
583            Map<String, Object> authorEmailMap = new HashMap<> ();
584            authorEmailMap.put("hidden", Boolean.toString(comment.isEmailHidden()));
585            authorEmailMap.put("value", authorEmail);
586            commentMap.put("author-email", authorEmailMap);
587        }
588        
589        String authorUrl = comment.getAuthorURL();
590        if (authorUrl != null)
591        {
592            commentMap.put("author-url", authorUrl);
593        }
594        
595        String text = comment.getContent();
596        if (text != null)
597        {
598            commentMap.put("text", comment.getContent());
599        }
600        commentMap.put("content", _jsonifyContent(content));
601        
602        result.put("comment", commentMap);
603        
604        return result;
605    }
606    
607    /**
608     * Jsonify the content.
609     * @param content The content
610     * @return contentMap the content map
611     */
612    protected Map<String, Object> _jsonifyContent (Content content) 
613    {
614        Map<String, Object> contentMap = new HashMap<> ();
615        
616        contentMap.put("id", content.getId());
617        contentMap.put("title", _contentHelper.getTitle(content));
618        contentMap.put("name", content.getName());
619        
620        return contentMap;
621    }
622
623    /**
624     * Get the parameters for a comment
625     * @param content The content
626     * @param comment The comment
627     * @return The parameters
628     */
629    protected Map<String, Object> getCommentParameters (Content content, Comment comment)
630    {
631        Map<String, Object> params = new HashMap<>();
632        
633        params.put("contentId", content.getId());
634        params.put("contentTitle", _contentHelper.getTitle(content));
635        params.put("id", comment.getId());
636        params.put("validated", comment.isValidated());
637        params.put("reportsCount", comment.getReportsCount());
638        
639        return params;
640    }
641    
642    /**
643     * Get the current user
644     * @return The current user
645     */
646    protected UserIdentity getCurrentUser ()
647    {
648        return _userProvider.getUser();
649    }
650
651    /**
652     * Determines if connected user has right on content
653     * @param rightId The right id
654     * @param content The content
655     * @return true if user has right
656     */
657    protected boolean hasRight (String rightId, Content content)
658    {
659        UserIdentity user = _userProvider.getUser();
660        
661        return _rightManager.hasRight(user, "CMS_Rights_CommentModerate", content) == RightResult.RIGHT_ALLOW;
662    }
663}