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    protected void fetchInvalidationsIfNeeded() {
128        // fetch invalidations from unrestricted session if needed
129        if (!documentManager.isStateSharedByAllThreadSessions()) {
130            documentManager.save();
131        }
132    }
133
134    /**
135     * Adds the post to the thread and starts the moderation WF on the post created.
136     */
137    @Override
138    public String addPost() {
139        DocumentModel dm = documentManager.createDocumentModel("Post");
140
141        dm.setProperty("post", "author", commentManagerActions.getPrincipalName());
142
143        dm.setProperty("post", "title", title);
144        dm.setProperty("post", "text", text);
145        dm.setProperty("post", "creationDate", new Date());
146        dm.setProperty("post", "filename", filename);
147        dm.setProperty("post", "fileContent", fileContent);
148
149        // save it to the repository
150        dm = commentManagerActions.addComment(dm);
151
152        if (threadAction.isCurrentThreadModerated() && !threadAction.isPrincipalModerator()) {
153            // start moderation workflow + warn user that post
154            // won't be displayed until moderation kicks in
155            startModeration(dm);
156            facesMessages.add(StatusMessage.Severity.INFO,
157                    resourcesAccessor.getMessages().get("label.comment.waiting_approval"));
158        } else {
159            // publish post
160            DocumentRef postRef = dm.getRef();
161            if (documentManager.hasPermission(postRef, SecurityConstants.WRITE_LIFE_CYCLE)) {
162                documentManager.followTransition(postRef, ForumConstants.TRANSITION_TO_PUBLISHED_STATE);
163                documentManager.save();
164            } else {
165                // Here user only granted with read rights should be able to
166                // create a post => open a system session to put it in
167                // published
168                // state
169                try (CoreSession systemSession = CoreInstance.openCoreSessionSystem(currentServerLocation.getName())) {
170                    // follow transition
171                    systemSession.followTransition(dm.getRef(), ForumConstants.TRANSITION_TO_PUBLISHED_STATE);
172                    systemSession.save();
173                }
174            }
175            fetchInvalidationsIfNeeded();
176            // NXP-1262 display the message only when about to publish
177            facesMessages.add(StatusMessage.Severity.INFO,
178                    resourcesAccessor.getMessages().get("label.comment.added.sucess"));
179        }
180
181        // force comment manager to reload posts
182        commentManagerActions.documentChanged();
183        cleanContextVariables();
184
185        return navigationContext.navigateToDocument(getParentThread());
186    }
187
188    @Override
189    public String cancelPost() {
190        cleanContextVariables();
191        commentManagerActions.cancelComment();
192        fetchInvalidationsIfNeeded();
193        return navigationContext.navigateToDocument(getParentThread());
194    }
195
196    @Override
197    public String deletePost() {
198        if (deletePostId == null) {
199            throw new NuxeoException("No id for post to delete");
200        }
201
202        DocumentModel thread = getParentThread();
203        DocumentModel post = documentManager.getDocument(new IdRef(deletePostId));
204
205        if (threadAction.isThreadModerated(thread)
206                && ForumConstants.PENDING_STATE.equals(post.getCurrentLifeCycleState())) {
207            Task task = getModerationTask(thread, deletePostId);
208            if (task != null) {
209                taskService.deleteTask(documentManager, task.getId());
210            }
211        }
212        commentManagerActions.deleteComment(deletePostId);
213
214        fetchInvalidationsIfNeeded();
215        Events.instance().raiseEvent(TaskEventNames.WORKFLOW_ENDED);
216
217        return navigationContext.navigateToDocument(getParentThread());
218    }
219
220    @Override
221    public String rejectPost(DocumentModel post) {
222        DocumentModel thread = getParentThread();
223
224        Task moderationTask = getModerationTask(thread, post.getId());
225        if (moderationTask == null) {
226            throw new NuxeoException("No moderation task found");
227        }
228
229        taskService.rejectTask(documentManager, (NuxeoPrincipal) currentUser, moderationTask, null);
230
231        Events.instance().raiseEvent(TaskEventNames.WORKFLOW_TASK_COMPLETED);
232
233        // force comment manager to reload posts
234        commentManagerActions.documentChanged();
235
236        fetchInvalidationsIfNeeded();
237
238        return navigationContext.navigateToDocument(getParentThread());
239    }
240
241    /**
242     * Ends the task on a post.
243     */
244    @Override
245    public String approvePost(DocumentModel post) {
246        DocumentModel thread = getParentThread();
247
248        Task moderationTask = getModerationTask(thread, post.getId());
249        if (moderationTask == null) {
250            throw new NuxeoException("No moderation task found");
251        }
252        taskService.acceptTask(documentManager, (NuxeoPrincipal) currentUser, moderationTask, null);
253
254        Events.instance().raiseEvent(TaskEventNames.WORKFLOW_TASK_COMPLETED);
255
256        // force comment manager to reload posts
257        commentManagerActions.documentChanged();
258
259        fetchInvalidationsIfNeeded();
260
261        return navigationContext.navigateToDocument(getParentThread());
262    }
263
264    @Override
265    public DocumentModel getParentThread() {
266        return navigationContext.getCurrentDocument();
267    }
268
269    @Override
270    public boolean isPostPublished(DocumentModel post) {
271        boolean published = false;
272        if (post != null && ForumConstants.PUBLISHED_STATE.equals(post.getCurrentLifeCycleState())) {
273            published = true;
274        }
275        return published;
276    }
277
278    /**
279     * Starts the moderation on given Post.
280     */
281    @SuppressWarnings("unchecked")
282    protected void startModeration(DocumentModel post) {
283
284        DocumentModel thread = getParentThread();
285        List<String> moderators = (ArrayList<String>) thread.getProperty("thread", "moderators");
286
287        if (moderators == null || moderators.isEmpty()) {
288            throw new NuxeoException("No moderators defined");
289        }
290
291        Map<String, String> vars = new HashMap<String, String>();
292        vars.put(ForumConstants.COMMENT_ID, post.getId());
293        vars.put(CreateTask.OperationTaskVariableName.createdFromCreateTaskOperation.name(), "false");
294        vars.put(Task.TaskVariableName.needi18n.name(), "true");
295        vars.put(Task.TaskVariableName.taskType.name(), ForumConstants.FORUM_TASK_TYPE);
296
297        vars.put(CreateTask.OperationTaskVariableName.acceptOperationChain.name(), CommentsConstants.ACCEPT_CHAIN_NAME);
298        vars.put(CreateTask.OperationTaskVariableName.rejectOperationChain.name(), CommentsConstants.REJECT_CHAIN_NAME);
299
300        taskService.createTask(documentManager, (NuxeoPrincipal) currentUser, thread,
301                ForumConstants.MODERATION_TASK_NAME, moderators, false, ForumConstants.MODERATION_TASK_NAME, null,
302                null, vars, null);
303        Events.instance().raiseEvent(TaskEventNames.WORKFLOW_NEW_STARTED);
304
305    }
306
307    protected Task getModerationTask(DocumentModel thread, String postId) {
308        List<Task> tasks = DocumentTaskProvider.getTasks("GET_FORUM_MODERATION_TASKS", documentManager, false, null,
309                thread.getId(), postId);
310        if (tasks != null && !tasks.isEmpty()) {
311            if (tasks.size() > 1) {
312                log.error("There are several moderation workflows running, " + "taking only first found");
313            }
314            Task task = tasks.get(0);
315            return task;
316        }
317        return null;
318    }
319
320    protected void cleanContextVariables() {
321        fileContent = null;
322        filename = null;
323        text = null;
324        title = null;
325    }
326
327    // getters/setters
328
329    @Override
330    public String getText() {
331        return text;
332    }
333
334    @Override
335    public void setText(String text) {
336        this.text = text;
337    }
338
339    @Override
340    public String getFilename() {
341        return filename;
342    }
343
344    @Override
345    public void setFilename(String filename) {
346        this.filename = filename;
347    }
348
349    @Override
350    public Blob getFileContent() {
351        return fileContent;
352    }
353
354    @Override
355    public void setFileContent(Blob fileContent) {
356        this.fileContent = fileContent;
357    }
358
359    /**
360     * Gets the title of the post for creation purpose. If the post to be created reply to a previous post, the title of
361     * the new post comes with the previous title, and a prefix (i.e : Re : Previous Title).
362     */
363    @Override
364    public String getTitle() {
365
366        String previousId = commentManagerActions.getSavedReplyCommentId();
367        if (previousId != null && !"".equals(previousId)) {
368            DocumentModel previousPost = documentManager.getDocument(new IdRef(previousId));
369
370            // Test to ensure that previous comment got the "post" schema
371            if (previousPost.getDataModel("post") != null) {
372                String previousTitle = (String) previousPost.getProperty("post", "title");
373                String prefix = resourcesAccessor.getMessages().get("label.forum.post.title.prefix");
374                title = prefix + previousTitle;
375            }
376        }
377        return title;
378    }
379
380    @Override
381    public void setTitle(String title) {
382        this.title = title;
383    }
384
385}