View Javadoc

1   /**
2    * This file Copyright (c) 2003-2011 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.module.exchangesimple;
35  
36  import info.magnolia.cms.beans.runtime.Document;
37  import info.magnolia.cms.beans.runtime.MultipartForm;
38  import info.magnolia.cms.core.Content;
39  import info.magnolia.cms.core.HierarchyManager;
40  import info.magnolia.cms.core.ItemType;
41  import info.magnolia.cms.core.MgnlNodeType;
42  import info.magnolia.cms.core.NodeData;
43  import info.magnolia.cms.core.SystemProperty;
44  import info.magnolia.cms.exchange.ExchangeException;
45  import info.magnolia.cms.filters.AbstractMgnlFilter;
46  import info.magnolia.cms.security.AccessDeniedException;
47  import info.magnolia.cms.security.MgnlKeyPair;
48  import info.magnolia.cms.security.Permission;
49  import info.magnolia.cms.security.PermissionUtil;
50  import info.magnolia.cms.security.SecurityUtil;
51  import info.magnolia.cms.util.ContentUtil;
52  import info.magnolia.cms.util.Rule;
53  import info.magnolia.cms.util.RuleBasedContentFilter;
54  import info.magnolia.context.MgnlContext;
55  import info.magnolia.context.SystemContext;
56  
57  import java.io.IOException;
58  import java.io.InputStream;
59  import java.security.DigestInputStream;
60  import java.security.InvalidParameterException;
61  import java.security.MessageDigest;
62  import java.security.NoSuchAlgorithmException;
63  import java.util.Iterator;
64  import java.util.List;
65  import java.util.zip.GZIPInputStream;
66  
67  import javax.jcr.ImportUUIDBehavior;
68  import javax.jcr.ItemNotFoundException;
69  import javax.jcr.Node;
70  import javax.jcr.PathNotFoundException;
71  import javax.jcr.Property;
72  import javax.jcr.PropertyType;
73  import javax.jcr.RepositoryException;
74  import javax.jcr.Session;
75  import javax.jcr.UnsupportedRepositoryOperationException;
76  import javax.jcr.lock.LockException;
77  import javax.servlet.FilterChain;
78  import javax.servlet.ServletException;
79  import javax.servlet.http.HttpServletRequest;
80  import javax.servlet.http.HttpServletResponse;
81  import javax.servlet.http.HttpSession;
82  
83  import org.apache.commons.io.IOUtils;
84  import org.apache.commons.lang.StringUtils;
85  import org.jdom.Element;
86  import org.jdom.JDOMException;
87  import org.jdom.input.SAXBuilder;
88  import org.safehaus.uuid.UUIDGenerator;
89  import org.slf4j.Logger;
90  import org.slf4j.LoggerFactory;
91  
92  import com.google.inject.Inject;
93  
94  /**
95   * This filter receives activation requests from another instance and applies them.
96   *
97   * @author Sameer Charles
98   * $Id$
99   */
100 public class ReceiveFilter extends AbstractMgnlFilter {
101 
102     private static final Logger log = LoggerFactory.getLogger(ReceiveFilter.class);
103 
104     public static final String SYSTEM_REPO = "mgnlSystem";
105 
106     private int unlockRetries = 10;
107 
108     private int retryWait = 2;
109 
110     private final ExchangeSimpleModule module;
111     private final MessageDigest md;
112 
113 
114     @Inject
115     public ReceiveFilter(ExchangeSimpleModule module) {
116         this.module = module;
117         try {
118             md = MessageDigest.getInstance("MD5");
119         } catch (NoSuchAlgorithmException e) {
120             throw new SecurityException("In order to proceed with activation please run Magnolia CMS using Java version with MD5 support.", e);
121         }
122     }
123 
124     public int getUnlockRetries() {
125         return unlockRetries;
126     }
127 
128     public void setUnlockRetries(int unlockRetries) {
129         this.unlockRetries = unlockRetries;
130     }
131 
132     public long getRetryWait() {
133         return retryWait;
134     }
135 
136     public void setRetryWait(int retryWait) {
137         this.retryWait = retryWait;
138     }
139 
140     @Override
141     public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
142         String statusMessage = "";
143         String status = "";
144         String result = null;
145         try {
146             final String utf8AuthorStatus = request.getHeader(BaseSyndicatorImpl.UTF8_STATUS);
147             // null check first to make sure we do not break activation from older versions w/o this flag
148             if (utf8AuthorStatus != null && (Boolean.parseBoolean(utf8AuthorStatus) != SystemProperty.getBooleanProperty(SystemProperty.MAGNOLIA_UTF8_ENABLED))) {
149                 throw new UnsupportedOperationException("Activation between instances with different UTF-8 setting is not supported.");
150             }
151             final String action = request.getHeader(BaseSyndicatorImpl.ACTION);
152             if (action == null) {
153                 throw new InvalidParameterException("Activation action must be set for each activation request.");
154             }
155 
156             // verify the author ... if not trusted yet, but no exception thrown, then we attempt to establish trust
157             if (!isAuthorAuthenticated(request, response)) {
158                 status = BaseSyndicatorImpl.ACTIVATION_HANDSHAKE;
159                 setResponseHeaders(response, statusMessage, status, result);
160                 return;
161             }
162             // we do not lock the content on handshake requests
163             applyLock(request);
164         }catch (Throwable e) {
165             log.error(e.getMessage(), e);
166             // we can only rely on the exception's actual message to give something back to the user here.
167             statusMessage = StringUtils.defaultIfEmpty(e.getMessage(), e.getClass().getSimpleName());
168             status = BaseSyndicatorImpl.ACTIVATION_FAILED;
169             setResponseHeaders(response, statusMessage, status, result);
170             return;
171         }
172 
173         try{
174             result = receive(request);
175             status = BaseSyndicatorImpl.ACTIVATION_SUCCESSFUL;
176         }
177         catch (OutOfMemoryError e) {
178             Runtime rt = Runtime.getRuntime();
179             log.error("---------\nOutOfMemoryError caught during activation. Total memory = "
180                     + rt.totalMemory()
181                     + ", free memory = "
182                     + rt.freeMemory()
183                     + "\n---------");
184             statusMessage = e.getMessage();
185             status = BaseSyndicatorImpl.ACTIVATION_FAILED;
186         }
187         catch (PathNotFoundException e) {
188             // this should not happen. PNFE should be already caught and wrapped in ExchangeEx
189             log.error(e.getMessage(), e);
190             statusMessage = "Parent not found (not yet activated): " + e.getMessage();
191             status = BaseSyndicatorImpl.ACTIVATION_FAILED;
192         } catch (ExchangeException e) {
193             log.debug(e.getMessage(), e);
194             statusMessage = e.getMessage();
195             status = BaseSyndicatorImpl.ACTIVATION_FAILED;
196         } catch (Throwable e) {
197             log.error(e.getMessage(), e);
198             // we can only rely on the exception's actual message to give something back to the user here.
199             statusMessage = StringUtils.defaultIfEmpty(e.getMessage(), e.getClass().getSimpleName());
200             status = BaseSyndicatorImpl.ACTIVATION_FAILED;
201         }
202         finally {
203             cleanUp(request, status);
204             setResponseHeaders(response, statusMessage, status, result);
205         }
206     }
207 
208     protected boolean isAuthorAuthenticated(HttpServletRequest request, HttpServletResponse response) throws NoSuchAlgorithmException, ExchangeException {
209         if (SecurityUtil.getPublicKey() == null) {
210             if (module.getTempKeys() == null) {
211                 // no temp keys set or module reset wiped them out
212                 MgnlKeyPair tempKeys = SecurityUtil.generateKeyPair(module.getActivationKeyLength());
213                 // we use a temp key to encrypt authors public key for transport ... intercepting this key will not anyone allow to decrypt public key sent by the author
214                 response.addHeader(BaseSyndicatorImpl.ACTIVATION_AUTH, tempKeys.getPublicKey());
215                 module.setTempKeys(tempKeys);
216                 return false;
217             } else {
218                 try {
219                     // we have temp keys so we expect that this time around we are getting the public key to store
220                     String authorsPublicKeyEncryptedByTempPublicKey = request.getHeader(BaseSyndicatorImpl.ACTIVATION_AUTH_KEY);
221                     // use our private key to decrypt
222                     String publicKey = SecurityUtil.decrypt(authorsPublicKeyEncryptedByTempPublicKey, module.getTempKeys().getPrivateKey());
223                     if (StringUtils.isNotBlank(publicKey)) {
224                         String authString = SecurityUtil.decrypt(request.getHeader(BaseSyndicatorImpl.ACTIVATION_AUTH), publicKey);
225                         String[] auth = authString.split(";");
226                         checkTimestamp(auth);
227                         // no private key for public
228                         // TODO: what about chain of instances? should we not store 2 sets - one generated by the instance (and possibly passed on) and one (public key only) received by the instance?
229                         SecurityUtil.updateKeys(new MgnlKeyPair(null, publicKey));
230                     }
231                 } finally {
232                     // cleanup temp keys no matter what
233                     module.setTempKeys(null);
234                 }
235                 if (SecurityUtil.getPublicKey() == null) {
236                     // we are too fast and trying before observation had a chance to kick in
237                     try {
238                         Thread.sleep(3000);
239                     } catch (InterruptedException e) {
240                         Thread.currentThread().interrupt();
241                     }
242                     if (SecurityUtil.getPublicKey() == null) {
243                         throw new ExchangeException("Failed to negotiate encryption key between author and public instance. Please try again later or contact admin if error persists.");
244                     }
245                 }
246             }
247         }
248         return true;
249     }
250 
251     protected void setResponseHeaders(HttpServletResponse response, String statusMessage, String status, String result) {
252         response.setHeader(BaseSyndicatorImpl.ACTIVATION_ATTRIBUTE_STATUS, status);
253         response.setHeader(BaseSyndicatorImpl.ACTIVATION_ATTRIBUTE_MESSAGE, statusMessage);
254     }
255 
256     /**
257      * handle activate or deactivate request.
258      * @param request
259      * @throws Exception if fails to update
260      */
261     protected synchronized String receive(HttpServletRequest request) throws Exception {
262         String action = request.getHeader(BaseSyndicatorImpl.ACTION);
263         log.debug("action: " + action);
264 
265         String[] auth = checkAuthenticated(request);
266 
267         String user = auth[1];
268 
269         String resourcesmd5 = auth[2];
270 
271         // TODO : this variable is used in log messages to identify the instance - but its content is really the folder name into which Magnolia was deployed, which is irrelevant.
272         String webapp = getWebappName();
273 
274         if (action.equalsIgnoreCase(BaseSyndicatorImpl.ACTIVATE)) {
275             String name = update(request, resourcesmd5);
276             // Everything went well
277             log.info("User {} successfuly activated {} on {}.", new Object[] { user, name, webapp });
278         }
279         else if (action.equalsIgnoreCase(BaseSyndicatorImpl.DEACTIVATE)) {
280             String name = remove(request, resourcesmd5);
281             // Everything went well
282             log.info("User {} succeessfuly deactivated {} on {}.", new Object[] { user, name, webapp });
283         }
284         else {
285             throw new UnsupportedOperationException("Method not supported : " + action);
286         }
287         return null;
288     }
289 
290     protected String[] checkAuthenticated(HttpServletRequest request) throws ExchangeException {
291         String encrypted = request.getHeader(BaseSyndicatorImpl.ACTIVATION_AUTH);
292         if (StringUtils.isBlank(encrypted)) {
293             log.debug("Attempt to access activation URL w/o proper information in request. Ignoring silently.");
294             throw new ExchangeException();
295         }
296 
297         String decrypted = SecurityUtil.decrypt(encrypted);
298         if (StringUtils.isBlank(decrypted)) {
299             throw new SecurityException("Handshake information for activation was incorrect. Someone attempted to impersonate author instance. Incoming request was from " + request.getRemoteAddr());
300         }
301 
302         String[] auth = decrypted.split(";");
303 
304         // timestamp;user;resourcemd;optional_encrypted_public_key
305         if (auth.length != 3) {
306             throw new SecurityException("Handshake information for activation was incorrect. Someone attempted to impersonate author instance. Incoming request was from " + request.getRemoteAddr());
307         }
308         // first part is a timestamp
309         checkTimestamp(auth);
310         return auth;
311     }
312 
313     private void checkTimestamp(String[] auth) {
314         long timestamp = System.currentTimeMillis();
315         long authorTimestamp = 0;
316         try {
317             authorTimestamp = Long.parseLong(auth[0]);
318         } catch (NumberFormatException e) {
319             throw new SecurityException("Handshake information for activation was incorrect. This might be an attempt to replay earlier activation request.");
320         }
321         if (Math.abs(timestamp - authorTimestamp) > module.getActivationDelayTolerance()) {
322             throw new SecurityException("Activation refused due to request arriving too late or time not synched between author and public instance. Please contact your administrator to ensure server times are synced or the tolerance is set high enough to counter the differences.");
323         }
324     }
325 
326     protected String getWebappName() {
327         return SystemProperty.getProperty(SystemProperty.MAGNOLIA_WEBAPP);
328     }
329 
330     /**
331      * @deprecated since 4.5. This method is not used anymore and there is no replacement. Authentication of activation is now handled by exchange of info encrypted by PPKey.
332      */
333     @Deprecated
334     protected String getUser(HttpServletRequest request) {
335         return null;
336     }
337 
338     /**
339      * handle update (activate) request.
340      * 
341      * @param request
342      *            incoming reuqest
343      * @param resourcesmd5
344      *            signature confirming validity of resource file
345      * @throws Exception
346      *             if fails to update
347      */
348     protected synchronized String update(HttpServletRequest request, String resourcesmd5) throws Exception {
349         MultipartForm data = MgnlContext.getPostedForm();
350         if (null != data) {
351             String newParentPath = this.getParentPath(request);
352             String resourceFileName = request.getHeader(BaseSyndicatorImpl.RESOURCE_MAPPING_FILE);
353             HierarchyManager hm = getHierarchyManager(request);
354             Element rootElement = getImportedContentRoot(data, resourceFileName, resourcesmd5);
355             Element topContentElement = rootElement.getChild(BaseSyndicatorImpl.RESOURCE_MAPPING_FILE_ELEMENT);
356             Content content = null;
357             try {
358                 String uuid = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_UUID_ATTRIBUTE);
359                 content = hm.getContentByUUID(uuid);
360                 // move content to new location if necessary.
361                 newParentPath = handleMovedContent(newParentPath, hm, topContentElement, content);
362                 handleChildren(request, content);
363                 this.importOnExisting(topContentElement, data, hm, content);
364             }
365             catch (ItemNotFoundException e) {
366                 // new content
367                 importFresh(topContentElement, data, hm, newParentPath);
368             }
369 
370             return orderImportedNode(newParentPath, hm, rootElement, topContentElement);
371         }
372         return null;
373     }
374 
375     protected Element getImportedContentRoot(MultipartForm data, String resourceFileName, String resourcesmd5) throws JDOMException, IOException {
376         Document resourceDocument = data.getDocument(resourceFileName);
377         SAXBuilder builder = new SAXBuilder();
378         InputStream documentInputStream = new DigestInputStream(resourceDocument.getStream(), md);
379         org.jdom.Document jdomDocument = builder.build(documentInputStream);
380         IOUtils.closeQuietly(documentInputStream);
381         String sign = SecurityUtil.byteArrayToHex(md.digest());
382         if (!resourcesmd5.equals(sign)) {
383             throw new SecurityException("Signature of received resource (" + sign + ") doesn't match expected signature (" + resourcesmd5 + "). This might mean that the activation operation have been intercepted by a third party and content have been modified during transfer.");
384         }
385 
386         return jdomDocument.getRootElement();
387     }
388 
389     protected void handleChildren(HttpServletRequest request, Content content) {
390         String ruleString = request.getHeader(BaseSyndicatorImpl.CONTENT_FILTER_RULE);
391         Rule rule = new Rule(ruleString, ",");
392         RuleBasedContentFilter filter = new RuleBasedContentFilter(rule);
393         // remove all child nodes
394         this.removeChildren(content, filter);
395     }
396 
397     protected String handleMovedContent(String newParentPath, HierarchyManager hm, Element topContentElement, Content content) throws RepositoryException {
398         String currentParentPath = content.getHandle();
399         currentParentPath = currentParentPath.substring(0, currentParentPath.lastIndexOf('/'));
400         String newName = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
401         if (!newParentPath.endsWith("/")) {
402             newParentPath += "/";
403         }
404         if (!currentParentPath.endsWith("/")) {
405             currentParentPath += "/";
406         }
407         if (!newParentPath.equals(currentParentPath) || !content.getName().equals(newName)) {
408             log.info("Moving content from {} to {} due to activation request.", new Object[] { content.getHandle(), newParentPath  + newName});
409             hm.moveTo(content.getHandle(), newParentPath + newName);
410         }
411         return newParentPath;
412     }
413 
414     protected String orderImportedNode(String newParentPath, HierarchyManager hm, Element rootElement, Element topContentElement) throws RepositoryException {
415         String name;
416         // order imported node
417         name = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
418         Content parent = hm.getContent(newParentPath);
419         List siblings = rootElement.getChild(BaseSyndicatorImpl.SIBLINGS_ROOT_ELEMENT).getChildren(BaseSyndicatorImpl.SIBLINGS_ELEMENT);
420         Iterator siblingsIterator = siblings.iterator();
421         while (siblingsIterator.hasNext()) {
422             Element sibling = (Element) siblingsIterator.next();
423             // check for existence and order
424             try {
425                 String siblingUUID = sibling.getAttributeValue(BaseSyndicatorImpl.SIBLING_UUID);
426                 Content beforeContent = hm.getContentByUUID(siblingUUID);
427                 log.debug("Ordering {} before {}", name, beforeContent.getName());
428                 order(parent, name, beforeContent.getName());
429                 break;
430             } catch (ItemNotFoundException e) {
431                 // ignore
432             } catch (RepositoryException re) {
433                 if (log.isDebugEnabled()) {
434                     log.debug("Failed to order node", re);
435                 } else {
436                     log.warn("Failed to order node");
437                 }
438             }
439         }
440 
441         // ensure the no sibling nodes are at the end ... since move is not activated immediately it is sometimes necessary to preserve right order
442         if (siblings.isEmpty()) {
443             order(parent, name, null);
444         }
445         return name;
446     }
447 
448 
449     protected void order(Content parent, String name, String orderBefore) throws RepositoryException {
450         try {
451             parent.orderBefore(name, orderBefore);
452         } catch (UnsupportedRepositoryOperationException e) {
453             // since not all types support ordering we should not enforce it, but only log the error
454             log.warn("Failed to order unorderable content {} at {} due to {}", new Object[] {name, parent.getHandle(), e.getMessage()});
455         }
456         parent.save();
457     }
458 
459     /**
460      * Copy all properties from source to destination (by cleaning the old properties).
461      * @param source the content node to be copied
462      * @param destination the destination node
463      */
464     protected synchronized void copyProperties(Content source, Content destination) throws RepositoryException {
465         // first remove all existing properties at the destination
466         // will be different with incremental activation
467         Iterator nodeDataIterator = destination.getNodeDataCollection().iterator();
468         while (nodeDataIterator.hasNext()) {
469             NodeData nodeData = (NodeData) nodeDataIterator.next();
470             // Ignore binary types, since these are sub nodes and already taken care of while
471             // importing sub resources
472             if (nodeData.getType() != PropertyType.BINARY) {
473                 nodeData.delete();
474             }
475         }
476 
477         // copy all properties
478         Node destinationNode = destination.getJCRNode();
479         nodeDataIterator = source.getNodeDataCollection().iterator();
480         while (nodeDataIterator.hasNext()) {
481             NodeData nodeData = (NodeData) nodeDataIterator.next();
482             Property property = nodeData.getJCRProperty();
483             if (property.getDefinition().isMultiple()) {
484                 if (destination.isGranted(Permission.WRITE)) {
485                     destinationNode.setProperty(nodeData.getName(), property.getValues());
486                 }
487                 else {
488                     throw new AccessDeniedException("User not allowed to " + Permission.PERMISSION_NAME_WRITE + " at [" + nodeData.getHandle() + "]");
489                 }
490             }
491             else {
492                 destination.createNodeData(nodeData.getName(), nodeData.getValue());
493             }
494         }
495     }
496 
497     /**
498      * remove children.
499      * @param content whose children to be deleted
500      * @param filter content filter
501      */
502     protected synchronized void removeChildren(Content content, Content.ContentFilter filter) {
503         Iterator children = content.getChildren(filter).iterator();
504         // remove sub nodes using the same filter used by the sender to collect
505         // this will make sure there is no existing nodes of the same type
506         while (children.hasNext()) {
507             Content child = (Content) children.next();
508             try {
509                 child.delete();
510             }
511             catch (Exception e) {
512                 log.error("Failed to remove " + child.getHandle() + " | " + e.getMessage());
513             }
514         }
515     }
516 
517     /**
518      * import on non existing tree.
519      * @param topContentElement
520      * @param data
521      * @param hierarchyManager
522      * @param parentPath
523      * @throws ExchangeException
524      * @throws RepositoryException
525      */
526     protected synchronized void importFresh(Element topContentElement, MultipartForm data, HierarchyManager hierarchyManager, String parentPath) throws ExchangeException, RepositoryException {
527         // content might still exists under different uuid if it was auto generated. Check the path first, if exists, then remove it before activating new content into same path
528         // TODO: handle same name siblings!
529         String path = parentPath + (parentPath.endsWith("/") ? "" : "/") + topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
530         if (hierarchyManager.isExist(path)) {
531             log.warn("Replacing {} due to name collision (but different UUIDs.). This operation could not be rolled back in case of failure and you need to reactivate the page manually.", path);
532             hierarchyManager.delete(path);
533         }
534         try {
535             importResource(data, topContentElement, hierarchyManager, parentPath);
536             hierarchyManager.save();
537         } catch (PathNotFoundException e) {
538             final String  message = "Parent content " + parentPath + " is not yet activated or you do not have write access to it. Please activate the parent content before activating children and ensure you have appropriate rights"; // .. on XXX will be appended to the error message by syndicator on the author instance
539             // this is not a system error so there should not be a need to log the exception all the time.
540             log.debug(message, e);
541             hierarchyManager.refresh(false); // revert all transient changes made in this session till now.
542             throw new ExchangeException(message);
543         } catch (Exception e) {
544             final String message = "Activation failed | " + e.getMessage();
545             log.error("Exception caught", e);
546             hierarchyManager.refresh(false); // revert all transient changes made in this session till now.
547             throw new ExchangeException(message);
548         }
549     }
550 
551     /**
552      * import on existing content, making sure that content which is not sent stays as is.
553      * @param topContentElement
554      * @param data
555      * @param hierarchyManager
556      * @param existingContent
557      * @throws ExchangeException
558      * @throws RepositoryException
559      */
560     protected synchronized void importOnExisting(Element topContentElement, MultipartForm data,
561             final HierarchyManager hierarchyManager, Content existingContent) throws ExchangeException, RepositoryException {
562         final Iterator<Content> fileListIterator = topContentElement.getChildren(BaseSyndicatorImpl.RESOURCE_MAPPING_FILE_ELEMENT).iterator();
563         final String uuid = UUIDGenerator.getInstance().generateTimeBasedUUID().toString();
564         final String handle = existingContent.getHandle();
565         // Can't execute in system context here just get hm from SC and use it for temp node handling
566         final HierarchyManager systemHM = MgnlContext.getSystemContext().getHierarchyManager(SYSTEM_REPO);
567         try {
568             while (fileListIterator.hasNext()) {
569                 Element fileElement = (Element) fileListIterator.next();
570                 importResource(data, fileElement, hierarchyManager, handle);
571             }
572             // use temporary node in mgnlSystem workspace to extract the top level node and copy its properties
573             Content activationTmp = ContentUtil.getOrCreateContent(systemHM.getRoot(), "activation-tmp", ItemType.FOLDER, true);
574             final Content transientNode = activationTmp.createContent(uuid, MgnlNodeType.NT_PAGE);
575             final String transientStoreHandle = transientNode.getHandle();
576             // import properties into transientStore
577             final String fileName = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_ID_ATTRIBUTE);
578             final String md5 = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_MD_ATTRIBUTE);
579             final InputStream inputStream = new DigestInputStream(new GZIPInputStream(data.getDocument(fileName).getStream()), md);
580             // need to import in system context
581             systemHM.getWorkspace().getSession().importXML(transientStoreHandle, inputStream, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW);
582             IOUtils.closeQuietly(inputStream);
583             final String calculatedMD5 = SecurityUtil.byteArrayToHex(md.digest());
584             if (!calculatedMD5.equals(md5)) {
585                 throw new SecurityException(fileName + " signature is not valid. Resource might have been modified in transit.");
586             }
587             // copy properties from transient store to existing content
588             Content tmpContent = transientNode.getContent(topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE));
589             copyProperties(tmpContent, existingContent);
590             systemHM.delete(transientStoreHandle);
591             hierarchyManager.save();
592             systemHM.save();
593         } catch (Exception e) {
594             // revert all transient changes made in this session till now.
595             hierarchyManager.refresh(false);
596             systemHM.refresh(false);
597 
598             log.error("Exception caught", e);
599             throw new ExchangeException("Activation failed : " + e.getMessage());
600         }
601     }
602 
603     /**
604      * import documents.
605      * @param data as sent
606      * @param resourceElement parent file element
607      * @param hm
608      * @param parentPath Path to the node parent
609      * @throws Exception
610      */
611     protected synchronized void importResource(MultipartForm data, Element resourceElement, HierarchyManager hm, String parentPath) throws Exception {
612 
613         // throws an exception in case you don't have the permission
614         PermissionUtil.isGranted(hm.getWorkspace().getSession(), parentPath, Session.ACTION_ADD_NODE);
615 
616         final String name = resourceElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
617         final String fileName = resourceElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_ID_ATTRIBUTE);
618         final String md5 = resourceElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_MD_ATTRIBUTE);
619         // do actual import
620         final InputStream inputStream = new DigestInputStream(new GZIPInputStream(data.getDocument(fileName).getStream()), md);
621         log.debug("Importing {} into parent path {}", new Object[] {name, parentPath});
622         hm.getWorkspace().getSession().importXML(parentPath, inputStream, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
623         IOUtils.closeQuietly(inputStream);
624         final String calculatedMD5 = SecurityUtil.byteArrayToHex(md.digest());
625         if (!calculatedMD5.equals(md5)) {
626             throw new SecurityException(fileName + " signature is not valid. Resource might have been modified in transit. Expected signature:" + md5 + ", actual signature found: " + calculatedMD5);
627         }
628         Iterator fileListIterator = resourceElement.getChildren(BaseSyndicatorImpl.RESOURCE_MAPPING_FILE_ELEMENT).iterator();
629         // parent path
630         try {
631             parentPath = hm.getContentByUUID(resourceElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_UUID_ATTRIBUTE)).getHandle();
632         } catch (ItemNotFoundException e) {
633             // non referencable content like meta data ...
634             // FYI: if we ever have non referencable same name sibling content the trouble will be here with child content being mixed
635             parentPath = StringUtils.removeEnd(parentPath, "/") + "/" + name;
636         }
637         while (fileListIterator.hasNext()) {
638             Element fileElement = (Element) fileListIterator.next();
639             importResource(data, fileElement, hm, parentPath);
640         }
641     }
642 
643     /**
644      * Deletes (de-activate) the content specified by the request.
645      * @param request
646      * @throws Exception if fails to update
647      */
648     protected synchronized String remove(HttpServletRequest request, String md5) throws Exception {
649 
650         if (!md5.equals(SecurityUtil.byteArrayToHex(md.digest(request.getHeader(BaseSyndicatorImpl.NODE_UUID).getBytes())))) {
651             throw new SecurityException("Signature of resource doesn't match. This seems like malicious attempt to delete content. Request was issued from " + request.getRemoteAddr());
652         }
653         HierarchyManager hm = getHierarchyManager(request);
654         String handle = null;
655         try {
656             Content node = this.getNode(request);
657             handle = node.getHandle();
658             hm.delete(handle);
659             hm.save();
660         } catch (ItemNotFoundException e) {
661             log.debug("Unable to delete node", e);
662         }
663         return handle;
664     }
665 
666     /**
667      * get hierarchy manager.
668      * @param request
669      * @throws ExchangeException
670      */
671     protected HierarchyManager getHierarchyManager(HttpServletRequest request) throws ExchangeException {
672         String workspaceName = request.getHeader(BaseSyndicatorImpl.WORKSPACE_NAME);
673 
674         if (StringUtils.isEmpty(workspaceName)) {
675             throw new ExchangeException("Repository or workspace name not sent, unable to activate. Workspace: " + workspaceName) ;
676         }
677         SystemContext sysCtx = MgnlContext.getSystemContext();
678         return sysCtx.getHierarchyManager(workspaceName);
679     }
680 
681     /**
682      * cleans temporary store and removes any locks set.
683      *
684      * @param request
685      * @param status
686      */
687     protected void cleanUp(HttpServletRequest request, String status) {
688         if (!BaseSyndicatorImpl.DEACTIVATE.equalsIgnoreCase(request.getHeader(BaseSyndicatorImpl.ACTION))) {
689             MultipartForm data = MgnlContext.getPostedForm();
690             if (null != data) {
691                 Iterator keys = data.getDocuments().keySet().iterator();
692                 while (keys.hasNext()) {
693                     String key = (String) keys.next();
694                     data.getDocument(key).delete();
695                 }
696             }
697             try {
698                 final String parentPath = getParentPath(request);
699                 if (StringUtils.isEmpty(parentPath) || this.getHierarchyManager(request).isExist(parentPath)) {
700                     try {
701                         Content content = this.getNode(request);
702                         if (content.isLocked()) {
703                             content.unlock();
704                         }
705                     }catch (ItemNotFoundException e) {
706                         // ignore - commit of deactivation
707                     }
708                 }
709             } catch (LockException le) {
710                 // either repository does not support locking OR this node never locked
711                 log.debug(le.getMessage(), le);
712             } catch (RepositoryException re) {
713                 // should never come here
714                 log.warn("Exception caught", re);
715             } catch (ExchangeException e) {
716                 // should never come here
717                 log.warn("Exception caught", e);
718             }
719         }
720 
721         // TODO : why is this here ? as far as I can tell, http sessions are never created when reaching this
722         try {
723             HttpSession httpSession = request.getSession(false);
724             if (httpSession != null) {
725                 httpSession.invalidate();
726             }
727         } catch (Throwable t) {
728             // its only a test so just dump
729             log.error("failed to invalidate session", t);
730         }
731     }
732 
733     /**
734      * apply lock.
735      * @param request
736      */
737     protected void applyLock(HttpServletRequest request) throws ExchangeException {
738         try {
739             Content parent = waitForLock(request);
740             if (parent != null) {
741                 // get a new deep lock
742                 parent.lock(true, true);
743             }
744         } catch (LockException le) {
745             // either repository does not support locking OR this node never locked
746             log.debug(le.getMessage());
747         } catch (RepositoryException re) {
748             // will blow fully at later stage
749             log.warn("Exception caught", re);
750         }
751     }
752 
753     /**
754      * Will wait for predefined amount of time and attempt predefined number of times to obtain unlocked content.
755      * 
756      * @param request
757      * @return unlocked content specified by the request or null in case such content doesnt exist
758      * @throws ExchangeException
759      * @throws RepositoryException
760      */
761     protected Content waitForLock(HttpServletRequest request) throws ExchangeException, RepositoryException {
762         int retries = getUnlockRetries();
763         long retryWait = getRetryWait() * 1000;
764         Content content = null;
765         try {
766             content = this.getNode(request);
767             while (content.isLocked() && retries > 0) {
768                 log.info("Content " + content.getHandle() + " is locked. Will retry " + retries + " more times.");
769                 try {
770                     Thread.sleep(retryWait);
771                 } catch (InterruptedException e) {
772                     // Restore the interrupted status
773                     Thread.currentThread().interrupt();
774                 }
775                 retries--;
776                 content = this.getNode(request);
777             }
778             if (content.isLocked()) {
779                 throw new ExchangeException("Content " + content.getHandle() + " was locked while activating " + request.getHeader(BaseSyndicatorImpl.NODE_UUID) + ". This most likely means that content have been at the same time activated by some other user. Please try again and if problem persists contact administrator.");
780             }
781         } catch (ItemNotFoundException e) {
782             // - when deleting new piece of content on the author and mgnl tries to deactivate it on public automatically
783             log.debug("Attempt to lock non existing content {} during (de)activation.", getUUID(request));
784         } catch (PathNotFoundException e) {
785             // - when attempting to activate the content for which parent content have not been yet activated
786             log.debug("Attempt to lock non existing content {}:{} during (de)activation.",getHierarchyManager(request).getName(), getParentPath(request));
787         }
788         return content;
789     }
790 
791     protected Content getNode(HttpServletRequest request) throws ExchangeException, RepositoryException {
792         if (request.getHeader(BaseSyndicatorImpl.PARENT_PATH) != null) {
793             String parentPath = this.getParentPath(request);
794             log.debug("parent path:" + parentPath);
795             return this.getHierarchyManager(request).getContent(parentPath);
796         } else if (!StringUtils.isEmpty(getUUID(request))){
797             log.debug("node uuid:" + request.getHeader(BaseSyndicatorImpl.NODE_UUID));
798             return this.getHierarchyManager(request).getContentByUUID(request.getHeader(BaseSyndicatorImpl.NODE_UUID));
799         } else {
800             throw new ExchangeException("Request is missing mandatory content identifier.");
801         }
802     }
803 
804     protected String getParentPath(HttpServletRequest request) {
805         String parentPath = request.getHeader(BaseSyndicatorImpl.PARENT_PATH);
806         if (StringUtils.isNotEmpty(parentPath)) {
807             return parentPath;
808         }
809         return "";
810     }
811 
812     protected String getUUID(HttpServletRequest request) {
813         String parentPath = request.getHeader(BaseSyndicatorImpl.NODE_UUID);
814         if (StringUtils.isNotEmpty(parentPath)) {
815             return parentPath;
816         }
817         return "";
818     }
819 
820 
821 }