View Javadoc

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