/*
* Copyright (C) 2016 LibreCCM Foundation.
*
* This library 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 2.1 of the License, or (at your option) any later version.
*
* This library 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 this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.librecms.contentsection;
import com.arsdigita.bebop.Page;
import com.arsdigita.cms.dispatcher.CMSPage;
import com.arsdigita.cms.dispatcher.ContentItemDispatcher;
import com.arsdigita.cms.ui.CMSApplicationPage;
import com.arsdigita.dispatcher.AccessDeniedException;
import com.arsdigita.dispatcher.DispatcherHelper;
import com.arsdigita.dispatcher.RequestContext;
import com.arsdigita.templating.PresentationManager;
import com.arsdigita.templating.Templating;
import com.arsdigita.util.Assert;
import com.arsdigita.util.Classes;
import com.arsdigita.web.ApplicationFileResolver;
import com.arsdigita.web.BaseApplicationServlet;
import com.arsdigita.web.LoginSignal;
import com.arsdigita.web.Web;
import com.arsdigita.web.WebConfig;
import com.arsdigita.xml.Document;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.librecms.CMSConfig;
import org.libreccm.cdi.utils.CdiUtil;
import org.libreccm.l10n.GlobalizationHelper;
import org.libreccm.security.PermissionChecker;
import org.libreccm.security.Shiro;
import org.libreccm.web.CcmApplication;
import org.librecms.CmsConstants;
import org.librecms.contentsection.privileges.ItemPrivileges;
import org.librecms.dispatcher.ItemResolver;
import org.librecms.lifecycle.Lifecycle;
import java.util.Date;
import javax.inject.Inject;
import javax.servlet.RequestDispatcher;
/*
* This servlet will maybe removed. Our current plan is to integrate the navigation
* application into ccm-cms, and to deliver all content using that app. Then
* this servlet becomes useless.
*/
/*
* NOTE:
* Repaired ItemURLCache to save multilingual items with automatic
* language negotiation. The cache now uses the remaining url part
* and the language concatinated as a hash table key. The delimiter
* is CACHE_KEY_DELIMITER.
*/
/*
* NOTE 2:
* In a process of refactoring from legacy compatible to legacy free applications.
* TODO:
* - replace url check using RequestContext which resolves to SiteNodeRequest
* implementation (due to SiteNodeRequest used in BaseApplicationServlet).
* - Refactor content item UI bebop ApplicationPage or PageFactory instead of
* legacy infected sitenode / package dispatchers.
*/
/**
* Content Section's Application Servlet according CCM core web application
* structure {
*
* @see com.arsdigita.web.Application} implements the content section UI.
*
* It handles the UI for content items and delegates the UI for sections and
* folders to jsp templates.
*
* @author unknown
* @author Peter Boy
* @author Jens Pelzetter
*/
@WebServlet(urlPatterns = {CmsConstants.CONTENT_SECTION_SERVLET_PATH,
"/templates/servlet/content-section/",
"/templates/servlet/content-section",
CmsConstants.CMS_SERVICE_SERVLET_PATH,
CmsConstants.CONTENT_ITEM_SERVLET_PATH})
public class ContentSectionServlet extends BaseApplicationServlet {
private static final long serialVersionUID = 8061725145564728637L;
private static final Logger LOGGER = LogManager.getLogger(
ContentSectionServlet.class);
/**
* Literal for the prefix (in url) for previewing items
*/
public static final String PREVIEW = "/preview";
/**
* Literal Template files suffix
*/
public static final String FILE_SUFFIX = ".jsp";
/**
* Literal of URL Stub for index file name (includes leading slash)
*/
public static final String INDEX_FILE = "/index";
public static final String XML_SUFFIX = ".xml";
public static final String XML_MODE = "xmlMode";
public static final String MEDIA_TYPE = "templateContext";
private static final String CACHE_KEY_DELIMITER = "%";
public static final String CONTENT_ITEM
= "com.arsdigita.cms.dispatcher.item";
public static final String CONTENT_SECTION
= "com.arsdigita.cms.dispatcher.section";
private final ContentItemDispatcher m_disp = new ContentItemDispatcher();
// private static Map itemResolverCache = Collections
// .synchronizedMap(new HashMap<>());
private static Map s_itemURLCacheMap = null;
/**
* Whether to cache the content items
*/
// private static final boolean s_cacheItems = true;
// NEW STUFF here used to process the pages in this servlet
/**
* URL (pathinfo) -> Page object mapping. Based on it (and the http request
* url) the doService method selects a page to display
*/
private final Map m_pages = new HashMap();
/**
* Path to directory containg ccm-cms template (jsp) files
*/
private String m_templatePath;
/**
* Resolver to actually use to find templates (JSP). JSP may be stored in
* file system or otherwise, depends on resolver. Resolver is retrieved from
* configuration. (probably used for other stuff as JSP's as well)
*/
private ApplicationFileResolver m_resolver;
@Inject
private ContentSectionManager sectionManager;
/**
* Init method overwrites parents init to pass in optional parameters
* {@link com.arsdigita.web.BaseServlet}. If not specified system wide
* defaults are used.
*
* @param config
*
* @throws javax.servlet.ServletException
*/
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
// optional init-param named template-path from ~/WEB-INF/web.xml
// may overwrite configuration parameters
String templatePath = config.getInitParameter("template-path");
if (templatePath == null) {
m_templatePath = CMSConfig.getConfig().getTemplateRootPath();
} else {
m_templatePath = config.getInitParameter("template-path");
}
// optional init-param named file-resolver from ~/WEB-INF/web.xml
String resolverName = config.getInitParameter("file-resolver");
if (resolverName == null) {
m_resolver = WebConfig.getConfig().getResolver();
} else {
m_resolver = (ApplicationFileResolver) Classes.newInstance(
resolverName);
}
LOGGER.debug("Template path is {} with resolver {}",
m_templatePath,
m_resolver.getClass().getName());
// NEW STUFF here will be used to process the pages in this servlet
// Currently NOT working
// addPage("/admin", new MainPage()); // index page at address ~/cs
// addPage("/admin/index.jsp", new MainPage());
// addPage("/admin/item.jsp", new MainPage());
}
/**
* Internal service method, adds one pair of Url - Page to the internal hash
* map, used as a cache.
*
* @param pathInfo url stub for a page to display
* @param page Page object to display
*/
private void addPage(final String pathInfo, final Page page) {
m_pages.put(pathInfo, page);
}
/**
* Implementation of parent's (abstract) doService method checks HTTP
* request to determine whether to handle a content item or other stuff
* which is delegated to jsp templates. {
*
* @see com.arsdigita.web.BaseApplicationServlet#doService
* (HttpServletRequest, HttpServletResponse, Application)}
*
* @param request
* @param response
* @param app
*
* @throws javax.servlet.ServletException
* @throws java.io.IOException
*/
@Override
protected void doService(final HttpServletRequest request,
final HttpServletResponse response,
final CcmApplication app)
throws ServletException, IOException {
if (!(app instanceof ContentSection)) {
throw new IllegalArgumentException(
"The provided application instance is not a content section.");
}
final ContentSection section = (ContentSection) app;
final RequestContext ctx = DispatcherHelper.getRequestContext();
final String url = ctx.getRemainingURLPart();
//Only for testing PageModel
if (url != null && url.endsWith("page-model/")) {
getServletContext()
.getRequestDispatcher("/page-model.bebop")
.include(request, response);
return;
}
//End Test PageModel
LOGGER.info("Resolving URL {} and trying as item first.", url);
final ItemResolver itemResolver = getItemResolver(section);
String pathInfo = request.getPathInfo();
if (pathInfo == null) {
pathInfo = "/";
}
final ContentItem item = getItem(section, pathInfo, itemResolver);
Assert.exists(pathInfo, "String pathInfo");
if (pathInfo.length() > 1 && pathInfo.endsWith("/")) {
/* NOTE: ServletAPI specifies, pathInfo may be empty or will
* start with a '/' character. It currently carries a
* trailing '/' if a "virtual" page, i.e. not a real jsp, but
* result of a servlet mapping. But Application requires url
* NOT to end with a trailing '/' for legacy free applications. */
pathInfo = pathInfo.substring(0, pathInfo.length() - 1);
}
final Page page = (Page) m_pages.get(pathInfo);
// ////////////////////////////////////////////////////////////////////
// Serve the page
// ////////////////////////////////////////////////////////////////////
/* FIRST try new style servlet based service */
if (page != null) {
// Check user access.
// checkUserAccess(request, response); // done in individual pages ??
if (page instanceof CMSPage) {
// backwards compatibility fix until migration completed
final CMSPage cmsPage = (CMSPage) page;
// final RequestContext ctx = DispatcherHelper.getRequestContext();
cmsPage.init();
cmsPage.dispatch(request, response, ctx);
} else {
final CMSApplicationPage cmsAppPage = (CMSApplicationPage) page;
cmsAppPage.init(request, response, app);
// Serve the page.
final Document doc = cmsAppPage.buildDocument(request, response);
final PresentationManager pm = Templating
.getPresentationManager();
pm.servePage(doc, request, response);
}
/* SECONDLY try if we have to serve an item (old style dispatcher based */
} else if (item != null) {
/* We have to serve an item here */
String param = request.getParameter("transID");
serveItem(request, response, section, item);
/* OTHERWISE delegate to a JSP in file system */
} else {
/* We have to deal with a content-section, folder or another bit */
if (LOGGER.isInfoEnabled()) {
LOGGER.info("NOT serving content item");
}
/* Store content section in http request to make it available
* for admin/index.jsp */
request.setAttribute(CONTENT_SECTION, section);
RequestDispatcher rd = m_resolver.resolve(m_templatePath,
request,
response,
app);
if (rd != null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Got dispatcher " + rd);
}
rd.forward(DispatcherHelper.restoreOriginalRequest(request),
response);
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("No dispatcher found for " + rd);
}
String requestUri = request.getRequestURI(); // same as ctx.getRemainingURLPart()
response.sendError(404, requestUri
+ " not found on this server.");
}
}
}
private void serveItem(final HttpServletRequest request,
final HttpServletResponse response,
final ContentSection section,
final ContentItem item)
throws ServletException, IOException {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("serving content item");
}
RequestContext ctx = DispatcherHelper.getRequestContext();
String url = ctx.getRemainingURLPart();
final ItemResolver itemResolver = getItemResolver(section);
//set the content item in the request
request.setAttribute(CONTENT_ITEM, item);
//set the template context
// ToDo
// final TemplateResolver templateResolver = m_disp.getTemplateResolver(
// section);
//
// String templateURL = url;
// if (!templateURL.startsWith("/")) {
// templateURL = "/" + templateURL;
// }
// if (templateURL.startsWith(PREVIEW)) {
// templateURL = templateURL.substring(PREVIEW.length());
// }
//
// final String sTemplateContext = itemResolver.getTemplateFromURL(
// templateURL);
// LOGGER.debug("setting template context to {}", sTemplateContext);
//
// templateResolver.setTemplateContext(sTemplateContext, request);
// ToDo End
// Work out how long to cache for....
// We take minimum(default timeout, lifecycle expiry)
Lifecycle cycle = item.getLifecycle();
int expires = DispatcherHelper.getDefaultCacheExpiry();
if (cycle != null) {
Date endDate = cycle.getEndDateTime();
if (endDate != null) {
int maxAge = (int) ((endDate.getTime() - System
.currentTimeMillis()) / 1000l);
if (maxAge < expires) {
expires = maxAge;
}
}
}
//use ContentItemDispatcher
m_disp.dispatch(request, response, ctx);
}
/**
* Fetches the content section from the request attributes.
*
* @param request The HTTP request
*
* @return The content section
*
* @pre ( request != null )
*/
public static ContentSection getContentSection(HttpServletRequest request) {
return (ContentSection) request.getAttribute(CONTENT_SECTION);
}
public static boolean checkAdminAccess(final HttpServletRequest request,
final ContentSection section) {
final PermissionChecker permissionChecker = CdiUtil.createCdiUtil()
.findBean(PermissionChecker.class);
return permissionChecker.isPermitted(ItemPrivileges.PREVIEW,
section)
|| permissionChecker.isPermitted(ItemPrivileges.EDIT,
section)
|| permissionChecker.isPermitted(ItemPrivileges.APPROVE,
section);
}
@SuppressWarnings("unchecked")
public ItemResolver getItemResolver(final ContentSection section) {
return sectionManager.getItemResolver(section);
}
public ContentItem getItem(final ContentSection section,
final String url,
final ItemResolver itemResolver) {
LOGGER.debug("getting item at url {}", url);
final HttpServletRequest request = Web.getRequest();
//first sanitize the url
String itemUrl = url;
if (url.endsWith(XML_SUFFIX)) {
request.setAttribute(XML_MODE, Boolean.TRUE);
LOGGER.debug("StraightXML Requested");
itemUrl = "/" + url.substring(1, url.length() - XML_SUFFIX.length());
} else {
request.setAttribute(XML_MODE, Boolean.FALSE);
if (url.endsWith(FILE_SUFFIX)) {
itemUrl = String.format(
"/%s",
url.substring(1, url.length() - FILE_SUFFIX.length()));
} else if (url.endsWith("/")) {
itemUrl = String.format("/%s",
url.substring(0, url.length() - 1));
}
}
if (!itemUrl.startsWith("/")) {
itemUrl = String.format("/%s", itemUrl);
}
ContentItem item;
final CdiUtil cdiUtil = CdiUtil.createCdiUtil();
final PermissionChecker permissionChecker = cdiUtil.findBean(
PermissionChecker.class);
final ContentItemManager itemManager = cdiUtil.findBean(
ContentItemManager.class);
// Check if the user has access to view public or preview pages
boolean hasPermission = true;
// If the remaining URL starts with "preview/", then try and
// preview this item. Otherwise look for the live item.
boolean preview = false;
if (itemUrl.startsWith(PREVIEW)) {
itemUrl = itemUrl.substring(PREVIEW.length());
preview = true;
}
if (preview) {
LOGGER.info("Trying to get item for PREVIEW");
item = itemResolver.getItem(section,
itemUrl,
ContentItemVersion.DRAFT.toString());
if (item != null) {
hasPermission = permissionChecker.isPermitted(
ItemPrivileges.PREVIEW, item);
}
} else {
LOGGER.info("Trying to get LIVE item");
//check if this item is in the cache
//we only cache live items
LOGGER.debug("Trying to get content item for URL {}from cache",
itemUrl);
// Get the negotiated locale
final GlobalizationHelper globalizationHelper = cdiUtil.findBean(
GlobalizationHelper.class);
final String lang = globalizationHelper.getNegotiatedLocale()
.getLanguage();
// XXX why assign a value and afterwards null??
// Effectively it just ignores the cache and forces a fallback to
// itemResover in any case. Maybe otherwise language selection /
// negotiation doesn't work correctly?
item = null;
if (item == null) {
LOGGER.debug("Did not find content item in cache, so trying "
+ "to retrieve and cache...");
//item not cached, so retreive it and cache it
item = itemResolver.getItem(section,
itemUrl,
ContentItemVersion.LIVE.toString());
if (LOGGER.isDebugEnabled() && item != null) {
LOGGER.debug("Sanity check: item.getPath() is {}",
itemManager.getItemPath(item));
}
if (item != null) {
LOGGER.debug("Content Item is not null");
hasPermission = permissionChecker.isPermitted(
ItemPrivileges.VIEW_PUBLISHED, item);
}
}
}
if (item == null && itemUrl.endsWith(INDEX_FILE)) {
if (item == null) {
LOGGER.info("no item found");
}
// look up folder if it's an index
itemUrl = itemUrl.substring(0, itemUrl.length() - INDEX_FILE
.length());
LOGGER.info("Attempting to match folder " + itemUrl);
item = itemResolver.getItem(section,
itemUrl,
ContentItemVersion.LIVE.toString());
if (item != null) {
hasPermission = permissionChecker.isPermitted(
ItemPrivileges.VIEW_PUBLISHED, item);
}
}
if (!hasPermission) {
// first, check if the user is logged-in
// if he isn't, give him a chance to do so...
final Shiro shiro = cdiUtil.findBean(Shiro.class);
if (shiro.getSubject().isAuthenticated()) {
throw new LoginSignal(request);
}
throw new AccessDeniedException();
}
return item;
}
public ContentItem getItem(final ContentSection section, final String url) {
final ItemResolver itemResolver = getItemResolver(section);
return getItem(section, url, itemResolver);
}
// synchronize access to the item-url cache
// private static synchronized void itemURLCachePut(
// final ContentSection section,
// final String sURL,
// final String lang,
// final Long itemID) {
//
// getItemURLCache(section).put(String.format(
// "%s" + CACHE_KEY_DELIMITER + "%s", sURL, lang), itemID);
// }
//
// /**
// * Maps the content item to the URL in a cache
// *
// * @param section the content section in which the content item is published
// * @param url the URL at which the content item s published
// * @param lang
// * @param item the content item at the URL
// */
// public static synchronized void itemURLCachePut(
// final ContentSection section,
// final String url,
// final String lang,
// final ContentItem item) {
//
// if (url == null || item == null) {
// return;
// }
// LOGGER.debug("adding cached entry for url {} and language {}",
// url,
// lang);
//
// itemURLCachePut(section, url, lang, item.getObjectId());
// }
//
// /**
// * Removes the cache entry for the URL, sURL
// *
// * @param section the content section in which to remove the key
// * @param url the cache entry key to remove
// * @param lang
// */
// public static synchronized void itemURLCacheRemove(
// final ContentSection section,
// final String url,
// final String lang) {
//
// LOGGER.debug("removing cached entry for url {} and language {} ",
// url,
// lang);
//
// getItemURLCache(section).remove(url + CACHE_KEY_DELIMITER + lang);
// }
//
// /**
// * Fetches the ContentItem published at that URL from the cache.
// *
// * @param section the content section in which the content item is published
// * @param url the URL for the item to fetch
// * @param lang
// * @return the ContentItem in the cache, or null
// */
// public static ContentItem itemURLCacheGet(final ContentSection section,
// final String url,
// final String lang) {
// final Long itemID = (Long) getItemURLCache(section).get(
// url + CACHE_KEY_DELIMITER + lang);
//
// if (itemID == null) {
// return null;
// } else {
// final ContentItemRepository itemRepo = CdiUtil.createCdiUtil().findBean(ContentItemRepository.class);
// try {
// return itemRepo.findById(itemID);
// } catch(NoResultException ex) {
// return null;
// }
// }
// }
//
// private static synchronized CacheTable getItemURLCache(ContentSection section) {
// Assert.exists(section, ContentSection.class);
// if (s_itemURLCacheMap == null) {
// initializeItemURLCache();
// }
//
// if (s_itemURLCacheMap.get(section.getPath()) == null) {
// final CacheTable cache = new CacheTable("ContentSectionServletItemURLCache" +
// section.getID().toString());
// s_itemURLCacheMap.put(section.getPath(), cache);
// }
//
// return (CacheTable) s_itemURLCacheMap.get(section.getPath());
// }
}