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         InputStream documentInputStream = new TeeInputStream(resourceDocument.getStream(), md5);
374         org.jdom.Document jdomDocument = builder.build(documentInputStream);
375         IOUtils.closeQuietly(documentInputStream);
376         
377         String sign = SecurityUtil.getMD5Hex(md5.toByteArray());
378         if (!resourcesmd5.equals(sign)) {
379             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.");
380         }
381 
382         return jdomDocument.getRootElement();
383     }
384 
385     protected void handleChildren(HttpServletRequest request, Content content) {
386         String ruleString = request.getHeader(BaseSyndicatorImpl.CONTENT_FILTER_RULE);
387         Rule rule = new Rule(ruleString, ",");
388         RuleBasedContentFilter filter = new RuleBasedContentFilter(rule);
389         // remove all child nodes
390         this.removeChildren(content, filter);
391     }
392 
393     protected String handleMovedContent(String newParentPath, HierarchyManager hm, Element topContentElement, Content content) throws RepositoryException {
394         String currentParentPath = content.getHandle();
395         currentParentPath = currentParentPath.substring(0, currentParentPath.lastIndexOf('/'));
396         String newName = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
397         if (!newParentPath.endsWith("/")) {
398             newParentPath += "/";
399         }
400         if (!currentParentPath.endsWith("/")) {
401             currentParentPath += "/";
402         }
403         if (!newParentPath.equals(currentParentPath) || !content.getName().equals(newName)) {
404             log.info("Moving content from {} to {} due to activation request.", new Object[] { content.getHandle(), newParentPath  + newName});
405             hm.moveTo(content.getHandle(), newParentPath + newName);
406         }
407         return newParentPath;
408     }
409 
410     protected String orderImportedNode(String newParentPath, HierarchyManager hm, Element rootElement, Element topContentElement) throws RepositoryException {
411         String name;
412         // order imported node
413         name = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
414         Content parent = hm.getContent(newParentPath);
415         List siblings = rootElement.getChild(BaseSyndicatorImpl.SIBLINGS_ROOT_ELEMENT).getChildren(BaseSyndicatorImpl.SIBLINGS_ELEMENT);
416         Iterator siblingsIterator = siblings.iterator();
417         while (siblingsIterator.hasNext()) {
418             Element sibling = (Element) siblingsIterator.next();
419             // check for existence and order
420             try {
421                 String siblingUUID = sibling.getAttributeValue(BaseSyndicatorImpl.SIBLING_UUID);
422                 Content beforeContent = hm.getContentByUUID(siblingUUID);
423                 log.debug("Ordering {} before {}", name, beforeContent.getName());
424                 order(parent, name, beforeContent.getName());
425                 break;
426             } catch (ItemNotFoundException e) {
427                 // ignore
428             } catch (RepositoryException re) {
429                 if (log.isDebugEnabled()) {
430                     log.debug("Failed to order node", re);
431                 } else {
432                     log.warn("Failed to order node");
433                 }
434             }
435         }
436 
437         // ensure the no sibling nodes are at the end ... since move is not activated immediately it is sometimes necessary to preserve right order
438         if (siblings.isEmpty()) {
439             order(parent, name, null);
440         }
441         return name;
442     }
443 
444 
445     protected void order(Content parent, String name, String orderBefore) throws RepositoryException {
446         try {
447             parent.orderBefore(name, orderBefore);
448         } catch (UnsupportedRepositoryOperationException e) {
449             // since not all types support ordering we should not enforce it, but only log the error
450             log.warn("Failed to order unorderable content {} at {} due to {}", new Object[] {name, parent.getHandle(), e.getMessage()});
451         }
452         parent.save();
453     }
454 
455     /**
456      * Copy all properties from source to destination (by cleaning the old properties).
457      * @param source the content node to be copied
458      * @param destination the destination node
459      */
460     protected synchronized void copyProperties(Content source, Content destination) throws RepositoryException {
461         // first remove all existing properties at the destination
462         // will be different with incremental activation
463         Iterator nodeDataIterator = destination.getNodeDataCollection().iterator();
464         while (nodeDataIterator.hasNext()) {
465             NodeData nodeData = (NodeData) nodeDataIterator.next();
466             // Ignore binary types, since these are sub nodes and already taken care of while
467             // importing sub resources
468             if (nodeData.getType() != PropertyType.BINARY) {
469                 nodeData.delete();
470             }
471         }
472 
473         // copy all properties
474         Node destinationNode = destination.getJCRNode();
475         nodeDataIterator = source.getNodeDataCollection().iterator();
476         while (nodeDataIterator.hasNext()) {
477             NodeData nodeData = (NodeData) nodeDataIterator.next();
478             Property property = nodeData.getJCRProperty();
479             if (property.getDefinition().isMultiple()) {
480                 if (destination.isGranted(Permission.WRITE)) {
481                     destinationNode.setProperty(nodeData.getName(), property.getValues());
482                 }
483                 else {
484                     throw new AccessDeniedException("User not allowed to " + Permission.PERMISSION_NAME_WRITE + " at [" + nodeData.getHandle() + "]");
485                 }
486             }
487             else {
488                 destination.createNodeData(nodeData.getName(), nodeData.getValue());
489             }
490         }
491     }
492 
493     /**
494      * remove children.
495      * @param content whose children to be deleted
496      * @param filter content filter
497      */
498     protected synchronized void removeChildren(Content content, Content.ContentFilter filter) {
499         Iterator children = content.getChildren(filter).iterator();
500         // remove sub nodes using the same filter used by the sender to collect
501         // this will make sure there is no existing nodes of the same type
502         while (children.hasNext()) {
503             Content child = (Content) children.next();
504             try {
505                 child.delete();
506             }
507             catch (Exception e) {
508                 log.error("Failed to remove " + child.getHandle() + " | " + e.getMessage());
509             }
510         }
511     }
512 
513     /**
514      * import on non existing tree.
515      * @param topContentElement
516      * @param data
517      * @param hierarchyManager
518      * @param parentPath
519      * @throws ExchangeException
520      * @throws RepositoryException
521      */
522     protected synchronized void importFresh(Element topContentElement, MultipartForm data, HierarchyManager hierarchyManager, String parentPath) throws ExchangeException, RepositoryException {
523         // 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
524         // TODO: handle same name siblings!
525         String path = parentPath + (parentPath.endsWith("/") ? "" : "/") + topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
526         if (hierarchyManager.isExist(path)) {
527             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);
528             hierarchyManager.delete(path);
529         }
530         try {
531             importResource(data, topContentElement, hierarchyManager, parentPath);
532             hierarchyManager.save();
533         } catch (PathNotFoundException e) {
534             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
535             // this is not a system error so there should not be a need to log the exception all the time.
536             log.debug(message, e);
537             hierarchyManager.refresh(false); // revert all transient changes made in this session till now.
538             throw new ExchangeException(message);
539         } catch (Exception e) {
540             final String message = "Activation failed | " + e.getMessage();
541             log.error("Exception caught", e);
542             hierarchyManager.refresh(false); // revert all transient changes made in this session till now.
543             throw new ExchangeException(message);
544         }
545     }
546 
547     /**
548      * import on existing content, making sure that content which is not sent stays as is.
549      * @param topContentElement
550      * @param data
551      * @param hierarchyManager
552      * @param existingContent
553      * @throws ExchangeException
554      * @throws RepositoryException
555      */
556     protected synchronized void importOnExisting(Element topContentElement, MultipartForm data,
557             final HierarchyManager hierarchyManager, Content existingContent) throws ExchangeException, RepositoryException {
558         final Iterator<Content> fileListIterator = topContentElement.getChildren(BaseSyndicatorImpl.RESOURCE_MAPPING_FILE_ELEMENT).iterator();
559         final String uuid = UUIDGenerator.getInstance().generateTimeBasedUUID().toString();
560         final String handle = existingContent.getHandle();
561         // Can't execute in system context here just get hm from SC and use it for temp node handling
562         final HierarchyManager systemHM = MgnlContext.getSystemContext().getHierarchyManager(SYSTEM_REPO);
563         try {
564             while (fileListIterator.hasNext()) {
565                 Element fileElement = (Element) fileListIterator.next();
566                 importResource(data, fileElement, hierarchyManager, handle);
567             }
568             // use temporary node in mgnlSystem workspace to extract the top level node and copy its properties
569             Content activationTmp = ContentUtil.getOrCreateContent(systemHM.getRoot(), "activation-tmp", ItemType.FOLDER, true);
570             final Content transientNode = activationTmp.createContent(uuid, MgnlNodeType.NT_PAGE);
571             final String transientStoreHandle = transientNode.getHandle();
572             // import properties into transientStore
573             final ByteArrayOutputStream md5 = new ByteArrayOutputStream();
574             final String fileName = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_ID_ATTRIBUTE);
575             final String expectedMD5 = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_MD_ATTRIBUTE);
576             final InputStream inputStream = new TeeInputStream(new GZIPInputStream(data.getDocument(fileName).getStream()), md5);
577             // need to import in system context
578             systemHM.getWorkspace().getSession().importXML(transientStoreHandle, inputStream, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW);
579             IOUtils.closeQuietly(inputStream);
580             final String calculatedMD5 = SecurityUtil.getMD5Hex(md5.toByteArray());
581             
582             if (!calculatedMD5.equals(expectedMD5)) {
583                 throw new SecurityException(fileName + " signature is not valid. Resource might have been modified in transit.");
584             }
585             // copy properties from transient store to existing content
586             Content tmpContent = transientNode.getContent(topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE));
587             copyProperties(tmpContent, existingContent);
588             systemHM.delete(transientStoreHandle);
589             hierarchyManager.save();
590             systemHM.save();
591         } catch (Exception e) {
592             // revert all transient changes made in this session till now.
593             hierarchyManager.refresh(false);
594             systemHM.refresh(false);
595 
596             log.error("Exception caught", e);
597             throw new ExchangeException("Activation failed : " + e.getMessage());
598         }
599     }
600 
601     /**
602      * import documents.
603      * @param data as sent
604      * @param resourceElement parent file element
605      * @param hm
606      * @param parentPath Path to the node parent
607      * @throws Exception
608      */
609     protected synchronized void importResource(MultipartForm data, Element resourceElement, HierarchyManager hm, String parentPath) throws Exception {
610 
611         // throws an exception in case you don't have the permission
612         PermissionUtil.isGranted(hm.getWorkspace().getSession(), parentPath, Session.ACTION_ADD_NODE);
613 
614         final String name = resourceElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
615         final String fileName = resourceElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_ID_ATTRIBUTE);
616         final String expectedMD5 = resourceElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_MD_ATTRIBUTE);
617         // do actual import
618         final ByteArrayOutputStream md5 = new ByteArrayOutputStream();
619         final InputStream inputStream = new TeeInputStream(new GZIPInputStream(data.getDocument(fileName).getStream()), md5);
620         log.debug("Importing {} into parent path {}", new Object[] {name, parentPath});
621         hm.getWorkspace().getSession().importXML(parentPath, inputStream, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
622         IOUtils.closeQuietly(inputStream);
623         final String calculatedMD5 = SecurityUtil.getMD5Hex(md5.toByteArray());
624         if (!calculatedMD5.equals(expectedMD5)) {
625             throw new SecurityException(fileName + " signature is not valid. Resource might have been modified in transit. Expected signature:" + expectedMD5 + ", actual signature found: " + calculatedMD5);
626         }
627         Iterator fileListIterator = resourceElement.getChildren(BaseSyndicatorImpl.RESOURCE_MAPPING_FILE_ELEMENT).iterator();
628         // parent path
629         try {
630             parentPath = hm.getContentByUUID(resourceElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_UUID_ATTRIBUTE)).getHandle();
631         } catch (ItemNotFoundException e) {
632             // non referencable content like meta data ...
633             // FYI: if we ever have non referencable same name sibling content the trouble will be here with child content being mixed
634             parentPath = StringUtils.removeEnd(parentPath, "/") + "/" + name;
635         }
636         while (fileListIterator.hasNext()) {
637             Element fileElement = (Element) fileListIterator.next();
638             importResource(data, fileElement, hm, parentPath);
639         }
640     }
641 
642     /**
643      * Deletes (de-activate) the content specified by the request.
644      * @param request
645      * @throws Exception if fails to update
646      */
647     protected synchronized String remove(HttpServletRequest request, String md5) throws Exception {
648 
649         if (!md5.equals(SecurityUtil.getMD5Hex(request.getHeader(BaseSyndicatorImpl.NODE_UUID)))) {
650             throw new SecurityException("Signature of resource doesn't match. This seems like malicious attempt to delete content. Request was issued from " + request.getRemoteAddr());
651         }
652         HierarchyManager hm = getHierarchyManager(request);
653         String handle = null;
654         try {
655             Content node = this.getNode(request);
656             handle = node.getHandle();
657             hm.delete(handle);
658             hm.save();
659         } catch (ItemNotFoundException e) {
660             log.debug("Unable to delete node", e);
661         }
662         return handle;
663     }
664 
665     /**
666      * get hierarchy manager.
667      * @param request
668      * @throws ExchangeException
669      */
670     protected HierarchyManager getHierarchyManager(HttpServletRequest request) throws ExchangeException {
671         String workspaceName = request.getHeader(BaseSyndicatorImpl.WORKSPACE_NAME);
672 
673         if (StringUtils.isEmpty(workspaceName)) {
674             throw new ExchangeException("Repository or workspace name not sent, unable to activate. Workspace: " + workspaceName) ;
675         }
676         SystemContext sysCtx = MgnlContext.getSystemContext();
677         return sysCtx.getHierarchyManager(workspaceName);
678     }
679 
680     /**
681      * cleans temporary store and removes any locks set.
682      *
683      * @param request
684      * @param status
685      */
686     protected void cleanUp(HttpServletRequest request, String status) {
687         if (!BaseSyndicatorImpl.DEACTIVATE.equalsIgnoreCase(request.getHeader(BaseSyndicatorImpl.ACTION))) {
688             MultipartForm data = MgnlContext.getPostedForm();
689             if (null != data) {
690                 Iterator keys = data.getDocuments().keySet().iterator();
691                 while (keys.hasNext()) {
692                     String key = (String) keys.next();
693                     data.getDocument(key).delete();
694                 }
695             }
696             try {
697                 final String parentPath = getParentPath(request);
698                 if (StringUtils.isEmpty(parentPath) || this.getHierarchyManager(request).isExist(parentPath)) {
699                     try {
700                         Content content = this.getNode(request);
701                         if (content.isLocked()) {
702                             content.unlock();
703                         }
704                     }catch (ItemNotFoundException e) {
705                         // ignore - commit of deactivation
706                     }
707                 }
708             } catch (LockException le) {
709                 // either repository does not support locking OR this node never locked
710                 log.debug(le.getMessage(), le);
711             } catch (RepositoryException re) {
712                 // should never come here
713                 log.warn("Exception caught", re);
714             } catch (ExchangeException e) {
715                 // should never come here
716                 log.warn("Exception caught", e);
717             }
718         }
719 
720         // TODO : why is this here ? as far as I can tell, http sessions are never created when reaching this
721         try {
722             HttpSession httpSession = request.getSession(false);
723             if (httpSession != null) {
724                 httpSession.invalidate();
725             }
726         } catch (Throwable t) {
727             // its only a test so just dump
728             log.error("failed to invalidate session", t);
729         }
730     }
731 
732     /**
733      * apply lock.
734      * @param request
735      */
736     protected void applyLock(HttpServletRequest request) throws ExchangeException {
737         try {
738             Content parent = waitForLock(request);
739             if (parent != null) {
740                 // get a new deep lock
741                 parent.lock(true, true);
742             }
743         } catch (LockException le) {
744             // either repository does not support locking OR this node never locked
745             log.debug(le.getMessage());
746         } catch (RepositoryException re) {
747             // will blow fully at later stage
748             log.warn("Exception caught", re);
749         }
750     }
751 
752     /**
753      * Will wait for predefined amount of time and attempt predefined number of times to obtain unlocked content.
754      * 
755      * @param request
756      * @return unlocked content specified by the request or null in case such content doesnt exist
757      * @throws ExchangeException
758      * @throws RepositoryException
759      */
760     protected Content waitForLock(HttpServletRequest request) throws ExchangeException, RepositoryException {
761         int retries = getUnlockRetries();
762         long retryWait = getRetryWait() * 1000;
763         Content content = null;
764         try {
765             content = this.getNode(request);
766             while (content.isLocked() && retries > 0) {
767                 log.info("Content " + content.getHandle() + " is locked. Will retry " + retries + " more times.");
768                 try {
769                     Thread.sleep(retryWait);
770                 } catch (InterruptedException e) {
771                     // Restore the interrupted status
772                     Thread.currentThread().interrupt();
773                 }
774                 retries--;
775                 content = this.getNode(request);
776             }
777             if (content.isLocked()) {
778                 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.");
779             }
780         } catch (ItemNotFoundException e) {
781             // - when deleting new piece of content on the author and mgnl tries to deactivate it on public automatically
782             log.debug("Attempt to lock non existing content {} during (de)activation.", getUUID(request));
783         } catch (PathNotFoundException e) {
784             // - when attempting to activate the content for which parent content have not been yet activated
785             log.debug("Attempt to lock non existing content {}:{} during (de)activation.",getHierarchyManager(request).getName(), getParentPath(request));
786         }
787         return content;
788     }
789 
790     protected Content getNode(HttpServletRequest request) throws ExchangeException, RepositoryException {
791         if (request.getHeader(BaseSyndicatorImpl.PARENT_PATH) != null) {
792             String parentPath = this.getParentPath(request);
793             log.debug("parent path:" + parentPath);
794             return this.getHierarchyManager(request).getContent(parentPath);
795         } else if (!StringUtils.isEmpty(getUUID(request))){
796             log.debug("node uuid:" + request.getHeader(BaseSyndicatorImpl.NODE_UUID));
797             return this.getHierarchyManager(request).getContentByUUID(request.getHeader(BaseSyndicatorImpl.NODE_UUID));
798         } else {
799             throw new ExchangeException("Request is missing mandatory content identifier.");
800         }
801     }
802 
803     protected String getParentPath(HttpServletRequest request) {
804         String parentPath = request.getHeader(BaseSyndicatorImpl.PARENT_PATH);
805         if (StringUtils.isNotEmpty(parentPath)) {
806             return parentPath;
807         }
808         return "";
809     }
810 
811     protected String getUUID(HttpServletRequest request) {
812         String parentPath = request.getHeader(BaseSyndicatorImpl.NODE_UUID);
813         if (StringUtils.isNotEmpty(parentPath)) {
814             return parentPath;
815         }
816         return "";
817     }
818 
819 
820 }