/* * Copyright (C) 2017 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.dispatcher; import com.arsdigita.bebop.PageState; import com.arsdigita.cms.CMS; import com.arsdigita.cms.dispatcher.CMSDispatcher; import com.arsdigita.cms.dispatcher.CMSPage; import com.arsdigita.cms.dispatcher.MasterPage; import com.arsdigita.cms.ui.ContentItemPage; import com.arsdigita.kernel.KernelConfig; import com.arsdigita.util.Assert; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.libreccm.core.CcmObject; import org.librecms.CmsConstants; import org.librecms.contentsection.ContentItem; import org.librecms.contentsection.ContentItemManager; import org.librecms.contentsection.ContentItemRepository; import org.librecms.contentsection.ContentItemVersion; import org.librecms.contentsection.ContentSection; import org.librecms.contentsection.Folder; import org.librecms.contentsection.FolderManager; import org.librecms.contentsection.FolderRepository; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.StringTokenizer; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.transaction.Transactional; /** * Resolves items to URLs and URLs to items for multiple language variants. * * For version 7.0.0 this call has been moved from the * {@code com.arsdigita.cms.dispatcher} package to the * {@code org.librecms.dispatcher} package and refactored to an CDI bean. This * was necessary to avoid several problems when accessing the entity beans for * {@link Category} and {@link ContentItem}, primarily the infamous * {@code LazyInitializationException}. Also several checks for null parameters * were added to avoid {@code NullPointerExcpetions}. * * * AS of version 7.0.0 this class not longer part of the public API. It is left * here to keep the changes to the UI classes as minimal as possible. For new * code other methods, for example from the {@link ContentItemManager} or the * {@link ContentItemRepository} should be used. Because this class is no longer * part of the public API the will might be removed or changed in future * releases without prior warning. * * * * * @author Michael Hanisch * @author Jens Pelzetter */ @RequestScoped public class MultilingualItemResolver implements ItemResolver { private static final Logger LOGGER = LogManager.getLogger( MultilingualItemResolver.class); private static final String ADMIN_PREFIX = "admin"; /** * The string identifying an item's ID in the query string of a URL. */ protected static final String ITEM_ID = "item_id"; /** * The separator used in URL query strings; should be either "&" or ";". */ protected static final String SEPARATOR = "&"; @Inject private FolderRepository folderRepo; @Inject private FolderManager folderManager; @Inject private ContentItemRepository itemRepo; @Inject private ContentItemManager itemManager; /** * Returns a content item based on section, url, and use context. * * @param section The current content section * @param itemUrl The section-relative URL * @param context The use context, e.g. ContentItem.LIVE, * CMSDispatcher.PREVIEW or * ContentItem.DRAFT. See {@link * #getCurrentContext}. * * @return The content item, or null if no such item exists */ @Transactional(Transactional.TxType.REQUIRED) @Override public ContentItem getItem(final ContentSection section, final String itemUrl, final String context) { if (section == null) { throw new IllegalArgumentException( "Can't get item from section null."); } if (itemUrl == null) { throw new IllegalArgumentException("Can't get item for URL null."); } if (context == null) { throw new IllegalArgumentException( "Can't get item for context null."); } LOGGER.debug("Resolving the item in content section \"{}\" at URL " + "\"{}\" for context \"{}\"...", section.getLabel(), itemUrl, context); final Folder rootFolder = section.getRootDocumentsFolder(); String url = stripTemplateFromURL(itemUrl); if (rootFolder == null) { // nothing to do, if root folder is null LOGGER.debug("The root folder is null; returning no item"); } else { LOGGER.debug("Using root folder {}...", rootFolder.getName()); if (ContentItemVersion.LIVE.toString().equals(context)) { LOGGER.debug("The use context is 'live'"); LOGGER.debug("The root folder has a live version; recursing"); final String prefix = String.join( "", section.getPrimaryUrl(), folderManager.getFolderPath(rootFolder)); if (url.startsWith(prefix)) { LOGGER. debug("The starts with prefix \"{}\"; removing it...", prefix); url = url.substring(prefix.length()); } final ContentItem item = getItemFromLiveURL(url, rootFolder); LOGGER.debug("Resolved URL \"{}\" to item {}...", url, Objects.toString(item)); return item; } else if (ContentItemVersion.DRAFT.toString().equals(context)) { LOGGER.debug("The use context is 'draft'"); // For 'draft' items, 'generateUrl()' method returns // URL like this // '/acs/wcms/admin/item.jsp?item_id=10201&set_tab=1' // Check if URL contains any match of string // 'item_id', then try to instanciate item_id value // and return FIXME: Please hack this if there is // more graceful solution. [aavetyan] if (Assert.isEnabled()) { Assert.isTrue(url.contains(ITEM_ID), String.format("url must contain parameter %s", ITEM_ID)); } final ContentItem item = getItemFromDraftURL(url); LOGGER.debug("Resolved URL \"{}\" to item {}.", url, Objects.toString(item)); return item; } else if (CMSDispatcher.PREVIEW.equals(context)) { LOGGER.debug("The use context is 'preview'"); final String prefix = CMSDispatcher.PREVIEW + "/"; if (url.startsWith(prefix)) { LOGGER.debug( "The URL starts with prefix \"{}\"; removing it", prefix); url = url.substring(prefix.length()); } final ContentItem item = getItemFromLiveURL(url, rootFolder); LOGGER.debug("Resolved URL \"{}\" to item {}.", url, Objects.toString(item)); return item; } else { throw new IllegalArgumentException(String.format( "Invalid item resolver context \"%s\".", context)); } } LOGGER.debug("No item resolved; returning null"); return null; } /** * Fetches the current context based on the page state. * * @param state the current page state * * @return the context of the current URL, such as * ContentItem.LIVE or ContentItem.DRAFT * * @see ContentItem#LIVE * @see ContentItem#DRAFT * * ToDo: Refactor to use the {@link ContentItemVersion} directly. */ @Transactional(Transactional.TxType.REQUIRED) @Override public String getCurrentContext(final PageState state) { LOGGER.debug("Getting the current context"); // XXX need to use Web.getWebContext().getRequestURL() here. String url = state.getRequest().getRequestURI(); final ContentSection section = CMS.getContext().getContentSection(); // If this page is associated with a content section, // transform the URL so that it is relative to the content // section site node. if (section != null) { final String sectionURL = section.getPrimaryUrl(); if (url.startsWith(sectionURL)) { url = url.substring(sectionURL.length()); } } // Remove any template-specific URL components (will only work // if they're first in the URL at this point; verify). XXX but // we don't actually verify? url = stripTemplateFromURL(url); // Determine if we are under the admin UI. if (url.startsWith(ADMIN_PREFIX) || url.startsWith(CmsConstants.CONTENT_CENTER_URL)) { return ContentItemVersion.DRAFT.toString(); } else { return ContentItemVersion.LIVE.toString(); } } /** * Generates a URL for a content item. * * @param itemId The item ID * @param name The name of the content page * @param state The page state * @param section the content section to which the item belongs * @param context the context of the URL, such as "live" or "admin" * * @return The URL of the item * * @see #getCurrentContext */ @Transactional(Transactional.TxType.REQUIRED) @Override public String generateItemURL(final PageState state, final Long itemId, final String name, final ContentSection section, final String context) { return generateItemURL(state, itemId, name, section, context, null); } /** * Generates a URL for a content item. * * @param itemId The item ID * @param name The name of the content page * @param state The page state * @param section the content section to which the item belongs * @param context the context of the URL, such as "live" or "admin" * @param templateContext the context for the URL, such as "public" * * @return The URL of the item * * @see #getCurrentContext */ @Transactional(Transactional.TxType.REQUIRED) @Override public String generateItemURL(final PageState state, final Long itemId, final String name, final ContentSection section, final String context, final String templateContext) { if (itemId == null) { throw new IllegalArgumentException( "Can't generate item URL for item id null."); } if (context == null) { throw new IllegalArgumentException( "Can't generate item URL for context null."); } if (section == null) { throw new IllegalArgumentException( "Can't generate item URL for section null."); } LOGGER.debug("Generating an item URL for item id {}, section \"{}\" " + "and context \"{}\" with name \"{}\"...", itemId, section.getLabel(), context, name); if (ContentItemVersion.DRAFT.toString().equals(context)) { // No template context here. return generateDraftURL(section, itemId); } else if (CMSDispatcher.PREVIEW.equals(context)) { final ContentItem item = itemRepo.findById(itemId).get(); return generatePreviewURL(section, item, templateContext); } else if (ContentItemVersion.LIVE.toString().equals(context)) { final ContentItem item = itemRepo.findById(itemId).get(); return generateLiveURL(section, item, templateContext); } else { throw new IllegalArgumentException("Unknown context '" + context + "'"); } } /** * Generates a URL for a content item. * * @param item The item * @param state The page state * @param section the content section to which the item belongs * @param context the context of the URL, such as "live" or "admin" * * @return The URL of the item * * @see #getCurrentContext */ @Transactional(Transactional.TxType.REQUIRED) @Override public String generateItemURL(final PageState state, final ContentItem item, final ContentSection section, final String context) { return generateItemURL(state, item, section, context, null); } /** * Generates a URL for a content item. * * @param item The item * @param state The page state * @param section the content section to which the item belongs * @param context the context of the URL, such as "live" or "admin" * @param templateContext the context for the URL, such as "public" * * @return The URL of the item * * @see #getCurrentContext */ @Transactional(Transactional.TxType.REQUIRED) @Override public String generateItemURL(final PageState state, final ContentItem item, final ContentSection section, final String context, final String templateContext) { if (item == null) { throw new IllegalArgumentException( "Can't generate URL for item null."); } if (context == null) { throw new IllegalArgumentException( "Can't generate URL for context null."); } final ContentSection contentSection; if (section == null) { contentSection = item.getContentType().getContentSection(); } else { contentSection = section; } LOGGER.debug("Generating an item URL for item \"{}\", section \"{}\" " + "and context \"{}\".", item.getDisplayName(), contentSection.getLabel(), context); if (ContentItemVersion.DRAFT.toString().equals(context)) { return generateDraftURL(section, item.getObjectId()); } else if (CMSDispatcher.PREVIEW.equals(context)) { return generatePreviewURL(section, item, templateContext); } else if (ContentItemVersion.LIVE.toString().equals(context)) { return generateLiveURL(contentSection, item, templateContext); } else { throw new IllegalArgumentException(String.format( "Unknown context \"%s\".", context)); } } @Transactional(Transactional.TxType.REQUIRED) @Override public CMSPage getMasterPage(final ContentItem item, final HttpServletRequest request) throws ServletException { LOGGER.debug("Getting the master page for item {}", item.getDisplayName()); final MasterPage masterPage = new MasterPage(); masterPage.init(); return masterPage; } /** * Returns content item's draft version URL * * @param section The content section to which the item belongs * @param itemId The content item's ID * * @return generated URL string */ @Transactional(Transactional.TxType.REQUIRED) protected String generateDraftURL(final ContentSection section, final Long itemId) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Generating draft URL for item ID " + itemId + " and section " + section); } if (Assert.isEnabled()) { Assert.isTrue(section != null && itemId != null, "get draft url: neither secion nor item " + "can be null"); } final String url = ContentItemPage.getItemURL( String.format("%s/", section.getPrimaryUrl()), itemId, ContentItemPage.AUTHORING_TAB); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Generated draft URL " + url); } return url; } /** * Generate a language-independent URL to the item in * the given section.

* When a client retrieves this URL, the URL is resolved to point to a * specific language instance of the item referenced here, i.e. this URL * will be resolved to a language-specific URL internally. * * @param section the ContentSection that contains this * item * @param item ContentItem for which a URL should be * constructed. * @param templateContext template context; will be ignored if * null * * @return a language-independent URL to the item in * the given section, which will be presented within * the given templateContext */ @Transactional(Transactional.TxType.REQUIRED) protected String generateLiveURL(final ContentSection section, final ContentItem item, final String templateContext) { LOGGER.debug("Generating live URL for item {} in section {}", Objects.toString(item), Objects.toString(section)); /* * URL = URL of section + templateContext + path to the ContentBundle * which contains the item */ final StringBuffer url = new StringBuffer(400); //url.append(section.getURL()); url .append(section.getPrimaryUrl()) .append("/"); /* * add template context, if one is given */ // This is breaking URL's...not sure why it's here. XXX // this should work with the appropriate logic. trying again. if (!(templateContext == null || templateContext.length() == 0)) { url .append(TEMPLATE_CONTEXT_PREFIX) .append(templateContext) .append("/"); } url.append(itemManager.getItemPath(item)); return url.toString(); } /** * Generate a URL which can be used to preview the item, using * the given templateContext.

* Only a specific language instance can be previewed, meaning there * no language negotiation is involved when a request is made to a * URL that has been generated by this method. * * @param section The ContentSection which contains the * item * @param item The ContentItem for which a URL * should be generated. * @param templateContext the context that determines which template should * render the item when it is previewed; ignored if * the argument given here is null * * @return a URL which can be used to preview the given item */ @Transactional(Transactional.TxType.REQUIRED) protected String generatePreviewURL(final ContentSection section, final ContentItem item, final String templateContext) { Assert.exists(section, "ContentSection section"); Assert.exists(item, "ContentItem item"); final StringBuffer url = new StringBuffer(100); url .append(section.getPrimaryUrl()) .append("/") .append(CMSDispatcher.PREVIEW) .append("/"); /* * add template context, if one is given */ // This is breaking URL's...not sure why it's here. XXX // this should work with the appropriate logic. trying again. if (!(templateContext == null || templateContext.length() == 0)) { url .append(TEMPLATE_CONTEXT_PREFIX) .append(templateContext) .append("/"); } url.append(itemManager.getItemPath(item)); return url.toString(); } /** * Retrieves ITEM_ID parameter from URL and instantiates item * according to this ID. * * @param url URL that indicates which item should be retrieved; must * contain the ITEM_ID parameter * * @return the ContentItem the given url points * to, or null if no ID has been found in the * url */ @Transactional(Transactional.TxType.REQUIRED) protected ContentItem getItemFromDraftURL(final String url) { LOGGER.debug("Looking up the item from draft URL ", url); int pos = url.indexOf(ITEM_ID); String item_id = url.substring(pos); // item_id == ITEM_ID=.... ? pos = item_id.indexOf("="); // should be exactly after the ITEM_ID string if (pos != ITEM_ID.length()) { // item_id seems to be something like ITEM_IDFOO= LOGGER.debug("No suitable item_id parameter found; returning null"); return null; // no ID found } pos++; // skip the "=" // ID is the string between the equal (at pos) and the next separator int i = item_id.indexOf(SEPARATOR); item_id = item_id.substring(pos, Math.min(i, item_id.length() - 1)); LOGGER.debug("Looking up item using item ID {}", item_id); final Optional item = itemRepo.findById(Long.parseLong( item_id)); if (item.isPresent()) { LOGGER.debug("Returning item {}", Objects.toString(item)); return item.get(); } else { return null; } } /** * Returns a content item based on URL relative to the root folder. * * @param url The content item url * @param parentFolder The parent folder object, url must be relevant to it * * @return The Content Item instance, it can also be either Bundle or Folder * objects, depending on URL and file language extension */ @Transactional(Transactional.TxType.REQUIRED) protected ContentItem getItemFromLiveURL(final String url, final Folder parentFolder) { if (parentFolder == null || url == null || url.equals("")) { LOGGER.debug("The url is null or parent folder was null " + "or something else is wrong, so just return " + "null."); return null; } LOGGER.debug("Resolving the item for live URL {}" + " and parent folder {}...", url, parentFolder.getName()); int len = url.length(); int index = url.indexOf('/'); if (index >= 0) { LOGGER.debug("The URL starts with a slash; paring off the first " + "URL element and recursing"); final String liveUrl = index + 1 < len ? url.substring(index + 1) : ""; return getItemFromLiveURL(liveUrl, parentFolder); } else { LOGGER.debug("Found a file element in the URL"); final String[] nameAndLang = getNameAndLangFromURLFrag(url); final String name = nameAndLang[0]; final Optional item = itemRepo.findByNameInFolder( parentFolder, name); if (item.isPresent()) { return item.get(); } else { return null; } } } /** * Returns an array containing the the item's name and lang based on the URL * fragment. * * @param url * * @return a two-element string array, the first element containing the * bundle name, and the second element containing the lang string */ @Transactional(Transactional.TxType.REQUIRED) protected String[] getNameAndLangFromURLFrag(final String url) { String name; String lang = null; /* * Try to find out if there's an extension with the language code * 1 Get a list of all "extensions", i.e. parts of the url * which are separated by colons * 2 If one or more extensions have been found, compare them against * the list of known language codes * 2a if a match is found, this language is used to retrieve an instance * from a bundle * 2b if no match is found */ final List exts = new ArrayList<>(5); final StringTokenizer tok = new StringTokenizer(url, "."); while (tok.hasMoreTokens()) { exts.add(tok.nextToken()); } if (exts.size() > 0) { LOGGER.debug("Found some file extensions to look at; they " + "are {}", exts); /* * We have found at least one extension, so we can * proceed. Now try to find out if one of the * extensions looks like a language code (we only * support 2-letter language codes!). * If so, use this as the language to look for. */ /* * First element is the NAME of the item, not an extension! */ name = exts.get(0); String ext; final Collection supportedLangs = KernelConfig.getConfig() .getSupportedLanguages(); Iterator supportedLangIt; for (int i = 1; i < exts.size(); i++) { ext = exts.get(i); LOGGER.debug("Examining extension {}", ext); /* * Loop through all extensions, but discard the * first one, which is the name of the item. */ if (ext != null && ext.length() == 2) { /* Only check extensions consisting of 2 * characters. * * Compare current extension with known * languages; if it matches, we've found the * language we should use! */ supportedLangIt = supportedLangs.iterator(); while (supportedLangIt.hasNext()) { if (ext.equals(supportedLangIt.next())) { lang = ext; LOGGER.debug("Found a match; using " + "language {}", lang); break; } } } else { LOGGER.debug("Discarding extension {}; it is too short", ext); } } } else { LOGGER.debug("The file has no extensions; no language was encoded"); name = url; // no extension, so we just have a name here lang = null; // no extension, so we cannot guess the language } LOGGER.debug("File name resolved to {}", name); LOGGER.debug("File language resolved to {}", lang); final String[] returnArray = new String[2]; returnArray[0] = name; returnArray[1] = lang; return returnArray; } /** * Finds a language instance of a content item given the bundle, name, and * lang string. Note: Because there not ContentBundles anymore this method * simply returns the provided item. * * @param lang The lang string from the URL * @param item The content bundle * * @return The negotiated lang instance for the current request. */ @Transactional(Transactional.TxType.REQUIRED) protected ContentItem getItemFromLangAndBundle(final String lang, final ContentItem item) { return item; } }