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