/** * Copyright © 2014 Instituto Superior Técnico * * This file is part of FenixEdu CMS. * * FenixEdu CMS is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * FenixEdu CMS is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with FenixEdu CMS. If not, see <http://www.gnu.org/licenses/>. */ package org.fenixedu.cms.domain; import static org.fenixedu.commons.i18n.LocalizedString.fromJson; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import org.fenixedu.bennu.core.domain.User; import org.fenixedu.bennu.core.groups.AnyoneGroup; import org.fenixedu.bennu.core.groups.Group; import org.fenixedu.bennu.core.security.Authenticate; import org.fenixedu.bennu.core.util.CoreConfiguration; import org.fenixedu.bennu.signals.DomainObjectEvent; import org.fenixedu.bennu.signals.Signal; import org.fenixedu.cms.domain.PermissionsArray.Permission; import org.fenixedu.cms.domain.component.Component; import org.fenixedu.cms.domain.component.StaticPost; import org.fenixedu.cms.domain.wraps.UserWrap; import org.fenixedu.cms.domain.wraps.Wrap; import org.fenixedu.cms.domain.wraps.Wrappable; import org.fenixedu.cms.exceptions.CmsDomainException; import org.fenixedu.commons.StringNormalizer; import org.fenixedu.commons.i18n.LocalizedString; import org.joda.time.DateTime; import com.google.common.collect.ImmutableList; import pt.ist.fenixframework.Atomic; /** * A post models a given content to be presented to the user. */ public class Post extends Post_Base implements Wrappable, Sluggable, Cloneable { public static final String SIGNAL_CREATED = "fenixedu.cms.post.created"; public static final String SIGNAL_DELETED = "fenixedu.cms.post.deleted"; public static final String SIGNAL_EDITED = "fenixedu.cms.post.edited"; public static final Comparator<Post> CREATION_DATE_COMPARATOR = Comparator.comparing(Post::getCreationDate).reversed(); /** * The logged {@link User} creates a new Post. * @param site site */ public Post(Site site) { super(); if (Authenticate.getUser() == null) { throw CmsDomainException.forbiden(); } setCreatedBy(Authenticate.getUser()); DateTime now = new DateTime(); setCreationDate(now); setModificationDate(now); setActive(false); setCanViewGroup(AnyoneGroup.get()); setSite(site); Signal.emit(Post.SIGNAL_CREATED, new DomainObjectEvent<Post>(this)); } @Override public Site getSite() { return super.getSite(); } /** * saves the name of the post and creates a new slug for the post. */ @Override public void setName(LocalizedString name) { LocalizedString prevName = getName(); super.setName(name); setModificationDate(new DateTime()); if (prevName == null) { String slug = StringNormalizer.slugify(name.getContent()); setSlug(slug); } } @Override public void setSlug(String slug) { super.setSlug(SlugUtils.makeSlug(this, slug)); } /** * A slug is valid if there are no other page on that site that have the * same slug. * * @param slug slug * @return true if it is a valid slug. */ @Override public boolean isValidSlug(String slug) { Post p = getSite().postForSlug(slug); return p == null || p == this; } /** * @return the URL link to the slug's page. */ public String getAddress() { if (isStaticPost()) { return getStaticPage().get().getAddress(); } else { return Optional.ofNullable(getSite().getViewPostPage()).map(page -> page.getAddress() + "/" + getSlug()).orElse(null); } } @Atomic public void delete() { Signal.emit(SIGNAL_DELETED, this.getOid()); setCreatedBy(null); setSite(null); setViewGroup(null); deleteFiles(); setLatestRevision(null); getComponentSet().stream().forEach(Component::delete); getCategoriesSet().stream().forEach(category -> category.removePosts(this)); getRevisionsSet().stream().forEach(PostContentRevision::delete); deleteDomainObject(); } public boolean hasPublicationPeriod() { return getPublicationBegin() != null && getPublicationEnd() != null; } public boolean isInPublicationPeriod() { boolean inBegin = getPublicationBegin() == null || getPublicationBegin().isBeforeNow(); boolean inEnd = getPublicationEnd() == null || getPublicationEnd().isAfterNow(); return inBegin && inEnd; } public boolean isVisible() { return getActive() && (!hasPublicationPeriod() || isInPublicationPeriod()); } /** * returns the group of people who can view this site. * * @return group the access group for this site */ public Group getCanViewGroup() { return getViewGroup().toGroup(); } /** * sets the access group for this site * * @param group the group of people who can view this site */ @Atomic public void setCanViewGroup(Group group) { super.setViewGroup(group.toPersistentGroup()); Set<User> groupMembers = group.getMembers().stream().collect(Collectors.toSet()); getEmbeddedFilesSorted().forEach(postFile -> postFile.getFiles().setAccessGroup(group)); //if the new group is more restricted then the attachment group is updated getAttachmentFilesSorted().map(PostFile::getFiles) .filter(file -> !file.getAccessGroup().getMembers().stream().allMatch(groupMembers::contains)) .forEach(file -> file.setAccessGroup(group)); } public static Post create(Site site, Page page, LocalizedString name, LocalizedString body, LocalizedString excerpt, Category category, boolean active, User creator) { Post post = new Post(site); post.setName(name); post.setBodyAndExcerpt(body, excerpt); post.setCreationDate(new DateTime()); if (creator == null) { post.setCreatedBy(page.getCreatedBy()); } else { post.setCreatedBy(creator); } post.addCategories(category); post.setActive(active); return post; } public String getEditUrl() { return getStaticPage().map(Page::getEditUrl).orElse(CoreConfiguration.getConfiguration().applicationUrl() + "/cms/posts/" + getSite().getSlug() + "/" + getSlug() + "/edit"); } public void fixOrder(List<PostFile> sortedItems) { for (int i = 0; i < sortedItems.size(); ++i) { sortedItems.get(i).setIndex(i); } } @Override public Post clone(CloneCache cloneCache) { return cloneCache.getOrClone(this, obj -> { Collection<Category> categories = new HashSet<>(getCategoriesSet()); List<PostFile> files = new ArrayList<>(getFilesSorted()); Post clone = new Post(getSite()); cloneCache.setClone(Post.this, clone); clone.setBodyAndExcerpt(getBody() != null ? fromJson(getBody().json()) : null, getExcerpt() != null ? fromJson(getExcerpt().json()) : null); clone.setName(getName() != null ? fromJson(getName().json()) : null); clone.setBodyAndExcerpt(getBody() != null ? fromJson(getBody().json()) : null, getExcerpt() != null ? fromJson(getExcerpt().json()) : null); clone.setLocation(getLocation() != null ? fromJson(getLocation().json()) : null); clone.setMetadata(getMetadata() != null ? getMetadata().clone() : null); clone.setCanViewGroup(getCanViewGroup()); clone.setPublicationBegin(getPublicationBegin()); clone.setPublicationEnd(getPublicationEnd()); clone.setCreatedBy(getCreatedBy()); clone.setCreationDate(getCreationDate()); clone.setViewGroup(getViewGroup()); categories.stream().map(category -> category.clone(cloneCache)).forEach(clone::addCategories); files.stream().map(file -> file.clone(cloneCache)).forEach(clone::addFiles); return clone; }); } public Stream<PostFile> getAttachmentFilesSorted() { return getFilesSet().stream().filter(pf -> !pf.getIsEmbedded()).sorted(); } public Stream<PostFile> getEmbeddedFilesSorted() { return getFilesSet().stream().filter(pf -> pf.getIsEmbedded()).sorted(); } private void deleteFiles() { ImmutableList.copyOf(getFilesSet()).forEach(PostFile::delete); } public List<PostFile> getFilesSorted() { return getFilesSet().stream().sorted().collect(Collectors.toList()); } @Override public void addFiles(PostFile postFile) { List<PostFile> list = getFilesSorted(); list.add(postFile.getIndex(), postFile); fixOrder(list); super.addFiles(postFile); } @Override public void removeFiles(PostFile files) { List<PostFile> list = getFilesSorted(); list.remove(files); fixOrder(list); super.removeFiles(files); } @Override public void addCategories(Category categories) { DateTime now = new DateTime(); super.addCategories(categories); categories.getComponentsSet().forEach(component -> { if (component.getPage() != null) { component.getPage().setModificationDate(now); } }); setModificationDate(now); } @Override public void setActive(boolean active) { super.setActive(active); setModificationDate(new DateTime()); } public void setBody(LocalizedString body) { setBodyAndExcerpt(body, null); } public void setBodyAndExcerpt(LocalizedString body, LocalizedString excerpt) { PostContentRevision pcr = new PostContentRevision(); pcr.setBody(body); pcr.setExcerpt(excerpt); pcr.setCreatedBy(Authenticate.getUser()); pcr.setPrevious(getLatestRevision()); pcr.setPost(this); pcr.setRevisionDate(new DateTime()); setLatestRevision(pcr); setModificationDate(new DateTime()); } public LocalizedString getExcerpt() { PostContentRevision pcr = getLatestRevision(); if (pcr == null) { return new LocalizedString(); } else { return pcr.getExcerpt(); } } public LocalizedString getBody() { PostContentRevision pcr = getLatestRevision(); if (pcr == null) { return new LocalizedString(); } else { return pcr.getBody(); } } public LocalizedString getPresentationBody() { return getBody(); } @Override public void setLocation(LocalizedString location) { super.setLocation(location); setModificationDate(new DateTime()); } @Override public void setPublicationEnd(DateTime publicationEnd) { super.setPublicationEnd(publicationEnd); setModificationDate(new DateTime()); } @Override public void setPublicationBegin(DateTime publicationBegin) { super.setPublicationBegin(publicationBegin); setModificationDate(new DateTime()); } public String getCategories() { return getCategoriesSet().stream().map(x -> x.getName().getContent()).reduce((x, y) -> x + "," + y).orElse(""); } public String getCategoriesString() { return getCategoriesSet().stream().map(x -> x.getName().getContent()).reduce((x, y) -> x + "," + y).orElse(""); } public Optional<Page> getStaticPage() { return getComponentSet().stream().filter(component -> StaticPost.class.isInstance(component)) .map(component -> component.getPage()).findFirst(); } public boolean isStaticPost() { return getComponentSet().stream().filter(component -> StaticPost.class.isInstance(component)).findAny().isPresent(); } public class PostWrap extends Wrap { public LocalizedString getName() { return Post.this.getName(); } public String getSlug() { return Post.this.getSlug(); } public boolean isVisible() { return getCanViewGroup().isMember(Authenticate.getUser()); } public String getVisibilityGroup() { return getCanViewGroup().toPersistentGroup().getPresentationName(); } public LocalizedString getExcerpt() { return Post.this.getExcerpt(); } public LocalizedString getBody() { return Post.this.getBody(); } public Wrap getCreatedBy() { return new UserWrap(Post.this.getCreatedBy()); } public DateTime getCreationDate() { return Post.this.getCreationDate(); } public DateTime getPublicationBegin() { return Post.this.getPublicationBegin(); } public DateTime getPublicationEnd() { return Post.this.getPublicationEnd(); } public DateTime getModificationDate() { return Post.this.getModificationDate(); } public Wrap getSite() { return Post.this.getSite().makeWrap(); } public String getAddress() { return Post.this.getAddress(); } public String getEditAddress() { return CoreConfiguration.getConfiguration().applicationUrl() + "/cms/posts/" + Post.this.getSite().getSlug() + "/" + Post.this.getSlug() + "/edit"; } public List<Wrap> getCategories() { return getCategoriesSet().stream().map(Wrap::make).collect(Collectors.toList()); } public List<Wrap> getAttachments() { return getAttachmentFilesSorted().map(PostFile::makeWrap).collect(Collectors.toList()); } } @Override public Wrap makeWrap() { return new PostWrap(); } public boolean isAccessible() { return getCanViewGroup().isMember(Authenticate.getUser()); } public boolean isModified() { return !getCreationDate().toLocalDate().equals(getModificationDate().toLocalDate()); } public static LocalizedString sanitize(LocalizedString original) { return Sanitization.sanitize(original); } private static final Object NONE = new Object(); public Iterator<PostContentRevision> getRevisionsIterator() { return new Iterator<PostContentRevision>() { PostContentRevision t = (PostContentRevision) NONE; @Override public boolean hasNext() { return t.getNext() != null; } @Override public PostContentRevision next() { return t = t == NONE ? getLatestRevision() : t.getNext(); } }; } public Iterable<PostContentRevision> getRevisions() { return () -> getRevisionsIterator(); } public boolean canDelete() { Set<Permission> required = new HashSet<>(); required.add(Permission.DELETE_POSTS); if (!Authenticate.getUser().equals(getCreatedBy())) { required.add(Permission.DELETE_OTHERS_POSTS); } if (isVisible()) { required.add(Permission.DELETE_POSTS_PUBLISHED); } return PermissionEvaluation.canDoThis(getSite(), required.toArray(new Permission[] {})); } public boolean canEdit() { ArrayList<Permission> permissions = new ArrayList<>(); permissions.add(Permission.EDIT_POSTS); if (!Authenticate.getUser().equals(getCreatedBy())) { permissions.add(Permission.EDIT_OTHERS_POSTS); } if (isVisible()) { permissions.add(Permission.EDIT_POSTS_PUBLISHED); } return PermissionEvaluation.canDoThis(getSite(), permissions.toArray(new Permission[] {})); } }