1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.module.forum;
35
36 import info.magnolia.cms.core.Content;
37 import info.magnolia.cms.core.HierarchyManager;
38 import info.magnolia.cms.core.NodeData;
39 import info.magnolia.cms.core.Path;
40 import info.magnolia.cms.security.AccessDeniedException;
41 import info.magnolia.cms.security.AccessManager;
42 import info.magnolia.cms.security.Permission;
43 import info.magnolia.cms.security.Role;
44 import info.magnolia.cms.security.RoleManager;
45 import info.magnolia.cms.security.SecuritySupport;
46 import info.magnolia.cms.security.User;
47 import info.magnolia.cms.security.UserManager;
48 import info.magnolia.cms.util.ExclusiveWrite;
49 import info.magnolia.cms.util.HierarchyManagerUtil;
50 import info.magnolia.cms.util.NodeDataUtil;
51 import info.magnolia.cms.util.SiblingsHelper;
52 import info.magnolia.context.MgnlContext;
53 import info.magnolia.module.ModuleRegistry;
54
55 import java.text.MessageFormat;
56 import java.util.Calendar;
57 import java.util.Collection;
58
59 import javax.jcr.RepositoryException;
60 import javax.jcr.query.Query;
61 import javax.jcr.query.QueryManager;
62 import javax.jcr.query.QueryResult;
63
64 import org.apache.commons.lang3.StringUtils;
65 import org.apache.commons.lang3.time.DateFormatUtils;
66
67
68
69
70
71
72
73 public class DefaultForumManager implements ForumManager {
74 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultForumManager.class);
75
76 public static final String FORUM_WORKSPACE = "forum";
77 public static final String FORUM_NODETYPE = "mgnl:forum";
78 public static final String THREAD_NODETYPE = "mgnl:thread";
79 public static final String MESSAGE_NODETYPE = "mgnl:message";
80 public static final String ALLOWS_NESTING_MESSAGES = "allowsNestingMessages";
81 public static final String VALIDATED_PROPERTY = "validated";
82 public static final String LOCKED_PROPERTY = "locked";
83 public static final String FIRST_MESSAGE_PROPERTY = "firstMessage";
84 public static final String LAST_MESSAGE_PROPERTY = "lastMessage";
85 public static final String CREATION_DATE_PROPERTY = "creationDate";
86 public static final String TITLE_PROPERTY = "title";
87 public static final String AUTHORID_PROPERTY = "authorId";
88 public static final String CONTENT_PROPERTY = "content";
89
90 public static final String ROLE_FORUM_ALL_ADMIN = "forum_ALL-admin";
91 public static final String ROLE_FORUM_ALL_MODERATOR = "forum_ALL-moderator";
92 public static final String ROLE_FORUM_ALL_USER = "forum_ALL-user";
93
94
95
96
97 public static final String ROLE_FORUM_BASE = "forum-base";
98
99
100
101 public static final String ROLE_FORUM_MODERATOR_BASE = "forum-moderator-base";
102
103 private static final String MESSAGES_QUERY_PATTERN = "select * from mgnl:message where jcr:path like ''{0}/%'' and (" + VALIDATED_PROPERTY + "=''true''{1}){2}";
104 private static final String SHOW_UNVALIDATED_MESSAGES_CLAUSE = " or " + VALIDATED_PROPERTY + " is null";
105
106 private final ForumConfiguration config;
107
108 public DefaultForumManager() {
109 this(ModuleRegistry.Factory.getInstance().getModuleInstance(ForumConfiguration.class));
110 }
111
112
113 public DefaultForumManager(ForumConfiguration config) {
114 this.config = config;
115 }
116
117 @Override
118 public Collection<Content> getForumList() throws RepositoryException {
119 final HierarchyManager hm = getHierarchyManager();
120 return hm.getRoot().getChildren(FORUM_NODETYPE);
121 }
122
123 @Override
124 public String getForumId(String shortName) throws RepositoryException {
125 if (!StringUtils.startsWith(shortName, "/")) {
126 shortName = "/" + shortName;
127 }
128 return getHierarchyManager().getContent(shortName).getUUID();
129 }
130
131 @Override
132 public Content getForum(String forumID) throws RepositoryException {
133 final HierarchyManager hm = getHierarchyManager();
134 return getForum(hm, forumID);
135 }
136
137 @Override
138 public Collection<Content> getThreads(Content forum) throws RepositoryException {
139 final boolean showingUnvalidatedMessages = config.isShowingUnvalidatedMessages();
140 return forum.getChildren(new ThreadsFilter(showingUnvalidatedMessages));
141 }
142
143 @Override
144 public Content createForum(String name, String title) throws RepositoryException {
145 return createForum(name, title, false);
146 }
147
148 @Override
149 public Content createForum(String name, String title, boolean withMessageNesting) throws RepositoryException {
150 final HierarchyManager hm = getHierarchyManager();
151 final String cleanName = Path.getUniqueLabel(hm.getWorkspace().getSession(), "/", cleanup(name));
152 final Content repoRoot = hm.getRoot();
153 final Content newForum = repoRoot.createContent(cleanName, FORUM_NODETYPE);
154 newForum.createNodeData("title", title);
155 newForum.setNodeData(ALLOWS_NESTING_MESSAGES, withMessageNesting);
156 repoRoot.save();
157
158 if (config.isCreateRolesForNewForums()) {
159 createForumRoles(newForum);
160 }
161 return newForum;
162 }
163
164 protected void createForumRoles(Content forumNode) {
165 final String forumName = forumNode.getName();
166 final String path = forumNode.getHandle();
167
168 MgnlContext.doInSystemContext(new MgnlContext.VoidOp() {
169 @Override
170 public void doExec() {
171 try {
172 final RoleManager roleManager = SecuritySupport.Factory.getInstance().getRoleManager();
173 String roleName = "forum-" + forumName + "-user";
174 if (roleManager.getRole(roleName) == null) {
175 final Role user = roleManager.createRole(roleName);
176 addPermission(user, path, Permission.ALL, roleManager);
177 }
178 } catch (Exception e) {
179 throw new RuntimeException(e);
180 }
181 }
182 });
183 }
184
185 protected void addPermission(Role user, String path, long permission, RoleManager roleManager) {
186 roleManager.addPermission(user, config.getRepository(), path, permission);
187 roleManager.addPermission(user, config.getRepository(), path + "/*", permission);
188 }
189
190 @Override
191 public Content createThread(String forumID, String threadTitle, String messageText, String author, boolean isAnonymous) throws RepositoryException {
192 return createThread(forumID, threadTitle, threadTitle, messageText, author, isAnonymous);
193 }
194
195 @Override
196 public Content createThread(String forumID, String threadTitle, String messageTitle, String messageText, String author, boolean isAnonymous) throws RepositoryException {
197
198 if (StringUtils.isEmpty(threadTitle)) {
199 throw new IllegalArgumentException("Thread title must be specified");
200 }
201 final HierarchyManager hm = getHierarchyManager();
202 final Content forum = getForum(hm, forumID);
203 checkedLockedOrModerator(hm, forum);
204
205 final String shortTitle = cleanup(threadTitle);
206 final Content thread = forum.createContent(shortTitle, THREAD_NODETYPE);
207 thread.createNodeData(TITLE_PROPERTY, threadTitle);
208 final String messageName = generateMessageName();
209 final Content message = createMessageInThread(forum, thread, messageName, messageTitle, messageText, author, isAnonymous);
210 thread.createNodeData(DefaultForumManager.FIRST_MESSAGE_PROPERTY, message);
211 forum.save();
212 return thread;
213 }
214
215 @Override
216 public Content replyToThread(String threadID, String inReplyToID, String messageTitle, String messageText, String author, boolean isAnonymous) throws RepositoryException {
217 synchronized (ExclusiveWrite.getInstance()) {
218 final HierarchyManager hm = getHierarchyManager();
219 final Content thread = getThread(hm, threadID);
220 final Content forum = getForumFromThread(thread);
221 checkedLockedOrModerator(hm, thread);
222
223 final Content inReplyTo;
224 if (inReplyToID != null) {
225 inReplyTo = hm.getContentByUUID(inReplyToID);
226 } else {
227 inReplyTo = null;
228 }
229
230 final String messageName = generateMessageName();
231 final Content newMessage = createMessageInThread(forum, thread, inReplyTo, messageName, messageTitle, messageText, author, isAnonymous);
232 forum.save();
233 return newMessage;
234 }
235 }
236
237 @Override
238 public boolean isAllowedToPostOnForum(Content forum) {
239 return isAllowedToPostOn(forum);
240 }
241
242 @Override
243 public boolean isAllowedToPostOnThread(Content thread) {
244 return isAllowedToPostOn(thread);
245 }
246
247 protected boolean isAllowedToPostOn(Content node) {
248 try {
249 final AccessManager am = MgnlContext.getAccessManager(node.getWorkspace().getName());
250 return am.isGranted(node.getHandle(), Permission.WRITE);
251 } catch (RepositoryException e) {
252 throw new RuntimeException(e);
253 }
254 }
255
256
257
258
259 protected void checkedLockedOrModerator(HierarchyManager hm, Content node) throws AccessDeniedException {
260 final boolean locked = node.getNodeData(LOCKED_PROPERTY).getBoolean();
261 if (locked) {
262 isModerator(hm, node);
263 }
264 }
265
266
267
268
269 protected void isModerator(HierarchyManager hm, Content node) throws AccessDeniedException {
270
271
272
273
274 isModerator();
275 }
276
277 @Override
278 public void isModerator() throws AccessDeniedException{
279 User currentUser = MgnlContext.getUser();
280
281
282 Collection<String> allRoles = currentUser.getAllRoles();
283 if (!allRoles.contains(ROLE_FORUM_ALL_MODERATOR) && !allRoles.contains(ROLE_FORUM_ALL_ADMIN)) {
284 throw new AccessDeniedException("User not allowed to perform that action.");
285 }
286 }
287
288 @Override
289 public Content getThread(String threadID) throws RepositoryException {
290 final HierarchyManager hm = getHierarchyManager();
291 return getThread(hm, threadID);
292 }
293
294 @Override
295 public PagedResult getMessages(Content thread, long page) throws RepositoryException {
296 if (page <= 0) {
297 throw new IllegalArgumentException("Page number must be >= 1");
298 }
299
300 final boolean showingUnvalidatedMessages = config.isShowingUnvalidatedMessages();
301
302 final String threadPath = thread.getHandle();
303 final HierarchyManager hm = getHierarchyManager();
304 final QueryManager qm = hm.getWorkspace().getQueryManager();
305 final Query query = qm.createQuery(getMessagesQueryString(threadPath, showingUnvalidatedMessages, ""), Query.SQL);
306 final QueryResult queryResult = query.execute();
307
308 final Pager pager = new Pager(hm, queryResult);
309 final long skip = config.getMessagesPerPage() * (page - 1);
310 return pager.page(skip, config.getMessagesPerPage());
311 }
312
313 @Override
314 public Collection<Content> getForumMessages(String forumName) throws RepositoryException {
315 final String forumPath = "/" + forumName;
316 final boolean showUnvalidatedMessages = config.isShowingUnvalidatedMessages();
317 final String sql = getMessagesQueryString(forumPath, showUnvalidatedMessages, " order by creationDate desc");
318 final HierarchyManager hm = getHierarchyManager();
319 final info.magnolia.cms.core.search.QueryManager qm = hm.getQueryManager();
320 info.magnolia.cms.core.search.Query query = qm.createQuery(sql, Query.SQL);
321 info.magnolia.cms.core.search.QueryResult result = query.execute();
322 return result.getContent(MESSAGE_NODETYPE);
323 }
324
325 protected String getMessagesQueryString(String path, boolean showUnvalidatedMessages, String orderClause) {
326 return MessageFormat.format(MESSAGES_QUERY_PATTERN, new String[] { path, showUnvalidatedMessages ? SHOW_UNVALIDATED_MESSAGES_CLAUSE : "", orderClause });
327 }
328
329 @Override
330 public Content getForumFromThread(Content thread) throws RepositoryException {
331
332 Content forum = thread.getParent();
333 while (!forum.isNodeType(FORUM_NODETYPE)) {
334 forum = forum.getParent();
335 }
336 return forum;
337 }
338
339 @Override
340 public Content getThreadFromMessage(Content message) throws RepositoryException {
341
342 Content thread = message.getParent();
343 while (!thread.isNodeType(THREAD_NODETYPE)) {
344 thread = thread.getParent();
345 }
346 return thread;
347 }
348
349 protected Content getForum(HierarchyManager hm, String forumID) throws RepositoryException {
350 return hm.getContentByUUID(forumID);
351 }
352
353 protected Content getThread(HierarchyManager hm, String threadID) throws RepositoryException {
354 return hm.getContentByUUID(threadID);
355 }
356
357 @Override
358 public void deleteForum(String forumID) throws RepositoryException {
359 deleteByUuid(forumID);
360 }
361
362 @Override
363 public void deleteThread(String threadID) throws RepositoryException {
364
365 final Content thread = getThread(threadID);
366 final Content forum = getForumFromThread(thread);
367 isModerator(getHierarchyManager(), thread);
368 checkAndUpdateLastMessage(forum, threadID);
369 deleteByUuid(threadID);
370 }
371
372 @Override
373 public void deleteMessage(String messageID) throws RepositoryException {
374 final Content message = getHierarchyManager().getContentByUUID(messageID);
375 final Content thread = getThreadFromMessage(message);
376 isModerator(getHierarchyManager(), message);
377
378
379 checkAndUpdateLastMessage(thread, messageID);
380
381 if (messageID.equals(thread.getNodeData(FIRST_MESSAGE_PROPERTY).getString())) {
382 SiblingsHelper siblings = SiblingsHelper.of(message);
383 if (siblings.isLast()) {
384 throw new UnsupportedOperationException("Thread needs at least one message. Create another message first or delete whole thread.");
385 } else {
386 Content nextMessage = siblings.next();
387 thread.deleteNodeData(FIRST_MESSAGE_PROPERTY);
388 thread.createNodeData(FIRST_MESSAGE_PROPERTY, nextMessage);
389 }
390 }
391 deleteByUuid(messageID);
392 }
393
394 @Override
395 public void lockForum(String forumID) throws RepositoryException {
396 changeLock(forumID, true);
397 }
398
399 @Override
400 public void unlockForum(String forumID) throws RepositoryException {
401 changeLock(forumID, false);
402 }
403
404 @Override
405 public void lockThread(String threadID) throws RepositoryException {
406 changeLock(threadID, true);
407 }
408
409 @Override
410 public void unlockThread(String threadID) throws RepositoryException {
411 changeLock(threadID, false);
412 }
413
414 @Override
415 public void validate(String messageID) throws RepositoryException {
416 changeValidationState(messageID, true);
417 }
418
419 @Override
420 public void invalidate(String messageID) throws RepositoryException {
421 changeValidationState(messageID, false);
422 }
423
424 protected void deleteByUuid(String uuid) throws RepositoryException {
425 final Content node = getHierarchyManager().getContentByUUID(uuid);
426 isModerator(getHierarchyManager(),node);
427 final Content parent = node.getParent();
428 node.delete();
429 parent.save();
430 }
431
432 protected void changeLock(String uuid, boolean value) throws RepositoryException {
433 final HierarchyManager hm = getHierarchyManager();
434 final Content node = hm.getContentByUUID(uuid);
435 isModerator(hm, node);
436 NodeDataUtil.getOrCreateAndSet(node, LOCKED_PROPERTY, value);
437 node.save();
438 }
439
440 protected void changeValidationState(String messageID, boolean value) throws RepositoryException {
441 final HierarchyManager hm = getHierarchyManager();
442 final Content node = hm.getContentByUUID(messageID);
443 isModerator(hm, node);
444 NodeDataUtil.getOrCreateAndSet(node, VALIDATED_PROPERTY, value);
445 node.save();
446 }
447
448
449
450
451 protected Content createMessageInThread(Content forum, Content thread, Content inReplyTo, String messageName, String messageTitle, String messageText, String author, boolean isAnonymous) throws RepositoryException {
452 final boolean allowsNestingMessages = allowsNestingMessages(forum);
453 final Content parent;
454 if (inReplyTo != null && allowsNestingMessages) {
455 parent = inReplyTo;
456 } else {
457 parent = thread;
458 }
459
460 final Content newMessage = createMessage(parent, messageName, messageTitle, messageText, author, isAnonymous);
461 thread.createNodeData(LAST_MESSAGE_PROPERTY, newMessage);
462 forum.createNodeData(LAST_MESSAGE_PROPERTY, newMessage);
463
464 if (inReplyTo != null && !allowsNestingMessages) {
465 newMessage.createNodeData("inReplyTo", inReplyTo);
466 }
467 return newMessage;
468 }
469
470
471
472
473
474
475 @Deprecated
476 protected Content createMessageInThread(Content forum, Content thread, String name, String title, String messageText, String author, boolean isAnonymous) throws RepositoryException {
477 return createMessageInThread(forum, thread, null, name, title, messageText, author, isAnonymous);
478 }
479
480
481
482
483 protected Content createMessage(Content parent, String name, String title, String messageText, String author, boolean isAnonymous) throws RepositoryException {
484 final Content message = parent.createContent(makeNameUnique(parent, name), MESSAGE_NODETYPE);
485 if (StringUtils.isNotEmpty(title)) {
486 message.createNodeData(TITLE_PROPERTY, title);
487 }
488 message.createNodeData(CONTENT_PROPERTY, messageText);
489 if (!isAnonymous) {
490 message.createNodeData(AUTHORID_PROPERTY, author);
491 } else {
492 message.createNodeData(AUTHORID_PROPERTY, UserManager.ANONYMOUS_USER);
493 message.createNodeData("anonymousUsername", author);
494 }
495 message.createNodeData(CREATION_DATE_PROPERTY, Calendar.getInstance());
496 return message;
497 }
498
499
500
501
502
503 protected String generateMessageName() {
504 final String messageNamePattern = config.getMessageNamePattern();
505 return DateFormatUtils.format(Calendar.getInstance().getTime(), messageNamePattern);
506 }
507
508 protected String makeNameUnique(Content parent, String name) {
509 return Path.getUniqueLabel(parent.getJCRNode(), cleanup(name));
510 }
511
512
513
514
515 protected String cleanup(String s) {
516 if (StringUtils.isEmpty(s)) {
517 return "_";
518 }
519
520 return Path.getValidatedLabel(s).toLowerCase();
521 }
522
523
524
525
526
527
528 private static class ThreadsFilter implements Content.ContentFilter {
529 private final boolean showUnvalidatedMessages;
530
531 public ThreadsFilter(boolean showUnvalidatedMessages) {
532 this.showUnvalidatedMessages = showUnvalidatedMessages;
533 }
534
535 @Override
536 public boolean accept(Content content) {
537 if (!content.isNodeType(THREAD_NODETYPE)) {
538 return false;
539 }
540
541 final NodeData firstMsgProp = content.getNodeData(DefaultForumManager.FIRST_MESSAGE_PROPERTY);
542 if (!firstMsgProp.isExist()) {
543 return false;
544 }
545 try {
546 final Content firstMessage = firstMsgProp.getReferencedContent();
547 final NodeData validatedProp = firstMessage.getNodeData(VALIDATED_PROPERTY);
548 return (!validatedProp.isExist() && showUnvalidatedMessages) || (validatedProp.isExist() && validatedProp.getBoolean());
549 } catch (RepositoryException e) {
550 log.error("Couldn't check if thread[" + content + "] could to be shown: " + e.getMessage(), e);
551 return false;
552 }
553 }
554 }
555
556 private void checkAndUpdateLastMessage(final Content forumOrThread, final String threadOrMessageID) throws RepositoryException {
557 Content lastMessage;
558
559 final Content threadOrMessage = forumOrThread.getHierarchyManager().getContentByUUID(threadOrMessageID);
560 if (threadOrMessage.hasNodeData(LAST_MESSAGE_PROPERTY)) {
561 lastMessage = threadOrMessage.getNodeData(LAST_MESSAGE_PROPERTY).getReferencedContent();
562 } else {
563 lastMessage = threadOrMessage;
564 }
565 Content threadOrForumLM = forumOrThread.getNodeData(LAST_MESSAGE_PROPERTY).getReferencedContent();
566 String forumOrThreadLastMessageID = threadOrForumLM.getUUID();
567 if (!forumOrThreadLastMessageID.equals(lastMessage.getUUID())) {
568
569 return;
570 }
571 lastMessage = null;
572
573 for (Content aThreadOrMessage : forumOrThread.getChildren(threadOrMessage.getItemType())) {
574 if (threadOrMessageID.equals(aThreadOrMessage.getUUID())) {
575
576 continue;
577 }
578 Content aMessage;
579 if (aThreadOrMessage.hasNodeData(LAST_MESSAGE_PROPERTY)) {
580 aMessage = aThreadOrMessage.getNodeData(LAST_MESSAGE_PROPERTY).getReferencedContent();
581 } else {
582 aMessage = aThreadOrMessage;
583 }
584 lastMessage = latestMessage(lastMessage, aMessage);
585 }
586 updateLastMessage(forumOrThread, lastMessage);
587
588
589
590 Content forum = forumOrThread.getParent();
591 if (forum.hasNodeData(LAST_MESSAGE_PROPERTY)) {
592 lastMessage = null;
593
594 if (forumOrThreadLastMessageID.equals(forum.getNodeData(LAST_MESSAGE_PROPERTY).getString())) {
595
596 for (Content thread : forum.getChildren(forumOrThread.getItemType())) {
597 Content aMessage = thread.getNodeData(LAST_MESSAGE_PROPERTY).getReferencedContent();
598 lastMessage = latestMessage(lastMessage, aMessage);
599 }
600 updateLastMessage(forum, lastMessage);
601
602 forum.save();
603 }
604 }
605 }
606
607 private void updateLastMessage(Content forumOrThread, Content lastMessage) throws RepositoryException {
608 forumOrThread.deleteNodeData(LAST_MESSAGE_PROPERTY);
609 if (lastMessage != null) {
610 forumOrThread.createNodeData(LAST_MESSAGE_PROPERTY, lastMessage);
611 }
612 }
613
614
615
616
617
618
619 protected Content latestMessage(Content firstMessage, Content secondMessage) {
620 if (firstMessage == null || secondMessage.getNodeData(CREATION_DATE_PROPERTY).getDate().after(firstMessage.getNodeData(CREATION_DATE_PROPERTY).getDate())) {
621 firstMessage = secondMessage;
622 }
623 return firstMessage;
624 }
625
626
627
628
629 protected boolean allowsNestingMessages(Content forum) {
630 return forum.getNodeData(ALLOWS_NESTING_MESSAGES).getBoolean();
631 }
632
633 protected HierarchyManager getHierarchyManager() {
634 return HierarchyManagerUtil.getHierarchyManager(MgnlContext.getInstance(), config.getRepository());
635 }
636
637 }