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