/** * This file Copyright (c) 2011-2012 Magnolia International * Ltd. (http://www.magnolia-cms.com). All rights reserved. * * * This file is dual-licensed under both the Magnolia * Network Agreement and the GNU General Public License. * You may elect to use one or the other of these licenses. * * This file is distributed in the hope that it will be * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the * implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. * Redistribution, except as permitted by whichever of the GPL * or MNA you select, is prohibited. * * 1. For the GPL license (GPL), you can redistribute and/or * modify this file under the terms of the GNU General * Public License, Version 3, as published by the Free Software * Foundation. You should have received a copy of the GNU * General Public License, Version 3 along with this program; * if not, write to the Free Software Foundation, Inc., 51 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * 2. For the Magnolia Network Agreement (MNA), this file * and the accompanying materials are made available under the * terms of the MNA which accompanies this distribution, and * is available at http://www.magnolia-cms.com/mna.html * * Any modifications to this file must keep this entire header * intact. * */ package info.magnolia.setup.for4_5; import static java.lang.String.format; import static org.apache.commons.lang.ArrayUtils.contains; import info.magnolia.cms.core.Content; import info.magnolia.cms.core.HierarchyManager; import info.magnolia.cms.core.NodeData; import info.magnolia.cms.filters.FilterManager; import info.magnolia.cms.util.ContentUtil; import info.magnolia.jcr.util.NodeTypes; import info.magnolia.module.InstallContext; import info.magnolia.module.delta.AbstractRepositoryTask; import info.magnolia.module.delta.TaskExecutionException; import java.util.Collection; import java.util.Set; import javax.jcr.RepositoryException; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Function; import com.google.common.base.Predicates; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; /** * Updates the given security filter's client callback configuration to reflect the changes introduced in 4.5. */ public class UpdateSecurityFilterClientCallbacksConfiguration extends AbstractRepositoryTask { private static final Logger log = LoggerFactory.getLogger(UpdateSecurityFilterClientCallbacksConfiguration.class); private static final String FORM_CLASS = "info.magnolia.cms.security.auth.callback.FormClientCallback"; private static final String COMPOSITE_CLASS = "info.magnolia.cms.security.auth.callback.CompositeCallback"; private static final String BASIC_CLASS = "info.magnolia.cms.security.auth.callback.BasicClientCallback"; private static final String REDIRECT_CLASS = "info.magnolia.cms.security.auth.callback.RedirectClientCallback"; private static final String[] SIMPLE_CALLBACK_CLASSES = new String[]{FORM_CLASS, BASIC_CLASS, REDIRECT_CLASS}; private final String fromFilterName; private final String targetFilterName; private boolean wereWeAbleToUpdateEverything = true; public UpdateSecurityFilterClientCallbacksConfiguration(String fromFilterName, String targetFilterName) { super("Security filter configuration", "Moves the client callback configuration from the " + fromFilterName + " to the new " + targetFilterName + " filter to enable multiple client callbacks."); this.fromFilterName = fromFilterName; this.targetFilterName = targetFilterName; } @Override protected void doExecute(InstallContext ctx) throws RepositoryException, TaskExecutionException { final HierarchyManager hm = ctx.getConfigHierarchyManager(); final Content fromFilterNode = hm.getContent(FilterManager.SERVER_FILTERS + "/" + fromFilterName); final Content targetFilterNode = hm.getContent(FilterManager.SERVER_FILTERS + "/" + targetFilterName); final Content newCallbacksNode = targetFilterNode.createContent("clientCallbacks", NodeTypes.ContentNode.NAME); final Content currentCallbackNode = fromFilterNode.getContent("clientCallback"); final String currentClass = currentCallbackNode.getNodeData("class").getString(); if (contains(SIMPLE_CALLBACK_CLASSES, currentClass)) { addCallback(ctx, newCallbacksNode, null, currentCallbackNode, null); } else if (!currentCallbackNode.hasChildren()) { // we can assume it's a simple custom callback which we can simply move addCallback(ctx, newCallbacksNode, "custom", currentCallbackNode, null); } else if (COMPOSITE_CLASS.equals(currentClass)) { final Collection<Content> existingCallbacks = currentCallbackNode.getContent("patterns").getChildren(); for (Content existingCallback : existingCallbacks) { final String clazz = existingCallback.getNodeData("class").getString(); if ("info.magnolia.cms.util.UrlPatternDelegate".equals(clazz)) { final String url = existingCallback.getNodeData("url").getString(); addCallback(ctx, newCallbacksNode, existingCallback.getName(), existingCallback.getContent("delegate"), url); } else { ctx.warn("Unknown callback class at " + existingCallback.getHandle() + ":" + clazz); wereWeAbleToUpdateEverything = false; } } } else { ctx.warn("Unknown callback class:" + currentClass); wereWeAbleToUpdateEverything = false; } // only rename if unsuccessful ? if (wereWeAbleToUpdateEverything) { currentCallbackNode.delete(); } else { ContentUtil.moveInSession(currentCallbackNode, fromFilterNode.getHandle() + "/_clientCallback_backup_config"); ctx.warn(format( "Client callback configuration for %s was not standard: an untouched copy of %s has been kept at %s. Please check, validate and correct the new configuration at %s.", fromFilterName, fromFilterNode.getHandle() + "/clientCallback", fromFilterNode.getHandle() + "/_clientCallback_backup_config", newCallbacksNode.getHandle() )); } } private void addCallback(InstallContext ctx, Content target, String givenCallbackName, Content source, String urlPattern) throws RepositoryException { final String clazz = source.getNodeData("class").getString(); final String newName; if (givenCallbackName == null && contains(SIMPLE_CALLBACK_CLASSES, clazz)) { newName = simplifyClassName(clazz); } else if (givenCallbackName != null) { newName = givenCallbackName; } else { log.warn("Can not determine name for callback at " + source.getHandle()); wereWeAbleToUpdateEverything = false; return; } final Content newCallback = target.createContent(newName, NodeTypes.ContentNode.NAME); copyStringProperty(source, newCallback, "class"); if (FORM_CLASS.equals(clazz)) { copyStringProperty(source, newCallback, "loginForm"); logAndIgnore(ctx, source, "realmName"); checkRemainingProperties(ctx, source, "class", "loginForm", "realmName"); } else if (REDIRECT_CLASS.equals(clazz)) { copyStringProperty(source, newCallback, "location"); logAndIgnore(ctx, source, "realmName"); checkRemainingProperties(ctx, source, "class", "location", "realmName"); } else if (BASIC_CLASS.equals(clazz)) { copyStringProperty(source, newCallback, "realmName"); checkRemainingProperties(ctx, source, "class", "realmName"); } else { log.warn("Unknown callback class: " + clazz + "; copying all properties."); wereWeAbleToUpdateEverything = false; copyRemainingProperties(ctx, source, newCallback, "class"); } if (urlPattern != null) { Content urlPatternContent = newCallback.createContent("urlPattern", NodeTypes.ContentNode.NAME); urlPatternContent.setNodeData("class", "info.magnolia.cms.util.SimpleUrlPattern"); urlPatternContent.setNodeData("patternString", urlPattern); } } private String simplifyClassName(String clazz) { return StringUtils.removeEnd(StringUtils.substringAfterLast(clazz, "."), "ClientCallback").toLowerCase(); } private void copyStringProperty(Content source, Content target, String propertyName) throws RepositoryException { target.setNodeData(propertyName, source.getNodeData(propertyName).getString()); } /** * Checks if the given node has a given property; logs it and continues if so. */ private void logAndIgnore(InstallContext ctx, Content source, String propertyName) throws RepositoryException { if (source.hasNodeData(propertyName)) { ctx.warn(source.getHandle() + " has a '" + propertyName + "' property; it is ignored and has been removed."); } } /** * Checks if the given node has other properties than those specified by the ignoredProperties parameter. */ private void checkRemainingProperties(InstallContext ctx, Content source, String... ignoredProperties) { final Set<String> ignoredPropsSet = Sets.newHashSet(ignoredProperties); final Collection<NodeData> allProps = source.getNodeDataCollection(); final Iterable<String> allPropsNames = Iterables.transform(allProps, new Function<NodeData, String>() { @Override public String apply(NodeData from) { return from.getName(); } }); final Iterable<String> remaining = Iterables.filter(allPropsNames, Predicates.not(Predicates.in(ignoredPropsSet))); if (!Iterables.isEmpty(remaining)) { log.warn(source.getHandle() + " has the following unknown properties which were not copied: " + remaining); wereWeAbleToUpdateEverything = false; } } private void copyRemainingProperties(InstallContext ctx, Content source, Content target, String... ignoredProperties) throws RepositoryException { final Collection<NodeData> existingProps = source.getNodeDataCollection(); for (NodeData prop : existingProps) { if (ArrayUtils.contains(ignoredProperties, prop.getName())) { continue; } copyStringProperty(source, target, prop.getName()); } } }