View Javadoc
1   /**
2    * This file Copyright (c) 2011-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.setup.for4_5;
35  
36  import static java.lang.String.format;
37  import static org.apache.commons.lang3.ArrayUtils.contains;
38  
39  import info.magnolia.cms.core.Content;
40  import info.magnolia.cms.core.HierarchyManager;
41  import info.magnolia.cms.core.NodeData;
42  import info.magnolia.cms.filters.FilterManager;
43  import info.magnolia.cms.util.ContentUtil;
44  import info.magnolia.cms.util.HierarchyManagerUtil;
45  import info.magnolia.jcr.util.NodeTypes;
46  import info.magnolia.module.InstallContext;
47  import info.magnolia.module.delta.AbstractRepositoryTask;
48  import info.magnolia.module.delta.TaskExecutionException;
49  
50  import java.util.Collection;
51  import java.util.Set;
52  
53  import javax.jcr.RepositoryException;
54  
55  import org.apache.commons.lang3.ArrayUtils;
56  import org.apache.commons.lang3.StringUtils;
57  import org.slf4j.Logger;
58  import org.slf4j.LoggerFactory;
59  
60  import com.google.common.base.Function;
61  import com.google.common.base.Predicates;
62  import com.google.common.collect.Iterables;
63  import com.google.common.collect.Sets;
64  
65  /**
66   * Updates the given security filter's client callback configuration to reflect the changes introduced in 4.5.
67   *
68   * @deprecated since 5.6 – without replacement.
69   */
70  @Deprecated
71  public class UpdateSecurityFilterClientCallbacksConfiguration extends AbstractRepositoryTask {
72  
73      private static final Logger log = LoggerFactory.getLogger(UpdateSecurityFilterClientCallbacksConfiguration.class);
74  
75      private static final String FORM_CLASS = "info.magnolia.cms.security.auth.callback.FormClientCallback";
76      private static final String COMPOSITE_CLASS = "info.magnolia.cms.security.auth.callback.CompositeCallback";
77      private static final String BASIC_CLASS = "info.magnolia.cms.security.auth.callback.BasicClientCallback";
78      private static final String REDIRECT_CLASS = "info.magnolia.cms.security.auth.callback.RedirectClientCallback";
79      private static final String[] SIMPLE_CALLBACK_CLASSES = new String[]{FORM_CLASS, BASIC_CLASS, REDIRECT_CLASS};
80      private final String fromFilterName;
81      private final String targetFilterName;
82      private boolean wereWeAbleToUpdateEverything = true;
83  
84      public UpdateSecurityFilterClientCallbacksConfiguration(String fromFilterName, String targetFilterName) {
85          super("Security filter configuration", "Moves the client callback configuration from the " + fromFilterName + " to the new " + targetFilterName + " filter to enable multiple client callbacks.");
86          this.fromFilterName = fromFilterName;
87          this.targetFilterName = targetFilterName;
88      }
89  
90      @Override
91      protected void doExecute(InstallContext ctx) throws RepositoryException, TaskExecutionException {
92          final HierarchyManager hm = HierarchyManagerUtil.asHierarchyManager(ctx.getConfigJCRSession());
93          final Content fromFilterNode = hm.getContent(FilterManager.SERVER_FILTERS + "/" + fromFilterName);
94          final Content targetFilterNode = hm.getContent(FilterManager.SERVER_FILTERS + "/" + targetFilterName);
95  
96          final Content newCallbacksNode = targetFilterNode.createContent("clientCallbacks", NodeTypes.ContentNode.NAME);
97          final Content currentCallbackNode = fromFilterNode.getContent("clientCallback");
98          final String currentClass = currentCallbackNode.getNodeData("class").getString();
99          if (contains(SIMPLE_CALLBACK_CLASSES, currentClass)) {
100             addCallback(ctx, newCallbacksNode, null, currentCallbackNode, null);
101         } else if (!currentCallbackNode.hasChildren()) {
102             // we can assume it's a simple custom callback which we can simply move
103             addCallback(ctx, newCallbacksNode, "custom", currentCallbackNode, null);
104         } else if (COMPOSITE_CLASS.equals(currentClass)) {
105             final Collection<Content> existingCallbacks = currentCallbackNode.getContent("patterns").getChildren();
106             for (Content existingCallback : existingCallbacks) {
107                 final String clazz = existingCallback.getNodeData("class").getString();
108                 if ("info.magnolia.cms.util.UrlPatternDelegate".equals(clazz)) {
109                     final String url = existingCallback.getNodeData("url").getString();
110                     addCallback(ctx, newCallbacksNode, existingCallback.getName(), existingCallback.getContent("delegate"), url);
111                 } else {
112                     ctx.warn("Unknown callback class at " + existingCallback.getHandle() + ":" + clazz);
113                     wereWeAbleToUpdateEverything = false;
114                 }
115             }
116         } else {
117             ctx.warn("Unknown callback class:" + currentClass);
118             wereWeAbleToUpdateEverything = false;
119         }
120 
121         // only rename if unsuccessful ?
122         if (wereWeAbleToUpdateEverything) {
123             currentCallbackNode.delete();
124         } else {
125             ContentUtil.moveInSession(currentCallbackNode, fromFilterNode.getHandle() + "/_clientCallback_backup_config");
126             ctx.warn(format(
127                     "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.",
128                     fromFilterName, fromFilterNode.getHandle() + "/clientCallback", fromFilterNode.getHandle() + "/_clientCallback_backup_config", newCallbacksNode.getHandle()
129             ));
130 
131         }
132     }
133 
134     private void addCallback(InstallContext ctx, Content target, String givenCallbackName, Content source, String urlPattern) throws RepositoryException {
135         final String clazz = source.getNodeData("class").getString();
136         final String newName;
137         if (givenCallbackName == null && contains(SIMPLE_CALLBACK_CLASSES, clazz)) {
138             newName = simplifyClassName(clazz);
139         } else if (givenCallbackName != null) {
140             newName = givenCallbackName;
141         } else {
142             log.warn("Can not determine name for callback at {}", source.getHandle());
143             wereWeAbleToUpdateEverything = false;
144             return;
145         }
146 
147         final Content newCallback = target.createContent(newName, NodeTypes.ContentNode.NAME);
148         copyStringProperty(source, newCallback, "class");
149         if (FORM_CLASS.equals(clazz)) {
150             copyStringProperty(source, newCallback, "loginForm");
151             logAndIgnore(ctx, source, "realmName");
152             checkRemainingProperties(ctx, source, "class", "loginForm", "realmName");
153         } else if (REDIRECT_CLASS.equals(clazz)) {
154             copyStringProperty(source, newCallback, "location");
155             logAndIgnore(ctx, source, "realmName");
156             checkRemainingProperties(ctx, source, "class", "location", "realmName");
157         } else if (BASIC_CLASS.equals(clazz)) {
158             copyStringProperty(source, newCallback, "realmName");
159             checkRemainingProperties(ctx, source, "class", "realmName");
160         } else {
161             log.warn("Unknown callback class: {}; copying all properties.", clazz);
162             wereWeAbleToUpdateEverything = false;
163             copyRemainingProperties(ctx, source, newCallback, "class");
164         }
165 
166         if (urlPattern != null) {
167             Content urlPatternContent = newCallback.createContent("urlPattern", NodeTypes.ContentNode.NAME);
168             urlPatternContent.setNodeData("class", "info.magnolia.cms.util.SimpleUrlPattern");
169             urlPatternContent.setNodeData("patternString", urlPattern);
170         }
171 
172     }
173 
174     private String simplifyClassName(String clazz) {
175         return StringUtils.removeEnd(StringUtils.substringAfterLast(clazz, "."), "ClientCallback").toLowerCase();
176     }
177 
178     private void copyStringProperty(Content source, Content target, String propertyName) throws RepositoryException {
179         target.setNodeData(propertyName, source.getNodeData(propertyName).getString());
180     }
181 
182     /**
183      * Checks if the given node has a given property; logs it and continues if so.
184      */
185     private void logAndIgnore(InstallContext ctx, Content source, String propertyName) throws RepositoryException {
186         if (source.hasNodeData(propertyName)) {
187             ctx.warn(source.getHandle() + " has a '" + propertyName + "' property; it is ignored and has been removed.");
188         }
189     }
190 
191     /**
192      * Checks if the given node has other properties than those specified by the ignoredProperties parameter.
193      */
194     private void checkRemainingProperties(InstallContext ctx, Content source, String... ignoredProperties) {
195         final Set<String> ignoredPropsSet = Sets.newHashSet(ignoredProperties);
196         final Collection<NodeData> allProps = source.getNodeDataCollection();
197         final Iterable<String> allPropsNames = Iterables.transform(allProps, new Function<NodeData, String>() {
198             @Override
199             public String apply(NodeData from) {
200                 return from.getName();
201             }
202         });
203         final Iterable<String> remaining = Iterables.filter(allPropsNames, Predicates.not(Predicates.in(ignoredPropsSet)));
204         if (!Iterables.isEmpty(remaining)) {
205             log.warn("{} has the following unknown properties which were not copied: {}", source.getHandle(), remaining);
206             wereWeAbleToUpdateEverything = false;
207         }
208     }
209 
210     private void copyRemainingProperties(InstallContext ctx, Content source, Content target, String... ignoredProperties) throws RepositoryException {
211         final Collection<NodeData> existingProps = source.getNodeDataCollection();
212         for (NodeData prop : existingProps) {
213             if (ArrayUtils.contains(ignoredProperties, prop.getName())) {
214                 continue;
215             }
216             copyStringProperty(source, target, prop.getName());
217         }
218     }
219 
220 }