View Javadoc

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