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