View Javadoc

1   /**
2    * This file Copyright (c) 2003-2013 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.module.exchangesimple;
35  
36  import info.magnolia.cms.beans.runtime.Document;
37  import info.magnolia.cms.beans.runtime.MultipartForm;
38  import info.magnolia.cms.core.Content;
39  import info.magnolia.cms.core.HierarchyManager;
40  import info.magnolia.cms.core.ItemType;
41  import info.magnolia.cms.core.MgnlNodeType;
42  import info.magnolia.cms.core.NodeData;
43  import info.magnolia.cms.core.SystemProperty;
44  import info.magnolia.cms.exchange.ExchangeException;
45  import info.magnolia.cms.filters.AbstractMgnlFilter;
46  import info.magnolia.cms.security.AccessDeniedException;
47  import info.magnolia.cms.security.MgnlKeyPair;
48  import info.magnolia.cms.security.Permission;
49  import info.magnolia.cms.security.PermissionUtil;
50  import info.magnolia.cms.security.SecurityUtil;
51  import info.magnolia.cms.util.ContentUtil;
52  import info.magnolia.cms.util.Rule;
53  import info.magnolia.cms.util.RuleBasedContentFilter;
54  import info.magnolia.context.MgnlContext;
55  import info.magnolia.context.SystemContext;
56  
57  import java.io.IOException;
58  import java.security.DigestInputStream;
59  import java.security.InvalidParameterException;
60  import java.security.NoSuchAlgorithmException;
61  import java.util.Iterator;
62  import java.util.List;
63  import java.util.zip.GZIPInputStream;
64  
65  import javax.jcr.ImportUUIDBehavior;
66  import javax.jcr.ItemNotFoundException;
67  import javax.jcr.Node;
68  import javax.jcr.PathNotFoundException;
69  import javax.jcr.Property;
70  import javax.jcr.PropertyType;
71  import javax.jcr.RepositoryException;
72  import javax.jcr.Session;
73  import javax.jcr.UnsupportedRepositoryOperationException;
74  import javax.jcr.lock.LockException;
75  import javax.servlet.FilterChain;
76  import javax.servlet.ServletException;
77  import javax.servlet.http.HttpServletRequest;
78  import javax.servlet.http.HttpServletResponse;
79  import javax.servlet.http.HttpSession;
80  
81  import org.apache.commons.io.IOUtils;
82  import org.apache.commons.lang.StringUtils;
83  import org.apache.jackrabbit.JcrConstants;
84  import org.jdom.Element;
85  import org.jdom.JDOMException;
86  import org.jdom.input.SAXBuilder;
87  import org.safehaus.uuid.UUIDGenerator;
88  import org.slf4j.Logger;
89  import org.slf4j.LoggerFactory;
90  
91  import com.google.inject.Inject;
92  
93  /**
94   * This filter receives activation requests from another instance and applies them.
95   * 
96   * @author Sameer Charles
97   * $Id$
98   */
99  public class ReceiveFilter extends AbstractMgnlFilter {
100 
101     private static final Logger log = LoggerFactory.getLogger(ReceiveFilter.class);
102 
103     public static final String SYSTEM_REPO = "mgnlSystem";
104 
105     public static final String ROOT_LOCK_NAME = "rootLock";
106 
107     private int unlockRetries = 10;
108 
109     private int retryWait = 2;
110 
111     private final ExchangeSimpleModule module;
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 (ExchangeException e) {
159             // can't obtain a lock ... this is (should be) a normal threading situation that we are
160             // just reporting back to user.
161             log.debug(e.getMessage(), e);
162             // we can only rely on the exception's actual message to give something back to the user
163             // here.
164             statusMessage = StringUtils.defaultIfEmpty(e.getMessage(), e.getClass().getSimpleName());
165             status = BaseSyndicatorImpl.ACTIVATION_FAILED;
166             setResponseHeaders(response, statusMessage, status, result);
167             return;
168         } catch (Throwable e) {
169             log.error(e.getMessage(), e);
170             // we can only rely on the exception's actual message to give something back to the user here.
171             statusMessage = StringUtils.defaultIfEmpty(e.getMessage(), e.getClass().getSimpleName());
172             status = BaseSyndicatorImpl.ACTIVATION_FAILED;
173             setResponseHeaders(response, statusMessage, status, result);
174             return;
175         }
176 
177         try {
178             result = receive(request);
179             status = BaseSyndicatorImpl.ACTIVATION_SUCCESSFUL;
180         } catch (OutOfMemoryError e) {
181             Runtime rt = Runtime.getRuntime();
182             log.error("---------\nOutOfMemoryError caught during activation. Total memory = "
183                     + rt.totalMemory()
184                     + ", free memory = "
185                     + rt.freeMemory()
186                     + "\n---------");
187             statusMessage = e.getMessage();
188             status = BaseSyndicatorImpl.ACTIVATION_FAILED;
189         } catch (PathNotFoundException e) {
190             // this should not happen. PNFE should be already caught and wrapped in ExchangeEx
191             log.error(e.getMessage(), e);
192             statusMessage = "Parent not found (not yet activated): " + e.getMessage();
193             status = BaseSyndicatorImpl.ACTIVATION_FAILED;
194         } catch (ExchangeException e) {
195             log.debug(e.getMessage(), e);
196             statusMessage = e.getMessage();
197             status = BaseSyndicatorImpl.ACTIVATION_FAILED;
198         } catch (Throwable e) {
199             log.error(e.getMessage(), e);
200             // we can only rely on the exception's actual message to give something back to the user here.
201             statusMessage = StringUtils.defaultIfEmpty(e.getMessage(), e.getClass().getSimpleName());
202             status = BaseSyndicatorImpl.ACTIVATION_FAILED;
203         } finally {
204             cleanUp(request, status);
205             setResponseHeaders(response, statusMessage, status, result);
206         }
207     }
208 
209     protected boolean isAuthorAuthenticated(HttpServletRequest request, HttpServletResponse response) throws NoSuchAlgorithmException, ExchangeException {
210         if (SecurityUtil.getPublicKey() == null) {
211             if (module.getTempKeys() == null) {
212                 // no temp keys set or module reset wiped them out
213                 MgnlKeyPair tempKeys = SecurityUtil.generateKeyPair(module.getActivationKeyLength());
214                 // 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
215                 response.addHeader(BaseSyndicatorImpl.ACTIVATION_AUTH, tempKeys.getPublicKey());
216                 module.setTempKeys(tempKeys);
217                 return false;
218             } else {
219                 try {
220                     // we have temp keys so we expect that this time around we are getting the public key to store
221                     String authorsPublicKeyEncryptedByTempPublicKey = request.getHeader(BaseSyndicatorImpl.ACTIVATION_AUTH_KEY);
222                     // use our private key to decrypt
223                     String publicKey = SecurityUtil.decrypt(authorsPublicKeyEncryptedByTempPublicKey, module.getTempKeys().getPrivateKey());
224                     if (StringUtils.isNotBlank(publicKey)) {
225                         String authString = SecurityUtil.decrypt(request.getHeader(BaseSyndicatorImpl.ACTIVATION_AUTH), publicKey);
226                         String[] auth = authString.split(";");
227                         checkTimestamp(auth);
228                         // no private key for public
229                         // 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?
230                         SecurityUtil.updateKeys(new MgnlKeyPair(null, publicKey));
231                     }
232                 } finally {
233                     // cleanup temp keys no matter what
234                     module.setTempKeys(null);
235                 }
236                 if (SecurityUtil.getPublicKey() == null) {
237                     // we are too fast and trying before observation had a chance to kick in
238                     try {
239                         Thread.sleep(3000);
240                     } catch (InterruptedException e) {
241                         Thread.currentThread().interrupt();
242                     }
243                     if (SecurityUtil.getPublicKey() == null) {
244                         throw new ExchangeException("Failed to negotiate encryption key between author and public instance. Please try again later or contact admin if error persists.");
245                     }
246                 }
247             }
248         }
249         return true;
250     }
251 
252     protected void setResponseHeaders(HttpServletResponse response, String statusMessage, String status, String result) {
253         response.setHeader(BaseSyndicatorImpl.ACTIVATION_ATTRIBUTE_STATUS, status);
254         response.setHeader(BaseSyndicatorImpl.ACTIVATION_ATTRIBUTE_MESSAGE, statusMessage);
255     }
256 
257     /**
258      * handle activate or deactivate request.
259      * 
260      * @param request
261      * @throws Exception if fails to update
262      */
263     protected synchronized String receive(HttpServletRequest request) throws Exception {
264         String action = request.getHeader(BaseSyndicatorImpl.ACTION);
265         log.debug("action: " + action);
266 
267         String[] auth = checkAuthenticated(request);
268 
269         String user = auth[1];
270 
271         String resourcesmd5 = auth[2];
272 
273         // 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.
274         String webapp = getWebappName();
275 
276         if (action.equalsIgnoreCase(BaseSyndicatorImpl.ACTIVATE)) {
277             String name = update(request, resourcesmd5);
278             // Everything went well
279             log.info("User {} successfuly activated {} on {}.", new Object[] { user, name, webapp });
280         }
281         else if (action.equalsIgnoreCase(BaseSyndicatorImpl.DEACTIVATE)) {
282             String name = remove(request, resourcesmd5);
283             // Everything went well
284             log.info("User {} succeessfuly deactivated {} on {}.", new Object[] { user, name, webapp });
285         }
286         else {
287             throw new UnsupportedOperationException("Method not supported : " + action);
288         }
289         return null;
290     }
291 
292     protected String[] checkAuthenticated(HttpServletRequest request) throws ExchangeException {
293         String encrypted = request.getHeader(BaseSyndicatorImpl.ACTIVATION_AUTH);
294         if (StringUtils.isBlank(encrypted)) {
295             log.debug("Attempt to access activation URL w/o proper information in request. Ignoring silently.");
296             throw new ExchangeException();
297         }
298 
299         String decrypted = SecurityUtil.decrypt(encrypted);
300         if (StringUtils.isBlank(decrypted)) {
301             throw new SecurityException("Handshake information for activation was incorrect. Someone attempted to impersonate author instance. Incoming request was from " + request.getRemoteAddr());
302         }
303 
304         String[] auth = decrypted.split(";");
305 
306         // timestamp;user;resourcemd;optional_encrypted_public_key
307         if (auth.length != 3) {
308             throw new SecurityException("Handshake information for activation was incorrect. Someone attempted to impersonate author instance. Incoming request was from " + request.getRemoteAddr());
309         }
310         // first part is a timestamp
311         checkTimestamp(auth);
312         return auth;
313     }
314 
315     private void checkTimestamp(String[] auth) {
316         long timestamp = System.currentTimeMillis();
317         long authorTimestamp = 0;
318         try {
319             authorTimestamp = Long.parseLong(auth[0]);
320         } catch (NumberFormatException e) {
321             throw new SecurityException("Handshake information for activation was incorrect. This might be an attempt to replay earlier activation request.");
322         }
323         if (Math.abs(timestamp - authorTimestamp) > module.getActivationDelayTolerance()) {
324             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.");
325         }
326     }
327 
328     protected String getWebappName() {
329         return SystemProperty.getProperty(SystemProperty.MAGNOLIA_WEBAPP);
330     }
331 
332     /**
333      * @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.
334      */
335     @Deprecated
336     protected String getUser(HttpServletRequest request) {
337         return null;
338     }
339 
340     /**
341      * handle update (activate) request.
342      * 
343      * @param request
344      * incoming reuqest
345      * @param resourcesmd5
346      * signature confirming validity of resource file
347      * @throws Exception
348      * if fails to update
349      */
350     protected synchronized String update(HttpServletRequest request, String resourcesmd5) throws Exception {
351         MultipartForm data = MgnlContext.getPostedForm();
352         if (null != data) {
353             String newParentPath = this.getParentPath(request);
354             String resourceFileName = request.getHeader(BaseSyndicatorImpl.RESOURCE_MAPPING_FILE);
355             HierarchyManager hm = getHierarchyManager(request);
356             Element rootElement = getImportedContentRoot(data, resourceFileName, resourcesmd5);
357             Element topContentElement = rootElement.getChild(BaseSyndicatorImpl.RESOURCE_MAPPING_FILE_ELEMENT);
358             Content content = null;
359             try {
360                 String uuid = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_UUID_ATTRIBUTE);
361                 content = hm.getContentByUUID(uuid);
362                 // move content to new location if necessary.
363                 newParentPath = handleMovedContent(newParentPath, hm, topContentElement, content);
364                 handleChildren(request, content);
365                 this.importOnExisting(topContentElement, data, hm, content);
366             } catch (ItemNotFoundException e) {
367                 // new content
368                 importFresh(topContentElement, data, hm, newParentPath);
369             }
370 
371             return orderImportedNode(newParentPath, hm, rootElement, topContentElement);
372         }
373         return null;
374     }
375 
376     protected Element getImportedContentRoot(MultipartForm data, String resourceFileName, String resourcesmd5) throws JDOMException, IOException {
377         Document resourceDocument = data.getDocument(resourceFileName);
378         SAXBuilder builder = new SAXBuilder();
379 
380         DigestInputStream documentInputStream = SecurityUtil.getDigestInputStream(resourceDocument.getStream());
381         org.jdom.Document jdomDocument = builder.build(documentInputStream);
382         IOUtils.closeQuietly(documentInputStream);
383 
384         final String sign = SecurityUtil.getMD5Hex(documentInputStream);
385         if (!resourcesmd5.equals(sign)) {
386             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.");
387         }
388 
389         return jdomDocument.getRootElement();
390     }
391 
392     protected void handleChildren(HttpServletRequest request, Content content) {
393         String ruleString = request.getHeader(BaseSyndicatorImpl.CONTENT_FILTER_RULE);
394         Rule rule = new Rule(ruleString, ",");
395         RuleBasedContentFilter filter = new RuleBasedContentFilter(rule);
396         // remove all child nodes
397         this.removeChildren(content, filter);
398     }
399 
400     protected String handleMovedContent(String newParentPath, HierarchyManager hm, Element topContentElement, Content content) throws RepositoryException {
401         String currentParentPath = content.getHandle();
402         currentParentPath = currentParentPath.substring(0, currentParentPath.lastIndexOf('/'));
403         String newName = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
404         if (!newParentPath.endsWith("/")) {
405             newParentPath += "/";
406         }
407         if (!currentParentPath.endsWith("/")) {
408             currentParentPath += "/";
409         }
410         if (!newParentPath.equals(currentParentPath) || !content.getName().equals(newName)) {
411             log.info("Moving content from {} to {} due to activation request.", new Object[] { content.getHandle(), newParentPath + newName });
412             hm.moveTo(content.getHandle(), newParentPath + newName);
413         }
414         return newParentPath;
415     }
416 
417     protected String orderImportedNode(String newParentPath, HierarchyManager hm, Element rootElement, Element topContentElement) throws RepositoryException {
418         String name;
419         // order imported node
420         name = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
421         Content parent = hm.getContent(newParentPath);
422         List siblings = rootElement.getChild(BaseSyndicatorImpl.SIBLINGS_ROOT_ELEMENT).getChildren(BaseSyndicatorImpl.SIBLINGS_ELEMENT);
423         Iterator siblingsIterator = siblings.iterator();
424         while (siblingsIterator.hasNext()) {
425             Element sibling = (Element) siblingsIterator.next();
426             // check for existence and order
427             String siblingUUID = sibling.getAttributeValue(BaseSyndicatorImpl.SIBLING_UUID);
428             try {
429                 Content beforeContent = hm.getContentByUUID(siblingUUID);
430                 log.debug("Ordering {} before {}", name, beforeContent.getName());
431                 order(parent, name, beforeContent.getName());
432                 break;
433             } catch (AccessDeniedException e) {
434                 log.info("User doesn't have enough rights to force order of nodes on {}", siblingUUID);
435             } catch (ItemNotFoundException e) {
436                 // ignore
437             } catch (RepositoryException re) {
438                 if (log.isDebugEnabled()) {
439                     log.debug("Failed to order node", re);
440                 } else {
441                     log.warn("Failed to order node");
442                 }
443             }
444         }
445 
446         // ensure the no sibling nodes are at the end ... since move is not activated immediately it is sometimes necessary to preserve right order
447         if (siblings.isEmpty()) {
448             order(parent, name, null);
449         }
450         return name;
451     }
452 
453     protected void order(Content parent, String name, String orderBefore) throws RepositoryException {
454         try {
455             parent.orderBefore(name, orderBefore);
456         } catch (UnsupportedRepositoryOperationException e) {
457             // since not all types support ordering we should not enforce it, but only log the error
458             log.warn("Failed to order unorderable content {} at {} due to {}", new Object[] { name, parent.getHandle(), e.getMessage() });
459         }
460         parent.save();
461     }
462 
463     /**
464      * Copy all properties from source to destination (by cleaning the old properties).
465      * 
466      * @param source the content node to be copied
467      * @param destination the destination node
468      */
469     protected synchronized void copyProperties(Content source, Content destination) throws RepositoryException {
470         // first remove all existing properties at the destination
471         // will be different with incremental activation
472         Iterator nodeDataIterator = destination.getNodeDataCollection().iterator();
473         while (nodeDataIterator.hasNext()) {
474             NodeData nodeData = (NodeData) nodeDataIterator.next();
475             // Ignore binary types, since these are sub nodes and already taken care of while
476             // importing sub resources
477             if (nodeData.getType() != PropertyType.BINARY) {
478                 nodeData.delete();
479             }
480         }
481 
482         // copy all properties
483         Node destinationNode = destination.getJCRNode();
484         nodeDataIterator = source.getNodeDataCollection().iterator();
485         while (nodeDataIterator.hasNext()) {
486             NodeData nodeData = (NodeData) nodeDataIterator.next();
487             Property property = nodeData.getJCRProperty();
488             if (property.getDefinition().isMultiple()) {
489                 if (destination.isGranted(Permission.WRITE)) {
490                     destinationNode.setProperty(nodeData.getName(), property.getValues());
491                 }
492                 else {
493                     throw new AccessDeniedException("User not allowed to " + Permission.PERMISSION_NAME_WRITE + " at [" + nodeData.getHandle() + "]");
494                 }
495             }
496             else {
497                 destination.createNodeData(nodeData.getName(), nodeData.getValue());
498             }
499         }
500     }
501 
502     /**
503      * remove children.
504      * 
505      * @param content whose children to be deleted
506      * @param filter content filter
507      */
508     protected synchronized void removeChildren(Content content, Content.ContentFilter filter) {
509         Iterator children = content.getChildren(filter).iterator();
510         // remove sub nodes using the same filter used by the sender to collect
511         // this will make sure there is no existing nodes of the same type
512         while (children.hasNext()) {
513             Content child = (Content) children.next();
514             try {
515                 child.delete();
516             } catch (Exception e) {
517                 log.error("Failed to remove " + child.getHandle() + " | " + e.getMessage());
518             }
519         }
520     }
521 
522     /**
523      * import on non existing tree.
524      * 
525      * @param topContentElement
526      * @param data
527      * @param hierarchyManager
528      * @param parentPath
529      * @throws ExchangeException
530      * @throws RepositoryException
531      */
532     protected synchronized void importFresh(Element topContentElement, MultipartForm data, HierarchyManager hierarchyManager, String parentPath) throws ExchangeException, RepositoryException {
533         // 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
534         // TODO: handle same name siblings!
535         String path = parentPath + (parentPath.endsWith("/") ? "" : "/") + topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
536         if (hierarchyManager.isExist(path)) {
537             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);
538             hierarchyManager.delete(path);
539         }
540         try {
541             importResource(data, topContentElement, hierarchyManager, parentPath);
542             hierarchyManager.save();
543         } catch (PathNotFoundException e) {
544             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
545             // this is not a system error so there should not be a need to log the exception all the time.
546             log.debug(message, e);
547             hierarchyManager.refresh(false); // revert all transient changes made in this session till now.
548             throw new ExchangeException(message);
549         } catch (Exception e) {
550             final String message = "Activation failed | " + e.getMessage();
551             log.error("Exception caught", e);
552             hierarchyManager.refresh(false); // revert all transient changes made in this session till now.
553             throw new ExchangeException(message);
554         }
555     }
556 
557     /**
558      * import on existing content, making sure that content which is not sent stays as is.
559      * 
560      * @param topContentElement
561      * @param data
562      * @param hierarchyManager
563      * @param existingContent
564      * @throws ExchangeException
565      * @throws RepositoryException
566      */
567     protected synchronized void importOnExisting(Element topContentElement, MultipartForm data,
568             final HierarchyManager hierarchyManager, Content existingContent) throws ExchangeException, RepositoryException {
569         final Iterator<Content> fileListIterator = topContentElement.getChildren(BaseSyndicatorImpl.RESOURCE_MAPPING_FILE_ELEMENT).iterator();
570         final String uuid = UUIDGenerator.getInstance().generateTimeBasedUUID().toString();
571         final String handle = existingContent.getHandle();
572         // Can't execute in system context here just get hm from SC and use it for temp node handling
573         final HierarchyManager systemHM = MgnlContext.getSystemContext().getHierarchyManager(SYSTEM_REPO);
574         try {
575             while (fileListIterator.hasNext()) {
576                 Element fileElement = (Element) fileListIterator.next();
577                 importResource(data, fileElement, hierarchyManager, handle);
578             }
579             // use temporary node in mgnlSystem workspace to extract the top level node and copy its properties
580             Content activationTmp = ContentUtil.getOrCreateContent(systemHM.getRoot(), "activation-tmp", ItemType.FOLDER, true);
581             final Content transientNode = activationTmp.createContent(uuid, MgnlNodeType.NT_PAGE);
582             systemHM.save();
583             final String transientStoreHandle = transientNode.getHandle();
584             // import properties into transientStore
585             final String fileName = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_ID_ATTRIBUTE);
586             final String expectedMD5 = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_MD_ATTRIBUTE);
587 
588             final DigestInputStream inputStream = SecurityUtil.getDigestInputStream(new GZIPInputStream(data.getDocument(fileName).getStream()));
589             // need to import in system context
590             // and yes we generate new uuids - content is stored here just totransfer it's props. We
591             // already have backup copy in same workspace w/ same uuid
592             // and using other strategy would destroy such copy
593             systemHM.getWorkspace().getSession().importXML(transientStoreHandle, inputStream, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW);
594             IOUtils.closeQuietly(inputStream);
595             final String calculatedMD5 = SecurityUtil.getMD5Hex(inputStream);
596 
597             if (!calculatedMD5.equals(expectedMD5)) {
598                 throw new SecurityException(fileName + " signature is not valid. Resource might have been modified in transit.");
599             }
600             // copy properties from transient store to existing content
601             Content tmpContent = transientNode.getContent(topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE));
602             copyProperties(tmpContent, existingContent);
603             systemHM.delete(transientStoreHandle);
604             hierarchyManager.save();
605             systemHM.save();
606         } catch (Exception e) {
607             // revert all transient changes made in this session till now.
608             hierarchyManager.refresh(false);
609             systemHM.refresh(false);
610 
611             log.error("Exception caught", e);
612             throw new ExchangeException("Activation failed : " + e.getMessage());
613         }
614     }
615 
616     /**
617      * import documents.
618      * 
619      * @param data as sent
620      * @param resourceElement parent file element
621      * @param hm
622      * @param parentPath Path to the node parent
623      * @throws Exception
624      */
625     protected synchronized void importResource(MultipartForm data, Element resourceElement, HierarchyManager hm, String parentPath) throws Exception {
626 
627         // throws an exception in case you don't have the permission
628         PermissionUtil.isGranted(hm.getWorkspace().getSession(), parentPath, Session.ACTION_ADD_NODE);
629 
630         final String name = resourceElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
631         final String fileName = resourceElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_ID_ATTRIBUTE);
632         final String expectedMD5 = resourceElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_MD_ATTRIBUTE);
633         // do actual import
634         final DigestInputStream inputStream = SecurityUtil.getDigestInputStream(new GZIPInputStream(data.getDocument(fileName).getStream()));
635         log.debug("Importing {} into parent path {}", new Object[] { name, parentPath });
636         hm.getWorkspace().getSession().importXML(parentPath, inputStream, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
637         IOUtils.closeQuietly(inputStream);
638         final String calculatedMD5 = SecurityUtil.getMD5Hex(inputStream);
639         if (!calculatedMD5.equals(expectedMD5)) {
640             throw new SecurityException(fileName + " signature is not valid. Resource might have been modified in transit. Expected signature:" + expectedMD5 + ", actual signature found: " + calculatedMD5);
641         }
642         Iterator fileListIterator = resourceElement.getChildren(BaseSyndicatorImpl.RESOURCE_MAPPING_FILE_ELEMENT).iterator();
643         // parent path
644         try {
645             parentPath = hm.getContentByUUID(resourceElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_UUID_ATTRIBUTE)).getHandle();
646         } catch (ItemNotFoundException e) {
647             // non referencable content like meta data ...
648             // FYI: if we ever have non referencable same name sibling content the trouble will be here with child content being mixed
649             parentPath = StringUtils.removeEnd(parentPath, "/") + "/" + name;
650         }
651         while (fileListIterator.hasNext()) {
652             Element fileElement = (Element) fileListIterator.next();
653             importResource(data, fileElement, hm, parentPath);
654         }
655     }
656 
657     /**
658      * Deletes (de-activate) the content specified by the request.
659      * 
660      * @param request
661      * @throws Exception if fails to update
662      */
663     protected synchronized String remove(HttpServletRequest request, String md5) throws Exception {
664 
665         if (!md5.equals(SecurityUtil.getMD5Hex(request.getHeader(BaseSyndicatorImpl.NODE_UUID)))) {
666             throw new SecurityException("Signature of resource doesn't match. This seems like malicious attempt to delete content. Request was issued from " + request.getRemoteAddr());
667         }
668         HierarchyManager hm = getHierarchyManager(request);
669         String handle = null;
670         try {
671             Content node = this.getNode(request);
672             handle = node.getHandle();
673             hm.delete(handle);
674             hm.save();
675         } catch (ItemNotFoundException e) {
676             log.debug("Unable to delete node", e);
677         }
678         return handle;
679     }
680 
681     /**
682      * get hierarchy manager.
683      * 
684      * @param request
685      * @throws ExchangeException
686      */
687     protected HierarchyManager getHierarchyManager(HttpServletRequest request) throws ExchangeException {
688         String workspaceName = request.getHeader(BaseSyndicatorImpl.WORKSPACE_NAME);
689 
690         if (StringUtils.isEmpty(workspaceName)) {
691             throw new ExchangeException("Repository or workspace name not sent, unable to activate. Workspace: " + workspaceName);
692         }
693         SystemContext sysCtx = MgnlContext.getSystemContext();
694         return sysCtx.getHierarchyManager(workspaceName);
695     }
696 
697     /**
698      * cleans temporary store and removes any locks set.
699      * 
700      * @param request
701      * @param status
702      */
703     protected void cleanUp(HttpServletRequest request, String status) {
704         if (!BaseSyndicatorImpl.DEACTIVATE.equalsIgnoreCase(request.getHeader(BaseSyndicatorImpl.ACTION))) {
705             MultipartForm data = MgnlContext.getPostedForm();
706             if (null != data) {
707                 Iterator keys = data.getDocuments().keySet().iterator();
708                 while (keys.hasNext()) {
709                     String key = (String) keys.next();
710                     data.getDocument(key).delete();
711                 }
712             }
713             releaseLock(request);
714         }
715 
716         // TODO : why is this here ? as far as I can tell, http sessions are never created when reaching this
717         try {
718             HttpSession httpSession = request.getSession(false);
719             if (httpSession != null) {
720                 httpSession.invalidate();
721             }
722         } catch (Throwable t) {
723             // its only a test so just dump
724             log.error("failed to invalidate session", t);
725         }
726     }
727 
728     protected void releaseLock(HttpServletRequest request) {
729         try {
730             final String parentPath = getParentPath(request);
731             Content content = null;
732             // unlock parent
733             if (StringUtils.isEmpty(parentPath) || this.getHierarchyManager(request).isExist(parentPath)) {
734                 try {
735                     content = this.getNode(request);
736                 } catch (ItemNotFoundException e) {
737                     // ignore - commit of deactivation
738                 }
739                 unlock(content);
740             }
741 
742             // unlock the node itself
743             Content item = null;
744             try {
745                 final String uuid = this.getUUID(request);
746                 if (StringUtils.isNotBlank(uuid)) {
747                     item = this.getHierarchyManager(request).getContentByUUID(uuid);
748                 }
749             } catch (ItemNotFoundException e) {
750                 // ignore - commit of deactivation
751             } catch (PathNotFoundException e) {
752                 // ignore - commit of deactivation
753             }
754             unlock(item);
755 
756             // unlock root (if locked)
757             Content rootLock = getRootLockOrNull(content);
758             unlock(rootLock);
759         } catch (LockException le) {
760             // either repository does not support locking OR this node never locked
761             log.warn(le.getMessage());
762         } catch (RepositoryException re) {
763             // should never come here
764             log.warn("Exception caught", re);
765         } catch (ExchangeException e) {
766             // should never come here
767             log.warn("Exception caught", e);
768         }
769     }
770 
771     private void unlock(Content content) throws RepositoryException, LockException {
772         if (content != null && content.isLocked()) {
773             content.unlock();
774             logLockStatus(content, false);
775         }
776     }
777 
778     /**
779      * apply lock.
780      * 
781      * @param request
782      */
783     protected void applyLock(HttpServletRequest request) throws ExchangeException {
784         Content node = null;
785         try {
786             node = getHierarchyManager(request).getContentByUUID(getUUID(request));
787         } catch (ItemNotFoundException e) {
788             // ignore node just ain't exist yet
789         } catch (RepositoryException e) {
790             throw new ExchangeException(e.getMessage(), e);
791         }
792         try {
793             Content parent = waitForLock(request);
794             lock(node, parent);
795         } catch (RepositoryException re) {
796             // will blow fully at later stage
797             log.warn("Exception caught", re);
798         }
799     }
800 
801     /**
802      * Will wait for predefined amount of time and attempt predefined number of times to obtain unlocked content.
803      * 
804      * @param request
805      * @return unlocked content specified by the request or null in case such content doesnt exist
806      * @throws ExchangeException
807      * @throws RepositoryException
808      */
809     protected Content waitForLock(HttpServletRequest request) throws ExchangeException, RepositoryException {
810         int retries = getUnlockRetries();
811         long retryWait = getRetryWait() * 1000;
812         Content parentOnActivationNodeOtherwise = null;
813         Content rootLock = null;
814         Content activatedNode;
815         try {
816             activatedNode = this.getHierarchyManager(request).getContentByUUID(this.getUUID(request));
817         } catch (RepositoryException e) {
818             // first time activation
819             activatedNode = null;
820         }
821         try {
822             parentOnActivationNodeOtherwise = this.getNode(request);
823             rootLock = getRootLockOrNull(parentOnActivationNodeOtherwise);
824 
825             while (isLocked(parentOnActivationNodeOtherwise, rootLock, activatedNode) && retries > 0) {
826                 log.info("Content " + parentOnActivationNodeOtherwise.getHandle() + " is locked. Will retry " + retries + " more times.");
827                 try {
828                     Thread.sleep(retryWait);
829                 } catch (InterruptedException e) {
830                     // Restore the interrupted status
831                     Thread.currentThread().interrupt();
832                 }
833                 retries--;
834                 parentOnActivationNodeOtherwise = this.getNode(request);
835                 rootLock = getRootLockOrNull(parentOnActivationNodeOtherwise);
836             }
837             if (isLocked(parentOnActivationNodeOtherwise, rootLock, activatedNode)) {
838                 throw new ExchangeException("Content " + parentOnActivationNodeOtherwise.getHandle() + " is locked while activating " + request.getHeader(BaseSyndicatorImpl.NODE_UUID)
839                         + ". 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.");
840             }
841         } catch (ItemNotFoundException e) {
842             // - when deleting new piece of content on the author and mgnl tries to deactivate it on public automatically
843             log.debug("Attempt to lock non existing content {} during (de)activation.", getUUID(request));
844         } catch (PathNotFoundException e) {
845             // - when attempting to activate the content for which parent content have not been yet activated
846             log.debug("Attempt to lock non existing content {}:{} during (de)activation.", getHierarchyManager(request).getName(), getParentPath(request));
847         }
848         return parentOnActivationNodeOtherwise;
849     }
850 
851     private boolean isLocked(Content parentContent, Content rootLock, Content activatedNode) throws RepositoryException {
852         return rootLock != null && rootLock.isLocked() || parentContent.isLocked() || activatedNode != null && activatedNode.isLocked();
853     }
854 
855     private Content getRootLockOrNull(Content content) throws ExchangeException {
856         try {
857             if (content != null && "/".equals(content.getHandle())) {
858                 return ContentUtil.getOrCreateContent(getBackupHierarchyManager().getRoot(), content.getHierarchyManager().getName() + "-" + ROOT_LOCK_NAME, ItemType.CONTENT, true);
859             }
860         } catch (RepositoryException e) {
861             throw new ExchangeException("Failed to obtain root lock.", e);
862         }
863         return null;
864     }
865 
866     protected HierarchyManager getBackupHierarchyManager() {
867         return MgnlContext.getSystemContext().getHierarchyManager(SYSTEM_REPO);
868     }
869 
870     /**
871      * Returns parent node of the activated node in case of activation or the node itself in case of
872      * deactivation.
873      */
874     protected Content getNode(HttpServletRequest request) throws ExchangeException, RepositoryException {
875         if (request.getHeader(BaseSyndicatorImpl.PARENT_PATH) != null) {
876             String parentPath = this.getParentPath(request);
877             log.debug("parent path:" + parentPath);
878             return this.getHierarchyManager(request).getContent(parentPath);
879         } else if (!StringUtils.isEmpty(getUUID(request))) {
880             final String uuid = getUUID(request);
881             log.debug("node uuid:" + uuid);
882             return this.getHierarchyManager(request).getContentByUUID(uuid);
883         } else {
884             throw new ExchangeException("Request is missing mandatory content identifier.");
885         }
886     }
887 
888     protected String getParentPath(HttpServletRequest request) {
889         String parentPath = request.getHeader(BaseSyndicatorImpl.PARENT_PATH);
890         if (StringUtils.isNotEmpty(parentPath)) {
891             return parentPath;
892         }
893         return "";
894     }
895 
896     protected String getUUID(HttpServletRequest request) {
897         final String uuid = request.getHeader(BaseSyndicatorImpl.NODE_UUID);
898         if (StringUtils.isNotEmpty(uuid)) {
899             return uuid;
900         }
901         return "";
902     }
903 
904     /**
905      * Method handling issuing of the lock. Magnolia needs to lock not only the updated node, but
906      * also the parent node in case activated node is just being moved within same hierarchy or in
907      * case of deactivation. Locking of parent is not always possible (e.g. root node is not
908      * lockable). This method treats all special cases and can be overridden in case even more
909      * specialized handling is needed.
910      * 
911      * @param node Activated node.
912      * @param parent Parent of activated node.
913      */
914     protected void lock(Content node, Content parent) throws ExchangeException, RepositoryException {
915         try {
916             if (parent != null) {
917                 if (parent.getHandle().equals("/")) {
918                     // root is not lockable so we lock this lock instead
919                     Content rootLock = getRootLockOrNull(parent);
920                     rootLock.lock(false, true);
921                     logLockStatus(rootLock, true);
922                     // and we also lock the node itself if it exists
923                     if (node != null) {
924                         if (!node.hasMixin(JcrConstants.MIX_LOCKABLE)) {
925                             node.addMixin(JcrConstants.MIX_LOCKABLE);
926                             node.save();
927                         }
928                         node.lock(true, true);
929                         logLockStatus(node, true);
930                     }
931                 } else {
932                     if (!parent.hasMixin(JcrConstants.MIX_LOCKABLE)) {
933                         parent.addMixin(JcrConstants.MIX_LOCKABLE);
934                         parent.save();
935                     }
936                     parent.lock(true, true);
937                     logLockStatus(parent, true);
938                 }
939             }
940         } catch (LockException le) {
941             // either repository does not support locking OR this node never locked
942             log.error(le.getMessage(), le);
943         }
944 
945     }
946 
947     private void logLockStatus(Content content, boolean isLock) throws RepositoryException {
948         if (log.isDebugEnabled()) {
949             // this log obtains too much data to processed all the time when not enabled
950             log.debug("{} {} {}locked {}:{}", new Object[] { content.getWorkspace().getSession(), isLock ^ content.isLocked() ? "DIDN'T" : "DID", isLock ? "" : "un", content.getWorkspace().getName(), content.getHandle() });
951         }
952     }
953 
954 }