1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
95
96
97
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
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
151 if (!isAuthorAuthenticated(request, response)) {
152 status = BaseSyndicatorImpl.ACTIVATION_HANDSHAKE;
153 setResponseHeaders(response, statusMessage, status, result);
154 return;
155 }
156
157 applyLock(request);
158 } catch (ExchangeException e) {
159
160
161 log.debug(e.getMessage(), e);
162
163
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
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
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
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
213 MgnlKeyPair tempKeys = SecurityUtil.generateKeyPair(module.getActivationKeyLength());
214
215 response.addHeader(BaseSyndicatorImpl.ACTIVATION_AUTH, tempKeys.getPublicKey());
216 module.setTempKeys(tempKeys);
217 return false;
218 } else {
219 try {
220
221 String authorsPublicKeyEncryptedByTempPublicKey = request.getHeader(BaseSyndicatorImpl.ACTIVATION_AUTH_KEY);
222
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
229
230 SecurityUtil.updateKeys(new MgnlKeyPair(null, publicKey));
231 }
232 } finally {
233
234 module.setTempKeys(null);
235 }
236 if (SecurityUtil.getPublicKey() == null) {
237
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
259
260
261
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
274 String webapp = getWebappName();
275
276 if (action.equalsIgnoreCase(BaseSyndicatorImpl.ACTIVATE)) {
277 String name = update(request, resourcesmd5);
278
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
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
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
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
334
335 @Deprecated
336 protected String getUser(HttpServletRequest request) {
337 return null;
338 }
339
340
341
342
343
344
345
346
347
348
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
363 newParentPath = handleMovedContent(newParentPath, hm, topContentElement, content);
364 handleChildren(request, content);
365 this.importOnExisting(topContentElement, data, hm, content);
366 } catch (ItemNotFoundException e) {
367
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
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
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
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
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
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
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
465
466
467
468
469 protected synchronized void copyProperties(Content source, Content destination) throws RepositoryException {
470
471
472 Iterator nodeDataIterator = destination.getNodeDataCollection().iterator();
473 while (nodeDataIterator.hasNext()) {
474 NodeData nodeData = (NodeData) nodeDataIterator.next();
475
476
477 if (nodeData.getType() != PropertyType.BINARY) {
478 nodeData.delete();
479 }
480 }
481
482
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
504
505
506
507
508 protected synchronized void removeChildren(Content content, Content.ContentFilter filter) {
509 Iterator children = content.getChildren(filter).iterator();
510
511
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
524
525
526
527
528
529
530
531
532 protected synchronized void importFresh(Element topContentElement, MultipartForm data, HierarchyManager hierarchyManager, String parentPath) throws ExchangeException, RepositoryException {
533
534
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";
545
546 log.debug(message, e);
547 hierarchyManager.refresh(false);
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);
553 throw new ExchangeException(message);
554 }
555 }
556
557
558
559
560
561
562
563
564
565
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
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
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
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
590
591
592
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
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
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
618
619
620
621
622
623
624
625 protected synchronized void importResource(MultipartForm data, Element resourceElement, HierarchyManager hm, String parentPath) throws Exception {
626
627
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
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
644 try {
645 parentPath = hm.getContentByUUID(resourceElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_UUID_ATTRIBUTE)).getHandle();
646 } catch (ItemNotFoundException e) {
647
648
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
659
660
661
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
683
684
685
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
699
700
701
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
717 try {
718 HttpSession httpSession = request.getSession(false);
719 if (httpSession != null) {
720 httpSession.invalidate();
721 }
722 } catch (Throwable t) {
723
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
733 if (StringUtils.isEmpty(parentPath) || this.getHierarchyManager(request).isExist(parentPath)) {
734 try {
735 content = this.getNode(request);
736 } catch (ItemNotFoundException e) {
737
738 }
739 unlock(content);
740 }
741
742
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
751 } catch (PathNotFoundException e) {
752
753 }
754 unlock(item);
755
756
757 Content rootLock = getRootLockOrNull(content);
758 unlock(rootLock);
759 } catch (LockException le) {
760
761 log.warn(le.getMessage());
762 } catch (RepositoryException re) {
763
764 log.warn("Exception caught", re);
765 } catch (ExchangeException e) {
766
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
780
781
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
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
797 log.warn("Exception caught", re);
798 }
799 }
800
801
802
803
804
805
806
807
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
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
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
843 log.debug("Attempt to lock non existing content {} during (de)activation.", getUUID(request));
844 } catch (PathNotFoundException e) {
845
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
872
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
906
907
908
909
910
911
912
913
914 protected void lock(Content node, Content parent) throws ExchangeException, RepositoryException {
915 try {
916 if (parent != null) {
917 if (parent.getHandle().equals("/")) {
918
919 Content rootLock = getRootLockOrNull(parent);
920 rootLock.lock(false, true);
921 logLockStatus(rootLock, true);
922
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
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
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 }