001/*
002 * (C) Copyright 2006-2007 Nuxeo SA (http://nuxeo.com/) and others.
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 * Contributors:
017 *     ${user}
018 *
019 * $Id
020 */
021
022package org.nuxeo.ecm.platform.forum.web;
023
024import java.security.Principal;
025import java.util.ArrayList;
026import java.util.Date;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033import org.jboss.seam.ScopeType;
034import org.jboss.seam.annotations.In;
035import org.jboss.seam.annotations.Name;
036import org.jboss.seam.annotations.Scope;
037import org.jboss.seam.annotations.web.RequestParameter;
038import org.jboss.seam.core.Events;
039import org.jboss.seam.faces.FacesMessages;
040import org.jboss.seam.international.StatusMessage;
041import org.nuxeo.ecm.automation.task.CreateTask;
042import org.nuxeo.ecm.core.api.Blob;
043import org.nuxeo.ecm.core.api.CoreInstance;
044import org.nuxeo.ecm.core.api.CoreSession;
045import org.nuxeo.ecm.core.api.DocumentModel;
046import org.nuxeo.ecm.core.api.DocumentRef;
047import org.nuxeo.ecm.core.api.IdRef;
048import org.nuxeo.ecm.core.api.NuxeoException;
049import org.nuxeo.ecm.core.api.NuxeoPrincipal;
050import org.nuxeo.ecm.core.api.security.SecurityConstants;
051import org.nuxeo.ecm.platform.comment.web.CommentManagerActions;
052import org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants;
053import org.nuxeo.ecm.platform.forum.web.api.PostAction;
054import org.nuxeo.ecm.platform.forum.web.api.ThreadAction;
055import org.nuxeo.ecm.platform.forum.workflow.ForumConstants;
056import org.nuxeo.ecm.platform.task.Task;
057import org.nuxeo.ecm.platform.task.TaskEventNames;
058import org.nuxeo.ecm.platform.task.TaskService;
059import org.nuxeo.ecm.platform.task.core.service.DocumentTaskProvider;
060import org.nuxeo.ecm.platform.ui.web.api.NavigationContext;
061import org.nuxeo.ecm.platform.util.RepositoryLocation;
062import org.nuxeo.ecm.webapp.helpers.ResourcesAccessor;
063
064/**
065 * This action listener is used to create a Post inside a Thread and also to handle the moderation cycle on Post.
066 *
067 * @author <a href="bchaffangeon@nuxeo.com">Brice Chaffangeon</a>
068 */
069@Name("postAction")
070@Scope(ScopeType.CONVERSATION)
071public class PostActionBean implements PostAction {
072
073    private static final long serialVersionUID = 1L;
074
075    private static final Log log = LogFactory.getLog(PostActionBean.class);
076
077    @In(create = true)
078    protected ThreadAction threadAction;
079
080    @In(create = true)
081    protected transient CommentManagerActions commentManagerActions;
082
083    @In(create = true, required = false)
084    protected transient CoreSession documentManager;
085
086    @In(create = true)
087    protected transient NavigationContext navigationContext;
088
089    @In(create = true)
090    protected transient TaskService taskService;
091
092    @In(required = false)
093    protected RepositoryLocation currentServerLocation;
094
095    @In(create = true)
096    protected transient Principal currentUser;
097
098    @In(create = true, required = false)
099    protected FacesMessages facesMessages;
100
101    @In(create = true)
102    protected ResourcesAccessor resourcesAccessor;
103
104    // the id of the comment to delete
105    @RequestParameter
106    protected String deletePostId;
107
108    protected String title;
109
110    protected String text;
111
112    protected String filename;
113
114    protected Blob fileContent;
115
116    @Override
117    public boolean checkWritePermissionOnThread() {
118        DocumentModel currentDocument = navigationContext.getCurrentDocument();
119        if (currentDocument != null) {
120            return documentManager.hasPermission(currentDocument.getRef(), SecurityConstants.READ_WRITE);
121        } else {
122            log.error("Cannot check write permission on thread: " + "no current document found");
123        }
124        return false;
125    }
126
127    /**
128     * Adds the post to the thread and starts the moderation WF on the post created.
129     */
130    @Override
131    public String addPost() {
132        DocumentModel dm = documentManager.createDocumentModel("Post");
133
134        dm.setProperty("post", "author", commentManagerActions.getPrincipalName());
135
136        dm.setProperty("post", "title", title);
137        dm.setProperty("post", "text", text);
138        dm.setProperty("post", "creationDate", new Date());
139        dm.setProperty("post", "filename", filename);
140        dm.setProperty("post", "fileContent", fileContent);
141
142        // save it to the repository
143        dm = commentManagerActions.addComment(dm);
144
145        if (threadAction.isCurrentThreadModerated() && !threadAction.isPrincipalModerator()) {
146            // start moderation workflow + warn user that post
147            // won't be displayed until moderation kicks in
148            startModeration(dm);
149            facesMessages.add(StatusMessage.Severity.INFO,
150                    resourcesAccessor.getMessages().get("label.comment.waiting_approval"));
151        } else {
152            // publish post
153            DocumentRef postRef = dm.getRef();
154            if (documentManager.hasPermission(postRef, SecurityConstants.WRITE_LIFE_CYCLE)) {
155                documentManager.followTransition(postRef, ForumConstants.TRANSITION_TO_PUBLISHED_STATE);
156                documentManager.save();
157            } else {
158                // Here user only granted with read rights should be able to
159                // create a post => open a system session to put it in
160                // published
161                // state
162                try (CoreSession systemSession = CoreInstance.openCoreSessionSystem(currentServerLocation.getName())) {
163                    // follow transition
164                    systemSession.followTransition(dm.getRef(), ForumConstants.TRANSITION_TO_PUBLISHED_STATE);
165                    systemSession.save();
166                }
167            }
168            // NXP-1262 display the message only when about to publish
169            facesMessages.add(StatusMessage.Severity.INFO,
170                    resourcesAccessor.getMessages().get("label.comment.added.sucess"));
171        }
172
173        // force comment manager to reload posts
174        commentManagerActions.documentChanged();
175        cleanContextVariables();
176
177        return navigationContext.navigateToDocument(getParentThread());
178    }
179
180    @Override
181    public String cancelPost() {
182        cleanContextVariables();
183        commentManagerActions.cancelComment();
184        return navigationContext.navigateToDocument(getParentThread());
185    }
186
187    @Override
188    public String deletePost() {
189        if (deletePostId == null) {
190            throw new NuxeoException("No id for post to delete");
191        }
192
193        DocumentModel thread = getParentThread();
194        DocumentModel post = documentManager.getDocument(new IdRef(deletePostId));
195
196        if (threadAction.isThreadModerated(thread)
197                && ForumConstants.PENDING_STATE.equals(post.getCurrentLifeCycleState())) {
198            Task task = getModerationTask(thread, deletePostId);
199            if (task != null) {
200                taskService.deleteTask(documentManager, task.getId());
201            }
202        }
203        commentManagerActions.deleteComment(deletePostId);
204
205        Events.instance().raiseEvent(TaskEventNames.WORKFLOW_ENDED);
206
207        return navigationContext.navigateToDocument(getParentThread());
208    }
209
210    @Override
211    public String rejectPost(DocumentModel post) {
212        DocumentModel thread = getParentThread();
213
214        Task moderationTask = getModerationTask(thread, post.getId());
215        if (moderationTask == null) {
216            throw new NuxeoException("No moderation task found");
217        }
218
219        taskService.rejectTask(documentManager, (NuxeoPrincipal) currentUser, moderationTask, null);
220
221        Events.instance().raiseEvent(TaskEventNames.WORKFLOW_TASK_COMPLETED);
222
223        // force comment manager to reload posts
224        commentManagerActions.documentChanged();
225
226        return navigationContext.navigateToDocument(getParentThread());
227    }
228
229    /**
230     * Ends the task on a post.
231     */
232    @Override
233    public String approvePost(DocumentModel post) {
234        DocumentModel thread = getParentThread();
235
236        Task moderationTask = getModerationTask(thread, post.getId());
237        if (moderationTask == null) {
238            throw new NuxeoException("No moderation task found");
239        }
240        taskService.acceptTask(documentManager, (NuxeoPrincipal) currentUser, moderationTask, null);
241
242        Events.instance().raiseEvent(TaskEventNames.WORKFLOW_TASK_COMPLETED);
243
244        // force comment manager to reload posts
245        commentManagerActions.documentChanged();
246
247        return navigationContext.navigateToDocument(getParentThread());
248    }
249
250    @Override
251    public DocumentModel getParentThread() {
252        return navigationContext.getCurrentDocument();
253    }
254
255    @Override
256    public boolean isPostPublished(DocumentModel post) {
257        boolean published = false;
258        if (post != null && ForumConstants.PUBLISHED_STATE.equals(post.getCurrentLifeCycleState())) {
259            published = true;
260        }
261        return published;
262    }
263
264    /**
265     * Starts the moderation on given Post.
266     */
267    @SuppressWarnings("unchecked")
268    protected void startModeration(DocumentModel post) {
269
270        DocumentModel thread = getParentThread();
271        List<String> moderators = (ArrayList<String>) thread.getProperty("thread", "moderators");
272
273        if (moderators == null || moderators.isEmpty()) {
274            throw new NuxeoException("No moderators defined");
275        }
276
277        Map<String, String> vars = new HashMap<String, String>();
278        vars.put(ForumConstants.COMMENT_ID, post.getId());
279        vars.put(CreateTask.OperationTaskVariableName.createdFromCreateTaskOperation.name(), "false");
280        vars.put(Task.TaskVariableName.needi18n.name(), "true");
281        vars.put(Task.TaskVariableName.taskType.name(), ForumConstants.FORUM_TASK_TYPE);
282
283        vars.put(CreateTask.OperationTaskVariableName.acceptOperationChain.name(), CommentsConstants.ACCEPT_CHAIN_NAME);
284        vars.put(CreateTask.OperationTaskVariableName.rejectOperationChain.name(), CommentsConstants.REJECT_CHAIN_NAME);
285
286        taskService.createTask(documentManager, (NuxeoPrincipal) currentUser, thread,
287                ForumConstants.MODERATION_TASK_NAME, moderators, false, ForumConstants.MODERATION_TASK_NAME, null,
288                null, vars, null);
289        Events.instance().raiseEvent(TaskEventNames.WORKFLOW_NEW_STARTED);
290
291    }
292
293    protected Task getModerationTask(DocumentModel thread, String postId) {
294        List<Task> tasks = DocumentTaskProvider.getTasks("GET_FORUM_MODERATION_TASKS", documentManager, false, null,
295                thread.getId(), postId);
296        if (tasks != null && !tasks.isEmpty()) {
297            if (tasks.size() > 1) {
298                log.error("There are several moderation workflows running, " + "taking only first found");
299            }
300            Task task = tasks.get(0);
301            return task;
302        }
303        return null;
304    }
305
306    protected void cleanContextVariables() {
307        fileContent = null;
308        filename = null;
309        text = null;
310        title = null;
311    }
312
313    // getters/setters
314
315    @Override
316    public String getText() {
317        return text;
318    }
319
320    @Override
321    public void setText(String text) {
322        this.text = text;
323    }
324
325    @Override
326    public String getFilename() {
327        return filename;
328    }
329
330    @Override
331    public void setFilename(String filename) {
332        this.filename = filename;
333    }
334
335    @Override
336    public Blob getFileContent() {
337        return fileContent;
338    }
339
340    @Override
341    public void setFileContent(Blob fileContent) {
342        this.fileContent = fileContent;
343    }
344
345    /**
346     * Gets the title of the post for creation purpose. If the post to be created reply to a previous post, the title of
347     * the new post comes with the previous title, and a prefix (i.e : Re : Previous Title).
348     */
349    @Override
350    public String getTitle() {
351
352        String previousId = commentManagerActions.getSavedReplyCommentId();
353        if (previousId != null && !"".equals(previousId)) {
354            DocumentModel previousPost = documentManager.getDocument(new IdRef(previousId));
355
356            // Test to ensure that previous comment got the "post" schema
357            if (previousPost.hasSchema("post")) {
358                String previousTitle = (String) previousPost.getProperty("post", "title");
359                String prefix = resourcesAccessor.getMessages().get("label.forum.post.title.prefix");
360                title = prefix + previousTitle;
361            }
362        }
363        return title;
364    }
365
366    @Override
367    public void setTitle(String title) {
368        this.title = title;
369    }
370
371}