/**
* ==========================================================================================
* = DIGITAL FACTORY v7.0 - Community Distribution =
* ==========================================================================================
*
* Rooted in Open Source CMS, Jahia's Digital Industrialization paradigm is about
* streamlining Enterprise digital projects across channels to truly control
* time-to-market and TCO, project after project.
* Putting an end to "the Tunnel effect", the Jahia Studio enables IT and
* marketing teams to collaboratively and iteratively build cutting-edge
* online business solutions.
* These, in turn, are securely and easily deployed as modules and apps,
* reusable across any digital projects, thanks to the Jahia Private App Store Software.
* Each solution provided by Jahia stems from this overarching vision:
* Digital Factory, Workspace Factory, Portal Factory and eCommerce Factory.
* Founded in 2002 and headquartered in Geneva, Switzerland,
* Jahia Solutions Group has its North American headquarters in Washington DC,
* with offices in Chicago, Toronto and throughout Europe.
* Jahia counts hundreds of global brands and governmental organizations
* among its loyal customers, in more than 20 countries across the globe.
*
* For more information, please visit http://www.jahia.com
*
* JAHIA'S DUAL LICENSING - IMPORTANT INFORMATION
* ============================================
*
* Copyright (C) 2002-2014 Jahia Solutions Group SA. All rights reserved.
*
* THIS FILE IS AVAILABLE UNDER TWO DIFFERENT LICENSES:
* 1/GPL OR 2/JSEL
*
* 1/ GPL
* ==========================================================
*
* IF YOU DECIDE TO CHOSE THE GPL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
*
* "This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* As a special exception to the terms and conditions of version 2.0 of
* the GPL (or any later version), you may redistribute this Program in connection
* with Free/Libre and Open Source Software ("FLOSS") applications as described
* in Jahia's FLOSS exception. You should have received a copy of the text
* describing the FLOSS exception, and it is also available here:
* http://www.jahia.com/license"
*
* 2/ JSEL - Commercial and Supported Versions of the program
* ==========================================================
*
* IF YOU DECIDE TO CHOOSE THE JSEL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
*
* Alternatively, commercial and supported versions of the program - also known as
* Enterprise Distributions - must be used in accordance with the terms and conditions
* contained in a separate written agreement between you and Jahia Solutions Group SA.
*
* If you are unsure which license is appropriate for your use,
* please contact the sales department at sales@jahia.com.
*/
package org.jahia.modules.spamfiltering.rules;
import org.apache.commons.lang.StringUtils;
import org.apache.velocity.tools.generic.DateTool;
import org.apache.velocity.tools.generic.EscapeTool;
import org.drools.core.spi.KnowledgeHelper;
import org.jahia.bin.Jahia;
import org.jahia.modules.spamfiltering.HostStats;
import org.jahia.modules.spamfiltering.SpamFilteringService;
import org.jahia.modules.spamfiltering.filters.SpamRenderFilter;
import org.jahia.modules.spamfiltering.listeners.SpamServletRequestListener;
import org.jahia.services.content.JCRNodeWrapper;
import org.jahia.services.content.JCRPropertyWrapper;
import org.jahia.services.content.nodetypes.ExtendedPropertyDefinition;
import org.jahia.services.content.rules.AddedNodeFact;
import org.jahia.services.content.rules.User;
import org.jahia.services.mail.MailService;
import org.jahia.services.usermanager.JahiaUser;
import org.jahia.settings.SettingsBean;
import org.jahia.utils.LanguageCodeConverters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.jcr.PropertyIterator;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.*;
/**
* Service class for checking content and applying spam filtering.
*
* @author Sergiy Shyrkov
*/
public class SpamFilteringRuleService {
private static final String SPAM_DETECTED_MIXIN = "jmix:spamFilteringSpamDetected";
private static final String SPAM_SESSIONS_PROPERTY_NAME = "org.jahia.modules.spamfiltering.spamSessions";
private static Logger logger = LoggerFactory.getLogger(SpamFilteringRuleService.class);
private static SpamFilteringRuleService instance = null;
private SpamFilteringService spamFilteringService;
private boolean sendSpamNotificationEmails = true;
private MailService mailService;
private SpamRenderFilter spamRenderFilter;
private String accountLockedTemplatePath;
private String emailFrom;
private String emailTo;
private String spamFilterHostUrlPart;
private long defaultBlacklistingTimeout = 24 * 60 * 60 * 1000; // 24 hours
private Map<String, HostStats> blacklistedHosts = new LinkedHashMap<String, HostStats>();
private String hostBlacklistedTemplatePath;
private boolean allowReadingWhenBlacklisted = true;
private String whitelistedHosts = "127.0.0.1,localhost";
private List<String> markSingleParentsOfTypes = new ArrayList<>();
public SpamFilteringRuleService() {
instance = this;
}
public static SpamFilteringRuleService getInstance() {
return instance;
}
public void setSpamFilteringService(SpamFilteringService spamFilteringService) {
this.spamFilteringService = spamFilteringService;
}
public void setMailService(MailService mailService) {
this.mailService = mailService;
}
public void setSendSpamNotificationEmails(boolean sendSpamNotificationEmails) {
this.sendSpamNotificationEmails = sendSpamNotificationEmails;
}
public void setAccountLockedTemplatePath(String accountLockedTemplatePath) {
this.accountLockedTemplatePath = accountLockedTemplatePath;
}
public void setEmailFrom(String emailFrom) {
this.emailFrom = emailFrom;
}
public void setEmailTo(String emailTo) {
this.emailTo = emailTo;
}
public void setSpamFilterHostUrlPart(String spamFilterHostUrlPart) {
this.spamFilterHostUrlPart = spamFilterHostUrlPart;
}
public void setSpamRenderFilter(SpamRenderFilter spamRenderFilter) {
this.spamRenderFilter = spamRenderFilter;
}
public Map<String, HostStats> getBlacklistedHosts() {
return blacklistedHosts;
}
public void setBlacklistedHosts(Map<String, HostStats> blacklistedHosts) {
this.blacklistedHosts = blacklistedHosts;
}
public void setDefaultBlacklistingTimeout(long defaultBlacklistingTimeout) {
this.defaultBlacklistingTimeout = defaultBlacklistingTimeout;
}
public void setHostBlacklistedTemplatePath(String hostBlacklistedTemplatePath) {
this.hostBlacklistedTemplatePath = hostBlacklistedTemplatePath;
}
public boolean isAllowReadingWhenBlacklisted() {
return allowReadingWhenBlacklisted;
}
public void setAllowReadingWhenBlacklisted(boolean allowReadingWhenBlacklisted) {
this.allowReadingWhenBlacklisted = allowReadingWhenBlacklisted;
}
public String getWhitelistedHosts() {
return whitelistedHosts;
}
public void setWhitelistedHosts(String whitelistedHosts) {
this.whitelistedHosts = whitelistedHosts;
}
public void setMarkSingleParentsOfTypes(List<String> markSingleParentsOfTypes) {
this.markSingleParentsOfTypes = markSingleParentsOfTypes;
}
/**
* Verifies the content of the node with anti-spam service and applies spam filtering (by assigning a special mixin).
*
* @param nodeFact
* the node which content should be checked
* @param maxSpamCount the number of maximum spams tolerated before the user is locked and his session is killed.
* @param drools
* the rule engine helper class
* @throws RepositoryException
* in case of an error
*/
public void checkForSpam(AddedNodeFact nodeFact, Integer maxSpamCount, KnowledgeHelper drools)
throws RepositoryException {
if (logger.isDebugEnabled()) {
logger.debug("Checking content of the node {} for spam", nodeFact.getPath());
}
try {
User user = (User) drools.getWorkingMemory().getGlobal("user");
HttpServletRequest httpServletRequest = spamRenderFilter.getHttpServletRequest();
if (httpServletRequest == null) {
// we didn't manage to get the request from our own filter, try to access it through Spring MVC's
// framework
try {
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
if (requestAttributes != null && requestAttributes instanceof ServletRequestAttributes) {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
httpServletRequest = servletRequestAttributes.getRequest();
}
} catch (IllegalStateException ise) {
logger.warn("Couldn't retrieve request from Spring MVC controller : " + ise.getMessage());
}
if (httpServletRequest == null ) {
// we could reach this stage if the incoming request is coming from a REST API call or another
// direct call to OSGi's HttpService.
if (SpamServletRequestListener.getServletRequestEvent() != null) {
ServletRequest servletRequest = SpamServletRequestListener.getServletRequestEvent().getServletRequest();
if (servletRequest instanceof HttpServletRequest) {
httpServletRequest = (HttpServletRequest) servletRequest;
}
}
}
}
boolean isSpam = false;
JCRNodeWrapper node = nodeFact.getNode();
String text = getTextContent(node);
if (StringUtils.isNotEmpty(text)) {
isSpam = spamFilteringService.isSpam(text, node, httpServletRequest);
}
if (isSpam) {
if (!node.isNodeType(SPAM_DETECTED_MIXIN)) {
// is detected as spam -> add mixin
node.getSession().checkout(node);
node.addMixin(SPAM_DETECTED_MIXIN);
JCRNodeWrapper parentNode = node.getParent();
if (parentNode.getNodes().getSize() == 1) {
for (String singleParentToMarkNodeType : markSingleParentsOfTypes) {
if (parentNode.isNodeType(singleParentToMarkNodeType)) {
logger.info("Found single parent node with node type " + singleParentToMarkNodeType + ", adding mixin " + SPAM_DETECTED_MIXIN + " to it too.");
parentNode.getSession().checkout(parentNode);
parentNode.addMixin(SPAM_DETECTED_MIXIN);
}
}
}
}
if (maxSpamCount != null && httpServletRequest != null) {
HttpSession httpSession = httpServletRequest.getSession(false);
JahiaUser jahiaUser = user.getJahiaUser();
if (httpSession != null && !"guest".equals(jahiaUser.getName()) && !jahiaUser.isRoot()) {
String spamSessionsValue = jahiaUser.getProperty(SPAM_SESSIONS_PROPERTY_NAME);
List<String> spamSessions = new ArrayList<String>();
if (spamSessionsValue != null) {
spamSessions.addAll(Arrays.asList(spamSessionsValue.split(",")));
}
spamSessions.add(httpSession.getId());
if (spamSessions.size() >= maxSpamCount) {
logger.info("Maximum number of spam count reached (" + maxSpamCount + "), locking user account and killing session...");
logger.info("Marking session " + httpSession.getId() + " as invalid and will be killed on next access.");
spamRenderFilter.addSessionToKill(httpSession.getId());
// add code to lock account
logger.info("Locking account " + jahiaUser + "...");
jahiaUser.setProperty("j:accountLocked", "true");
if (sendSpamNotificationEmails) {
logger.info("Sending account lock notification to administrator...");
sendAccountLockNotification(node, jahiaUser, httpServletRequest);
}
// we clear the session list to avoid it growing to big
spamSessions.clear();
} else {
logger.info("User " + jahiaUser + " has sent " + spamSessions.size() + " spam so far.");
}
if (spamSessions.size() > 0) {
jahiaUser.setProperty(SPAM_SESSIONS_PROPERTY_NAME, StringUtils.join(spamSessions, ","));
} else {
jahiaUser.removeProperty(SPAM_SESSIONS_PROPERTY_NAME);
}
} else {
// let's use IP-based blocking if we cannot use authenticated sessions
String remoteHost = httpServletRequest.getRemoteHost();
if (remoteHost == null) {
remoteHost = httpServletRequest.getRemoteAddr();
}
if (remoteHost != null) {
if (whitelistedHosts.contains(remoteHost)) {
logger.debug("Host {} is whitelisted, bypassing blacklisting mechanism.", remoteHost);
} else {
HostStats hostStats = blacklistedHosts.get(remoteHost);
if (hostStats == null) {
hostStats = new HostStats(remoteHost, new Date(), 0, false, 0);
}
int hostSpamCount = hostStats.getSpamCount();
hostSpamCount++;
hostStats.setLastPost(new Date());
hostStats.setSpamCount(hostSpamCount);
if (hostSpamCount >= maxSpamCount) {
// we've reached the maximum spam count trigger, let's black list the host.
logger.info("Maximum number of spam count reached (" + maxSpamCount + "), temporarily blacklisting host=" + hostStats + " and killing session...");
logger.info("Marking session " + httpSession.getId() + " as invalid and will be killed on next access.");
hostStats.setBlacklisted(true);
hostStats.setBlacklistingTimeout(System.currentTimeMillis() + defaultBlacklistingTimeout);
hostStats.setBlacklistingCount(hostStats.getBlacklistingCount() + 1);
if (sendSpamNotificationEmails) {
logger.info("Sending host blacklisting notification to administrator...");
sendHostBlacklistingNotification(node, jahiaUser, httpServletRequest, hostStats);
}
} else {
logger.info("Host {} has sent {} spam messages so far.", hostStats, hostSpamCount);
}
blacklistedHosts.put(remoteHost, hostStats);
}
} else {
logger.warn("Remote host couldn't be resolved, maybe there is a configuration issue ?");
}
}
}
} else if (node.isNodeType(SPAM_DETECTED_MIXIN)) {
// no longer spam -> remove mixin
node.getSession().checkout(node);
node.removeMixin(SPAM_DETECTED_MIXIN);
JCRNodeWrapper parentNode = node.getParent();
if (parentNode.getNodes().getSize() == 1) {
for (String singleParentToMarkNodeType : markSingleParentsOfTypes) {
if (parentNode.isNodeType(singleParentToMarkNodeType) && parentNode.isNodeType(SPAM_DETECTED_MIXIN)) {
logger.info("Found single parent node with node type " + singleParentToMarkNodeType + ", removing mixin " + SPAM_DETECTED_MIXIN + " to it too.");
parentNode.getSession().checkout(parentNode);
parentNode.removeMixin(SPAM_DETECTED_MIXIN);
}
}
}
}
logger.info("Content of the node {} is{} detected as spam", node.getPath(),
!isSpam ? " not" : "");
} catch (Exception e) {
logger.warn("Unable to check the content of the node " + nodeFact.getPath()
+ " for spam. Cause: " + e.getMessage(), e);
}
}
private void sendAccountLockNotification(JCRNodeWrapper node, JahiaUser jahiaUser, HttpServletRequest httpServletRequest) throws RepositoryException {
// Prepare mail to be sent :
String administratorEmail = emailTo == null ? mailService.getSettings().getTo() : emailTo;
Locale defaultLocale = null;
if (node.getExistingLocales() != null &&
node.getExistingLocales().size() > 0) {
defaultLocale = node.getExistingLocales().get(0);
}
if (defaultLocale == null) {
defaultLocale = LanguageCodeConverters.languageCodeToLocale(SettingsBean.getInstance().getDefaultLanguageCode());
}
Map<String, Object> bindings = new HashMap<String, Object>();
bindings.put("spamNode", node.getParent());
bindings.put("spamNewNode", node);
bindings.put("ParentSpamNode", node.getParent().getParent());
bindings.put("submitter", jahiaUser);
if (httpServletRequest != null) {
bindings.put("httpServletRequest", httpServletRequest);
}
bindings.put("date", new DateTool());
bindings.put("esc", new EscapeTool());
bindings.put("submissionDate", Calendar.getInstance());
bindings.put("spamURL", spamFilterHostUrlPart + Jahia.getContextPath() + node.getUrl());
try {
bindings.put("locale", defaultLocale);
mailService.sendMessageWithTemplate(accountLockedTemplatePath, bindings, administratorEmail, emailFrom, "", "", defaultLocale, "Jahia Spam Filtering");
logger.info("Account "+jahiaUser+" locked notification sent by e-mail to " + administratorEmail + " using locale " + defaultLocale);
} catch (Exception e) {
logger.error("Couldn't sent spam account lock email notification: ", e);
}
}
private void sendHostBlacklistingNotification(JCRNodeWrapper node, JahiaUser jahiaUser, HttpServletRequest httpServletRequest, HostStats hostStats) throws RepositoryException {
// Prepare mail to be sent :
String administratorEmail = emailTo == null ? mailService.getSettings().getTo() : emailTo;
Locale defaultLocale = null;
if (node.getExistingLocales() != null &&
node.getExistingLocales().size() > 0) {
defaultLocale = node.getExistingLocales().get(0);
}
if (defaultLocale == null) {
defaultLocale = LanguageCodeConverters.languageCodeToLocale(SettingsBean.getInstance().getDefaultLanguageCode());
}
Map<String, Object> bindings = new HashMap<String, Object>();
bindings.put("spamNode", node.getParent());
bindings.put("spamNewNode", node);
bindings.put("ParentSpamNode", node.getParent().getParent());
bindings.put("submitter", jahiaUser);
if (httpServletRequest != null) {
bindings.put("httpServletRequest", httpServletRequest);
}
bindings.put("date", new DateTool());
bindings.put("esc", new EscapeTool());
bindings.put("submissionDate", Calendar.getInstance());
bindings.put("spamURL", spamFilterHostUrlPart + Jahia.getContextPath() + node.getUrl());
bindings.put("hostStats", hostStats);
try {
bindings.put("locale", defaultLocale);
mailService.sendMessageWithTemplate(hostBlacklistedTemplatePath, bindings, administratorEmail, emailFrom, "", "", defaultLocale, "Jahia Spam Filtering");
logger.info("Host " + hostStats + " blacklisting notification sent by e-mail to " + administratorEmail + " using locale " + defaultLocale);
} catch (Exception e) {
logger.error("Couldn't sent spam account lock email notification: ", e);
}
}
private String getTextContent(JCRNodeWrapper node) throws RepositoryException {
StringBuilder text = new StringBuilder();
for (PropertyIterator iterator = node.getProperties(); iterator.hasNext();) {
JCRPropertyWrapper prop = (JCRPropertyWrapper) iterator.nextProperty();
ExtendedPropertyDefinition def = (ExtendedPropertyDefinition) prop.getDefinition();
if (prop.getType() == PropertyType.STRING && !def.isHidden() && !def.isProtected()) {
if (prop.isMultiple()) {
for (Value jcrValue : prop.getValues()) {
String val = jcrValue.getString();
if (StringUtils.isNotEmpty(val)) {
if (text.length() > 0) {
text.append("\n");
}
text.append(val);
}
}
} else {
String val = prop.getString();
if (StringUtils.isNotEmpty(val)) {
if (text.length() > 0) {
text.append("\n");
}
text.append(val);
}
}
}
}
return text.toString();
}
}