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.core.Content;
37 import info.magnolia.cms.core.HierarchyManager;
38 import info.magnolia.cms.core.ItemType;
39 import info.magnolia.cms.core.MetaData;
40 import info.magnolia.cms.core.SystemProperty;
41 import info.magnolia.cms.core.version.ContentVersion;
42 import info.magnolia.cms.exchange.ExchangeException;
43 import info.magnolia.cms.exchange.Subscriber;
44 import info.magnolia.cms.exchange.Subscription;
45 import info.magnolia.cms.exchange.Syndicator;
46 import info.magnolia.cms.security.AccessDeniedException;
47 import info.magnolia.cms.security.SecurityUtil;
48 import info.magnolia.cms.security.User;
49 import info.magnolia.cms.util.ContentUtil;
50 import info.magnolia.cms.util.Rule;
51 import info.magnolia.cms.util.RuleBasedContentFilter;
52 import info.magnolia.context.MgnlContext;
53 import info.magnolia.init.MagnoliaConfigurationProperties;
54 import info.magnolia.logging.AuditLoggingUtil;
55
56 import java.io.File;
57 import java.io.IOException;
58 import java.io.UnsupportedEncodingException;
59 import java.net.HttpURLConnection;
60 import java.net.MalformedURLException;
61 import java.net.URL;
62 import java.net.URLConnection;
63 import java.net.URLEncoder;
64 import java.security.MessageDigest;
65 import java.security.NoSuchAlgorithmException;
66 import java.util.Calendar;
67 import java.util.Iterator;
68 import java.util.List;
69
70 import javax.jcr.RepositoryException;
71 import javax.jcr.Session;
72
73 import org.apache.commons.lang.StringUtils;
74 import org.slf4j.Logger;
75 import org.slf4j.LoggerFactory;
76
77 import EDU.oswego.cs.dl.util.concurrent.Sync;
78
79 import com.google.inject.Inject;
80
81
82
83
84
85
86 public abstract class BaseSyndicatorImpl implements Syndicator {
87 private static final Logger log = LoggerFactory.getLogger(BaseSyndicatorImpl.class);
88
89 private final MessageDigest md;
90
91
92
93
94 public static final String DEFAULT_HANDLER = ".magnolia/activation";
95
96 public static final String PARENT_PATH = "mgnlExchangeParentPath";
97
98 public static final String MAPPED_PARENT_PATH = "mgnlExchangeMappedParent";
99
100
101
102
103 public static final String PATH = "mgnlExchangePath";
104
105 public static final String NODE_UUID = "mgnlExchangeNodeUUID";
106
107
108
109
110 @Deprecated
111 public static final String REPOSITORY_NAME = "mgnlExchangeRepositoryName";
112
113 public static final String WORKSPACE_NAME = "mgnlExchangeWorkspaceName";
114
115 public static final String VERSION_NAME = "mgnlExchangeVersionName";
116
117
118
119
120 public static final String RESOURCE_MAPPING_FILE = "mgnlExchangeResourceMappingFile";
121
122 public static final String UTF8_STATUS = "mgnlUTF8Status";
123
124
125
126
127
128
129 public static final String SIBLINGS_ROOT_ELEMENT = "NodeSiblings";
130
131 public static final String SIBLINGS_ELEMENT = "sibling";
132
133 public static final String SIBLING_UUID = "siblingUUID";
134
135 public static final String RESOURCE_MAPPING_FILE_ELEMENT = "File";
136
137 public static final String RESOURCE_MAPPING_NAME_ATTRIBUTE = "name";
138
139 public static final String RESOURCE_MAPPING_UUID_ATTRIBUTE = "contentUUID";
140
141 public static final String RESOURCE_MAPPING_ID_ATTRIBUTE = "resourceId";
142
143 public static final String RESOURCE_MAPPING_MD_ATTRIBUTE = "resourceMD";
144
145 public static final String RESOURCE_MAPPING_ROOT_ELEMENT = "Resources";
146
147 public static final String ACTION = "mgnlExchangeAction";
148
149 public static final String ACTIVATE = "activate";
150
151 public static final String DEACTIVATE = "deactivate";
152
153 public static final String COMMIT = "commit";
154
155 public static final String ROLLBACK = "rollback";
156
157 public static final String CONTENT_FILTER_RULE = "mgnlExchangeFilterRule";
158
159 public static final String ACTIVATION_SUCCESSFUL = "sa_success";
160
161 public static final String ACTIVATION_HANDSHAKE = "sa_handshake";
162
163 public static final String ACTIVATION_FAILED = "sa_failed";
164
165 public static final String ACTIVATION_ATTRIBUTE_STATUS = "sa_attribute_status";
166
167 public static final String ACTIVATION_ATTRIBUTE_MESSAGE = "sa_attribute_message";
168
169 public static final String ACTIVATION_ATTRIBUTE_VERSION = "sa_attribute_version";
170
171 public static final String ACTIVATION_AUTH = "X-magnolia-act-auth";
172 public static final String ACTIVATION_AUTH_KEY = "X-magnolia-act-auth-init";
173
174 public BaseSyndicatorImpl() {
175 try {
176 md = MessageDigest.getInstance("MD5");
177 } catch (NoSuchAlgorithmException e) {
178 throw new SecurityException("In order to proceed with activation please run Magnolia CMS using Java version with MD5 support.", e);
179 }
180 }
181
182
183
184
185
186
187 protected static void executeInPool(Runnable job) throws ExchangeException {
188 try {
189 ThreadPool.getInstance().execute(job);
190 } catch (InterruptedException e) {
191
192
193
194
195 String message = "could not execute job in pool";
196 log.error(message, e);
197 throw new ExchangeException(message, e);
198 }
199 }
200
201
202
203
204
205
206
207
208
209
210 protected static void acquireIgnoringInterruption(Sync latch) {
211 try {
212 latch.acquire();
213 } catch (InterruptedException e) {
214
215 acquireIgnoringInterruption(latch);
216
217 Thread.currentThread().interrupt();
218 }
219 }
220
221
222
223
224 @Deprecated
225 protected String repositoryName;
226
227 protected String workspaceName;
228
229 protected String parent;
230
231 protected Rule contentFilterRule;
232
233 protected User user;
234
235 private Calendar contentVersionDate;
236
237 private MagnoliaConfigurationProperties properties;
238
239 private ResourceCollector resourceCollector;
240
241
242
243
244
245
246
247
248
249 @Override
250 public void init(User user, String repositoryName, String workspaceName, Rule rule) {
251 this.user = user;
252 this.contentFilterRule = rule;
253 this.repositoryName = repositoryName;
254 this.workspaceName = workspaceName;
255 }
256
257
258
259
260
261
262
263
264
265 @Override
266 public void activate(String parent, Content content) throws ExchangeException, RepositoryException {
267 this.activate(parent, content, null);
268 }
269
270
271
272
273
274
275
276
277
278
279
280 @Override
281 public void activate(String parent, Content content, List<String> orderBefore) throws ExchangeException, RepositoryException {
282 this.activate(null, parent, content, orderBefore);
283 }
284
285
286
287
288
289
290
291
292
293
294 @Override
295 public void activate(Subscriber subscriber, String parent, Content content) throws ExchangeException, RepositoryException {
296 this.activate(subscriber, parent, content, null);
297 }
298
299
300
301
302
303
304
305
306
307
308
309 @Override
310 public void activate(Subscriber subscriber, String parent, Content content, List<String> orderBefore) throws ExchangeException, RepositoryException {
311 this.parent = parent;
312 String path = content.getHandle();
313
314 if (content instanceof ContentVersion) {
315 contentVersionDate = ((ContentVersion)content).getCreated();
316 }
317
318 ActivationContent activationContent = null;
319 try {
320 activationContent = resourceCollector.collect(content, orderBefore, parent, workspaceName, repositoryName, contentFilterRule);
321 if (null == subscriber) {
322 this.activate(activationContent, path);
323 } else {
324 this.activate(subscriber, activationContent, path);
325 }
326 if (Boolean.parseBoolean(activationContent.getproperty(ItemType.DELETED_NODE_MIXIN))) {
327 final HierarchyManager hm = content.getHierarchyManager();
328 final Session session = content.getJCRNode().getSession();
329 String uuid = content.getUUID();
330 if (StringUtils.isNotBlank(uuid)) {
331 if (content instanceof ContentVersion) {
332
333 content = hm.getContentByUUID(uuid);
334 }
335 Content parentContent = content.getParent();
336 content.delete();
337 parentContent.save();
338 } else {
339 log.warn("Content {}:{} was already removed.", new String[] {content.getWorkspace().getName(), path});
340 }
341 } else {
342 this.updateActivationDetails(path);
343 }
344 log.info("Exchange: activation succeeded [{}]", path);
345 } catch (Exception e) {
346 if (log.isDebugEnabled()) {
347 log.error("Exchange: activation failed for path:" + ((path != null) ? path : "[null]"), e);
348 long timestamp = System.currentTimeMillis();
349 log.warn("moving files from failed activation to *.failed" + timestamp );
350 Iterator<File> keys = activationContent.getFiles().values().iterator();
351 while (keys.hasNext()) {
352 File f = keys.next();
353 f.renameTo(new File(f.getAbsolutePath()+".failed" + timestamp));
354 }
355 activationContent.getFiles().clear();
356
357 }
358 throw new ExchangeException(e);
359 } finally {
360 log.debug("Cleaning temporary files");
361 cleanTemporaryStore(activationContent);
362 }
363 }
364
365
366
367
368 public abstract void activate(ActivationContent activationContent, String nodePath) throws ExchangeException;
369
370
371
372
373
374 public String activate(Subscriber subscriber, ActivationContent activationContent, String nodePath) throws ExchangeException {
375
376 log.debug("activate");
377 if (null == subscriber) {
378 throw new ExchangeException("Null Subscriber");
379 }
380
381 String parentPath = null;
382
383
384 Subscription subscription = subscriber.getMatchedSubscription(nodePath, this.repositoryName);
385 if (null != subscription) {
386
387 parentPath = this.getMappedPath(this.parent, subscription);
388 activationContent.setProperty(PARENT_PATH, parentPath);
389 } else {
390 log.debug("Exchange : subscriber [{}] is not subscribed to {}", subscriber.getName(), nodePath);
391 return "not subscribed";
392 }
393 log.debug("Exchange : sending activation request to {} with user {}", subscriber.getName(), this.user.getName());
394
395 URLConnection urlConnection = null;
396 String versionName = null;
397 try {
398 urlConnection = prepareConnection(subscriber, getActivationURL(subscriber));
399 versionName = transportActivatedData(activationContent, urlConnection, null);
400
401 String status = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_STATUS);
402
403 if (StringUtils.equals(status, ACTIVATION_HANDSHAKE)) {
404 String handshakeKey = urlConnection.getHeaderField(ACTIVATION_AUTH);
405
406 urlConnection.getContent();
407
408
409 urlConnection = prepareConnection(subscriber, getActivationURL(subscriber));
410
411 versionName = transportActivatedData(activationContent, urlConnection, handshakeKey);
412 status = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_STATUS);
413 }
414
415
416 if (StringUtils.equals(status, ACTIVATION_FAILED)) {
417 String message = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_MESSAGE);
418 throw new ExchangeException("Message received from subscriber: " + message);
419 }
420 urlConnection.getContent();
421 log.debug("Exchange : activation request sent to {}", subscriber.getName());
422 }
423 catch (ExchangeException e) {
424 throw e;
425 }
426 catch (IOException e) {
427 log.debug("Failed to transport following activated content {" + StringUtils.join(activationContent.getProperties().keySet().iterator(), ',') + "} due to " + e.getMessage(), e);
428 String url = (urlConnection == null ? null : urlConnection.getURL().toString());
429 url = SecurityUtil.stripPasswordFromUrl(url);
430
431 throw new ExchangeException("Not able to send the activation request [" + url + "]: " + e.getMessage(), e);
432 }
433 catch (Exception e) {
434 throw new ExchangeException(e);
435 }
436 return versionName;
437 }
438
439 private String transportActivatedData(ActivationContent activationContent, URLConnection urlConnection, String handshakeKey) throws ExchangeException {
440 String versionName;
441 this.addActivationHeaders(urlConnection, activationContent, handshakeKey);
442
443 Transporter.transport((HttpURLConnection) urlConnection, activationContent);
444
445 versionName = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_VERSION);
446 return versionName;
447 }
448
449
450
451
452 protected void cleanTemporaryStore(ActivationContent activationContent) {
453 if (activationContent == null) {
454 log.debug("Clean temporary store - nothing to do");
455 return;
456 }
457 if (log.isDebugEnabled()) {
458 log.debug("Debugging is enabled. Keeping temporary files in store for debugging purposes. Clean the store manually once done with debugging.");
459 return;
460 }
461
462 Iterator<String> keys = activationContent.getFiles().keySet().iterator();
463 while (keys.hasNext()) {
464 String key = keys.next();
465 log.debug("Removing temporary file {}", key);
466 activationContent.getFile(key).delete();
467 }
468 }
469
470 public synchronized void deactivate(String path) throws ExchangeException, RepositoryException {
471 final Content node = getHierarchyManager().getContent(path);
472 deactivate(node);
473 }
474
475
476
477
478
479
480 @Override
481 public synchronized void deactivate(Content node) throws ExchangeException, RepositoryException {
482 String nodeUUID = node.getUUID();
483 String path = node.getHandle();
484 this.doDeactivate(nodeUUID, path);
485 updateDeactivationDetails(nodeUUID);
486 }
487
488
489
490
491
492
493
494 @Override
495 public synchronized void deactivate(Subscriber subscriber, Content node) throws ExchangeException, RepositoryException {
496 String nodeUUID = node.getUUID();
497 String path = node.getHandle();
498 this.doDeactivate(subscriber, nodeUUID, path);
499 updateDeactivationDetails(nodeUUID);
500 }
501
502
503
504
505 public abstract void doDeactivate(String nodeUUID, String nodePath) throws ExchangeException;
506
507
508
509
510
511
512 public abstract String doDeactivate(Subscriber subscriber, String nodeUUID, String nodePath) throws ExchangeException;
513
514
515
516
517
518 protected String getDeactivationURL(Subscriber subscriberInfo) {
519 return getActivationURL(subscriberInfo);
520 }
521
522
523
524
525
526
527
528
529 protected void addDeactivationHeaders(URLConnection connection, String nodeUUID, String handshakeKey) {
530 connection.addRequestProperty(REPOSITORY_NAME, this.repositoryName);
531 connection.addRequestProperty(WORKSPACE_NAME, this.workspaceName);
532
533 String md5 = "";
534 if (nodeUUID != null) {
535 connection.addRequestProperty(NODE_UUID, nodeUUID);
536
537 md5 = SecurityUtil.byteArrayToHex(md.digest(nodeUUID.getBytes()));
538 }
539
540 String pass = System.currentTimeMillis() + ";" + this.user.getName() + ";" + md5;
541
542
543 addHandshakeInfo(connection, handshakeKey);
544
545 connection.setRequestProperty(ACTIVATION_AUTH, SecurityUtil.encrypt(pass));
546 connection.addRequestProperty(ACTION, DEACTIVATE);
547 }
548
549 protected void addHandshakeInfo(URLConnection connection, String handshakeKey) {
550 if (handshakeKey != null) {
551 connection.setRequestProperty(ACTIVATION_AUTH_KEY, SecurityUtil.encrypt(SecurityUtil.getPublicKey(), handshakeKey));
552 }
553 }
554
555
556
557
558 protected String getActivationURL(Subscriber subscriberInfo) {
559 final String url = subscriberInfo.getURL();
560 if (!url.endsWith("/")) {
561 return url + "/" + DEFAULT_HANDLER;
562 }
563 return url + DEFAULT_HANDLER;
564 }
565
566
567
568
569
570
571
572 protected void addActivationHeaders(URLConnection connection, ActivationContent activationContent, String handshakeKey) {
573
574 String md5 = activationContent.getproperty(RESOURCE_MAPPING_MD_ATTRIBUTE);
575 String pass = System.currentTimeMillis() + ";" + this.user.getName() + ";" + md5;
576 activationContent.setProperty(ACTIVATION_AUTH, SecurityUtil.encrypt(pass));
577 Iterator<String> headerKeys = activationContent.getProperties().keySet().iterator();
578 while (headerKeys.hasNext()) {
579 String key = headerKeys.next();
580 if (RESOURCE_MAPPING_MD_ATTRIBUTE.equals(key)) {
581
582 continue;
583 }
584 String value = activationContent.getproperty(key);
585 if(SystemProperty.getBooleanProperty(SystemProperty.MAGNOLIA_UTF8_ENABLED)) {
586 try {
587 value = URLEncoder.encode(value, "UTF-8");
588 }
589 catch (UnsupportedEncodingException e) {
590
591 }
592 }
593 connection.setRequestProperty(key, value);
594 }
595 addHandshakeInfo(connection, handshakeKey);
596 }
597
598
599
600
601 protected void updateActivationDetails(String path) throws RepositoryException {
602
603 Content page = getSystemHierarchyManager().getContent(path);
604 updateMetaData(page, ACTIVATE);
605 page.save();
606 AuditLoggingUtil.log(AuditLoggingUtil.ACTION_ACTIVATE, this.workspaceName, page.getItemType(), path );
607 }
608
609
610
611
612 protected void updateDeactivationDetails(String nodeUUID) throws RepositoryException {
613
614 Content page = getSystemHierarchyManager().getContentByUUID(nodeUUID);
615 updateMetaData(page, DEACTIVATE);
616 page.save();
617 AuditLoggingUtil.log(AuditLoggingUtil.ACTION_DEACTIVATE, this.workspaceName, page.getItemType(), page.getHandle() );
618 }
619
620
621 private HierarchyManager getHierarchyManager() {
622 return MgnlContext.getHierarchyManager(this.workspaceName);
623 }
624
625 private HierarchyManager getSystemHierarchyManager() {
626 return MgnlContext.getSystemContext().getHierarchyManager(this.workspaceName);
627 }
628
629
630
631
632
633 protected void updateMetaData(Content node, String type) throws AccessDeniedException {
634
635 MetaData md = node.getMetaData();
636 if (type.equals(ACTIVATE)) {
637 md.setActivated();
638 }
639 else {
640 md.setUnActivated();
641 }
642 md.setActivatorId(this.user.getName());
643 md.setLastActivationActionDate();
644
645 if(type.equals(ACTIVATE)){
646 if(md.getModificationDate() != null && md.getModificationDate().after(contentVersionDate)){
647 try {
648 Thread.sleep(1);
649 } catch (InterruptedException e) {
650 e.printStackTrace();
651 }
652 md.setModificationDate();
653 }
654 }
655
656 Iterator<Content> children;
657 if (type.equals(ACTIVATE)) {
658
659 children = node.getChildren(new RuleBasedContentFilter(contentFilterRule)).iterator();
660 }
661 else {
662
663 children = node.getChildren(ContentUtil.EXCLUDE_META_DATA_CONTENT_FILTER).iterator();
664 }
665
666 while (children.hasNext()) {
667 Content child = children.next();
668 this.updateMetaData(child, type);
669 }
670
671
672 }
673
674
675
676 protected String getMappedPath(String path, Subscription subscription) {
677 String toURI = subscription.getToURI();
678 if (null != toURI) {
679 String fromURI = subscription.getFromURI();
680
681 fromURI = StringUtils.removeEnd(fromURI, "/");
682 toURI = StringUtils.removeEnd(toURI, "/");
683
684 path = path.replaceFirst(fromURI, toURI);
685 if (path.equals("")) {
686 path = "/";
687 }
688 }
689 return path;
690 }
691
692 protected URLConnection prepareConnection(Subscriber subscriber, String urlString) throws ExchangeException {
693
694
695
696 try {
697 URL url = new URL(urlString);
698 URLConnection urlConnection = url.openConnection();
699 urlConnection.setConnectTimeout(subscriber.getConnectTimeout());
700 urlConnection.setReadTimeout(subscriber.getReadTimeout());
701
702 return urlConnection;
703 } catch (MalformedURLException e) {
704 throw new ExchangeException("Incorrect URL for subscriber " + subscriber + "[" + SecurityUtil.stripPasswordFromUrl(urlString) + "]");
705 } catch (IOException e) {
706 throw new ExchangeException("Not able to send the activation request [" + SecurityUtil.stripPasswordFromUrl(urlString) + "]: " + e.getMessage());
707 } catch (Exception e) {
708 throw new ExchangeException(e);
709 }
710 }
711
712 @Inject
713 public void setResouceCollector(ResourceCollector resourceCollector) {
714 this.resourceCollector = resourceCollector;
715 }
716 }