-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Expand file tree
/
Copy pathPubSubEngine.java
More file actions
2070 lines (1931 loc) · 96.7 KB
/
Copy pathPubSubEngine.java
File metadata and controls
2070 lines (1931 loc) · 96.7 KB
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright (C) 2005-2008 Jive Software, 2017-2026 Ignite Realtime Foundation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.openfire.pubsub;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Interner;
import com.google.common.collect.Interners;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.openfire.*;
import org.jivesoftware.openfire.component.InternalComponentManager;
import org.jivesoftware.openfire.pep.PEPService;
import org.jivesoftware.openfire.pubsub.cluster.RefreshNodeTask;
import org.jivesoftware.openfire.pubsub.models.AccessModel;
import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.util.ImmediateFuture;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.util.TaskEngine;
import org.jivesoftware.util.cache.CacheFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField;
import org.xmpp.packet.*;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
/**
* A PubSubEngine is responsible for handling packets sent to a pub-sub service.
*
* @author Matt Tucker
*/
public class PubSubEngine
{
private static final Logger Log = LoggerFactory.getLogger(PubSubEngine.class);
private static final Interner<JID> userMutex = Interners.newWeakInterner();
private static final String MUTEX_SUFFIX_NODE = " psn";
/**
* The packet router for the server.
*/
private PacketRouter router = null;
public PubSubEngine(PacketRouter router) {
this.router = router;
}
/**
* Handles IQ packets sent to the pubsub service. Requests of disco#info and disco#items
* are not being handled by the engine. Instead the service itself should handle disco packets.
*
* @param service the PubSub service this action is to be performed for.
* @param iq the IQ packet sent to the pubsub service.
* @return <code>null</code> if the IQ packet was not handled by the engine, otherwise a {@link Future} that
* indicates when processing is complete. Processing will be carried out asynchronously if there is the possibility
* of sending a disco#info to a remote server, which could block for up to 60 seconds. If processing is carried out
* synchronously, the returned future completes immediately. Note that the returned future will only return
* <code>null</code> when it completes.
*/
public Future<?> process(final PubSubService service, final IQ iq) {
// Ignore IQs of type ERROR or RESULT
if (IQ.Type.error == iq.getType() || IQ.Type.result == iq.getType()) {
return new ImmediateFuture<>();
}
final Element childElement = iq.getChildElement();
String namespace = null;
if (childElement != null) {
namespace = childElement.getNamespaceURI();
}
if ("http://jabber.org/protocol/pubsub".equals(namespace)) {
Element action = childElement.element("publish");
if (action != null) {
// Entity publishes an item
// Complete this asynchronously, as UserManager::isRegisteredUser(JID) blocks, waiting for a result which may come in on this thread
final Element finalAction = action;
return TaskEngine.getInstance().submit(() -> publishItemsToNode(service, iq, finalAction));
}
action = childElement.element("subscribe");
if (action != null) {
// Entity subscribes to a node
return subscribeNode(service, iq, childElement, action);
}
action = childElement.element("options");
if (action != null) {
if (IQ.Type.get == iq.getType()) {
// Subscriber requests subscription options form
getSubscriptionConfiguration(service, iq, childElement, action);
}
else {
// Subscriber submits completed options form
configureSubscription(service, iq, action);
}
return new ImmediateFuture<>();
}
action = childElement.element("create");
if (action != null) {
// Entity is requesting to create a new node
final Element finalAction = action;
// Complete this asynchronously, as UserManager::isRegisteredUser(JID) blocks, waiting for a result which may come in on this thread
return TaskEngine.getInstance().submit(() -> createNode(service, iq, childElement, finalAction, getPublishOptions( iq )));
}
action = childElement.element("unsubscribe");
if (action != null) {
// Entity unsubscribes from a node
unsubscribeNode(service, iq, action);
return new ImmediateFuture<>();
}
action = childElement.element("subscriptions");
if (action != null) {
// Entity requests all current subscriptions
getSubscriptions(service, iq, childElement);
return new ImmediateFuture<>();
}
action = childElement.element("affiliations");
if (action != null) {
// Entity requests all current affiliations
getAffiliations(service, iq, childElement);
return new ImmediateFuture<>();
}
action = childElement.element("items");
if (action != null) {
// Subscriber requests all active items
getPublishedItems(service, iq, action);
return new ImmediateFuture<>();
}
action = childElement.element("retract");
if (action != null) {
// Entity deletes an item
deleteItems(service, iq, action);
return new ImmediateFuture<>();
}
// Unknown action requested
sendErrorPacket(iq, PacketError.Condition.bad_request, null);
return new ImmediateFuture<>();
}
else if ("http://jabber.org/protocol/pubsub#owner".equals(namespace)) {
Element action = childElement.element("configure");
if (action != null) {
String nodeID = action.attributeValue("node");
if (nodeID == null) {
// if user is not sysadmin then return nodeid-required error
if (!service.isServiceAdmin(iq.getFrom()) ||
!service.isCollectionNodesSupported()) {
// Configure elements must have a node attribute so answer an error
Element pubsubError = DocumentHelper.createElement(QName.get(
"nodeid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return new ImmediateFuture<>();
}
else {
// Sysadmin is trying to configure root collection node
nodeID = service.getRootCollectionNode().getUniqueIdentifier().getNodeId();
}
}
if (IQ.Type.get == iq.getType()) {
// Owner requests configuration form of a node
getNodeConfiguration(service, iq, childElement, nodeID);
}
else {
// Owner submits or cancels node configuration form
configureNode(service, iq, action, nodeID);
}
return new ImmediateFuture<>();
}
action = childElement.element("default");
if (action != null) {
// Owner requests default configuration options for
// leaf or collection nodes
getDefaultNodeConfiguration(service, iq, childElement, action);
return new ImmediateFuture<>();
}
action = childElement.element("delete");
if (action != null) {
// Owner deletes a node
deleteNode(service, iq, action);
return new ImmediateFuture<>();
}
action = childElement.element("subscriptions");
if (action != null) {
if (IQ.Type.get == iq.getType()) {
// Owner requests all affiliated entities
getNodeSubscriptions(service, iq, action);
}
else {
modifyNodeSubscriptions(service, iq, action);
}
return new ImmediateFuture<>();
}
action = childElement.element("affiliations");
if (action != null) {
if (IQ.Type.get == iq.getType()) {
// Owner requests all affiliated entities
getNodeAffiliations(service, iq, action);
}
else {
modifyNodeAffiliations(service, iq, action);
}
return new ImmediateFuture<>();
}
action = childElement.element("purge");
if (action != null) {
// Owner purges items from a node
purgeNode(service, iq, action);
return new ImmediateFuture<>();
}
// Unknown action requested so return error to sender
sendErrorPacket(iq, PacketError.Condition.bad_request, null);
return new ImmediateFuture<>();
}
else if ("http://jabber.org/protocol/commands".equals(namespace)) {
// Process ad-hoc command
IQ reply = service.getManager().process(iq);
router.route(reply);
return new ImmediateFuture<>();
}
return null;
}
/**
* Handles Presence packets sent to the pubsub service. Only process available and not
* available presences.
*
* @param service the PubSub service this action is to be performed for.
* @param presence the Presence packet sent to the pubsub service.
*/
public void process(PubSubService service, Presence presence) {
if (presence.isAvailable()) {
JID subscriber = presence.getFrom();
Map<JID, String> fullPresences = service.getSubscriberPresences().get(subscriber.asBareJID());
if (fullPresences == null) {
synchronized (userMutex.intern(subscriber.asBareJID())) {
fullPresences = service.getSubscriberPresences().get(subscriber.asBareJID());
if (fullPresences == null) {
fullPresences = new ConcurrentHashMap<>();
service.getSubscriberPresences().put(subscriber.asBareJID(), fullPresences);
}
}
}
Presence.Show show = presence.getShow();
fullPresences.put(subscriber, show == null ? "online" : show.name());
}
else if (presence.getType() == Presence.Type.unavailable) {
JID subscriber = presence.getFrom();
Map<JID, String> fullPresences = service.getSubscriberPresences().get(subscriber.asBareJID());
if (fullPresences != null) {
fullPresences.remove(subscriber);
if (fullPresences.isEmpty()) {
service.getSubscriberPresences().remove(subscriber.asBareJID());
}
}
}
}
/**
* Handles Message packets sent to the pubsub service. Messages may be of type error
* when an event notification was sent to a susbcriber whose address is no longer available.<p>
*
* Answers to authorization requests sent to node owners to approve pending subscriptions
* will also be processed by this method.
*
* @param service the PubSub service this action is to be performed for.
* @param message the Message packet sent to the pubsub service.
*/
public void process(PubSubService service, Message message) {
if (message.getType() == Message.Type.error) {
// Process Messages of type error to identify possible subscribers that no longer exist
if (message.getError().getType() == PacketError.Type.cancel) {
// TODO Assuming that owner is the bare JID (as defined in the JEP). This can be replaced with an explicit owner specified in the packet
JID owner = message.getFrom().asBareJID();
// Terminate the subscription of the entity to all nodes hosted at the service
cancelAllSubscriptions(service, owner);
}
else if (message.getError().getType() == PacketError.Type.auth) {
// TODO Queue the message to be sent again later (will retry a few times and
// will be discarded when the retry limit is reached)
}
}
else if (message.getType() == Message.Type.normal) {
// Check that this is an answer to an authorization request
DataForm authForm = (DataForm) message.getExtension("x", "jabber:x:data");
if (authForm != null && authForm.getType() == DataForm.Type.submit) {
String formType = authForm.getField("FORM_TYPE").getValues().get(0);
// Check that completed data form belongs to an authorization request
if ("http://jabber.org/protocol/pubsub#subscribe_authorization".equals(formType)) {
// Process the answer to the authorization request
processAuthorizationAnswer(service, authForm, message);
}
}
}
}
private void publishItemsToNode(PubSubService service, IQ iq, Element publishElement) {
String nodeID = publishElement.attributeValue("node");
Node node;
JID from = iq.getFrom();
// TODO Assuming that owner is the bare JID (as defined in the JEP). This can be replaced with an explicit owner specified in the packet
JID owner = from.asBareJID();
if (nodeID == null) {
// XEP-0060 Section 7.2.3.3 - No node was specified. Return bad_request error
// This suggests that Instant nodes should not be auto-created
Element pubsubError = DocumentHelper.createElement(QName.get("nodeid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
// Optional Publish Options.
final DataForm publishOptions = getPublishOptions( iq );
// Look for the specified node
node = service.getNode(nodeID);
if (node == null) {
if (service instanceof PEPService && service.isServiceAdmin(owner) && canAutoCreate( publishOptions ) ) {
// If it is a PEP service & publisher is service owner - auto create nodes.
CreateNodeResponse response = createNodeHelper(service, iq.getFrom(), iq.getChildElement().element("configure"), publishElement.attributeValue("node"), publishOptions);
if (response.newNode == null) {
// New node creation failed. Since pep#auto-create is advertised
// in disco#info, node creation error should be sent to the client.
sendErrorPacket(iq, response.creationStatus, response.pubsubError);
return;
} else {
// Node creation succeeded, set node to newNode.
node = response.newNode;
}
} else {
// Node does not exist. Return item-not-found error
sendErrorPacket(iq, PacketError.Condition.item_not_found, null);
return;
}
} else {
// Check if the preconditions defined in the publish options (if any) are met.
if ( !nodeMeetsPreconditions( node, publishOptions ) )
{
Element pubsubError = DocumentHelper.createElement(QName.get("precondition-not-met", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.conflict, pubsubError);
return;
}
}
if (!node.getPublisherModel().canPublish(node, owner) && !service.isServiceAdmin(owner)) {
// Entity does not have sufficient privileges to publish to node
sendErrorPacket(iq, PacketError.Condition.forbidden, null);
return;
}
if (node.isCollectionNode()) {
// Node is a collection node. Return feature-not-implemented error
Element pubsubError = DocumentHelper.createElement(
QName.get("unsupported", "http://jabber.org/protocol/pubsub#errors"));
pubsubError.addAttribute("feature", "publish");
sendErrorPacket(iq, PacketError.Condition.feature_not_implemented, pubsubError);
return;
}
LeafNode leafNode = (LeafNode) node;
Iterator<Element> itemElements = publishElement.elementIterator("item");
// Check that an item was included if node persist items or includes payload
if (!itemElements.hasNext() && leafNode.isItemRequired()) {
Element pubsubError = DocumentHelper.createElement(QName.get(
"item-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
// Check that no item was included if node doesn't persist items and doesn't
// includes payload
if (itemElements.hasNext() && !leafNode.isItemRequired()) {
Element pubsubError = DocumentHelper.createElement(QName.get(
"item-forbidden", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
List<Element> items = new ArrayList<>();
List<Element> entries;
Element payload;
while (itemElements.hasNext()) {
Element item = itemElements.next();
entries = item.elements();
payload = entries.isEmpty() ? null : entries.get(0);
// Check that a payload was included if node is configured to include payload
// in notifications
if (payload == null && leafNode.isPayloadDelivered()) {
Element pubsubError = DocumentHelper.createElement(QName.get(
"payload-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
// Check that the payload (if any) contains only one child element
if (entries.size() > 1) {
Element pubsubError = DocumentHelper.createElement(QName.get(
"invalid-payload", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
// Check that the payload size does not exceed the node's configured maximum (XEP-0060 §7.1.3.5)
if (payload != null) {
final int payloadSize = payload.asXML().getBytes(StandardCharsets.UTF_8).length;
if (payloadSize > leafNode.getMaxPayloadSize()) {
Element pubsubError = DocumentHelper.createElement(QName.get(
"payload-too-big", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.not_acceptable, pubsubError);
return;
}
}
items.add(item);
}
// Return success operation
router.route(IQ.createResultIQ(iq));
// Publish item and send event notifications to subscribers
leafNode.publishItems(from, items);
}
/**
* Get the dataform that describes the publish options from the request, or null if no such form was included.
*
* @param iq The publish request (cannot be null).
* @return A publish options data form (possibly null).
*/
public static DataForm getPublishOptions( IQ iq )
{
final Element publishOptionsElement = iq.getChildElement().element( "publish-options" );
if ( publishOptionsElement == null )
{
return null;
}
final Element x = publishOptionsElement.element( QName.get( DataForm.ELEMENT_NAME, DataForm.NAMESPACE ) );
if ( x == null )
{
return null;
}
final DataForm result = new DataForm( x );
if ( result.getType() != DataForm.Type.submit )
{
return null;
}
final FormField formType = result.getField( "FORM_TYPE" );
if ( formType == null || !"http://jabber.org/protocol/pubsub#publish-options".equals( formType.getFirstValue() ) )
{
return null;
}
return result;
}
/**
* Checks if a node is allowed to be auto-created, given the configuration of the service and the optional publish options.
*
* @param publishOptions publish options (can be null)
* @return true if auto-creation of nodes on publish to a non-existent node is allowed, otherwise false.
*/
private boolean canAutoCreate( DataForm publishOptions )
{
// Since pep#auto-create is advertised in disco#info in hard-code, this is always allowed, unless the publish options explicitly forbid this.
if ( publishOptions == null )
{
return true;
}
final FormField field = publishOptions.getField( "pubsub#auto-create" );
if ( field == null )
{
return true;
}
final String firstValue = field.getFirstValue();
return "1".equals( firstValue ) || "true".equalsIgnoreCase( firstValue );
}
/**
* Checks whether the configuration of a node satisfies the supplied preconditions.
*
* This method is used to evaluate the "publish-options as preconditions" flow of XEP-0060 (§7.1.5):
* a publisher supplies a data form naming the configuration fields it requires the (existing) node to
* have, and publishing is only allowed if the node already meets them.
*
* For the preconditions to be met, the node's configuration must, for every precondition field, contain
* a field with the same variable name whose value(s) include all of the value(s) required by the
* precondition. The node may have additional values beyond those required; only the absence of a
* required value causes rejection. Value comparison is order-independent: a field's values are treated
* as a set rather than an ordered list.
*
* Comparison follows the boolean equivalences defined by XEP-0004: the values {@code "true"} and
* {@code "1"} are considered equal, as are {@code "false"} and {@code "0"}.
*
* A precondition is not satisfied if the node configuration is missing the named field entirely, or if
* any value required by the precondition is absent from the node's value set (after boolean
* normalization). The {@code FORM_TYPE} field is ignored and never compared.
*
* @param node The node whose configuration is checked (cannot be null).
* @param preconditions The preconditions to check against. May be null, in which case {@code true} is
* returned (no preconditions to satisfy).
* @return {@code true} if every precondition is met, otherwise {@code false}.
*/
@VisibleForTesting
static boolean nodeMeetsPreconditions(final Node node, final DataForm preconditions)
{
if ( preconditions == null )
{
return true;
}
final DataForm conditions = node.getConfigurationForm(null);
if ( conditions == null )
{
// No configuration to match against; any non-FORM_TYPE precondition cannot be met.
return preconditions.getFields().stream().allMatch( f -> "FORM_TYPE".equals( f.getVariable() ) );
}
for ( final FormField precondition : preconditions.getFields() )
{
if ( "FORM_TYPE".equals( precondition.getVariable() ) )
{
continue;
}
final FormField condition = conditions.getField( precondition.getVariable() );
if ( condition == null )
{
// Node config does not define this field. Reject.
return false;
}
final Set<String> nodeValues = normalizeValues( condition );
final Set<String> requiredValues = normalizeValues( precondition );
if ( !nodeValues.containsAll( requiredValues ) )
{
return false;
}
}
return true;
}
private static Set<String> normalizeValues(final FormField field)
{
final Set<String> normalized = new HashSet<>();
if ( field == null )
{
return normalized;
}
for ( final String value : field.getValues() )
{
if ( value == null )
{
continue;
}
switch ( value )
{
case "true": normalized.add( "1" ); break;
case "false": normalized.add( "0" ); break;
default: normalized.add( value );
}
}
return normalized;
}
private void deleteItems(PubSubService service, IQ iq, Element retractElement) {
String nodeID = retractElement.attributeValue("node");
Node node;
if (nodeID == null) {
// No node was specified. Return bad_request error
Element pubsubError = DocumentHelper.createElement(QName.get(
"nodeid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
else {
// Look for the specified node
node = service.getNode(nodeID);
if (node == null) {
// Node does not exist. Return item-not-found error
sendErrorPacket(iq, PacketError.Condition.item_not_found, null);
return;
}
}
// Get the items to delete
Iterator<Element> itemElements = retractElement.elementIterator("item");
if (!itemElements.hasNext()) {
Element pubsubError = DocumentHelper.createElement(QName.get(
"item-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
if (node.isCollectionNode()) {
// Cannot delete items from a collection node. Return an error.
Element pubsubError = DocumentHelper.createElement(QName.get(
"unsupported", "http://jabber.org/protocol/pubsub#errors"));
pubsubError.addAttribute("feature", "persistent-items");
sendErrorPacket(iq, PacketError.Condition.feature_not_implemented, pubsubError);
return;
}
LeafNode leafNode = (LeafNode) node;
if (!leafNode.isItemRequired()) {
// Cannot delete items from a leaf node that doesn't handle itemIDs. Return an error.
Element pubsubError = DocumentHelper.createElement(QName.get(
"unsupported", "http://jabber.org/protocol/pubsub#errors"));
pubsubError.addAttribute("feature", "persistent-items");
sendErrorPacket(iq, PacketError.Condition.feature_not_implemented, pubsubError);
return;
}
List<PublishedItem> items = new ArrayList<>();
while (itemElements.hasNext()) {
Element itemElement = (Element) itemElements.next();
String itemID = itemElement.attributeValue("id");
if (itemID != null) {
PublishedItem item = node.getPublishedItem(itemID);
if (item == null) {
// ItemID does not exist. Return item-not-found error
sendErrorPacket(iq, PacketError.Condition.item_not_found, null);
return;
}
else {
if (item.canDelete(iq.getFrom())) {
items.add(item);
}
else {
// Publisher does not have sufficient privileges to delete this item
sendErrorPacket(iq, PacketError.Condition.forbidden, null);
return;
}
}
}
else {
// No item ID was specified so return a bad_request error
Element pubsubError = DocumentHelper.createElement(QName.get(
"item-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
}
// Send reply with success
router.route(IQ.createResultIQ(iq));
// Delete items and send subscribers a notification
leafNode.deleteItems(items);
}
private Future<?> subscribeNode(final PubSubService service, final IQ iq, final Element childElement, Element subscribeElement) {
String nodeID = subscribeElement.attributeValue("node");
final Node node;
if (nodeID == null) {
if (service.isCollectionNodesSupported()) {
// Entity subscribes to root collection node
node = service.getRootCollectionNode();
}
else {
// Service does not have a root collection node so return a nodeid-required error
Element pubsubError = DocumentHelper.createElement(QName.get(
"nodeid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return new ImmediateFuture<>();
}
}
else {
// Look for the specified node
node = service.getNode(nodeID);
if (node == null) {
// Node does not exist. Return item-not-found error
sendErrorPacket(iq, PacketError.Condition.item_not_found, null);
return new ImmediateFuture<>();
}
}
// Check if sender and subscriber JIDs match or if a valid "trusted proxy" is being used
final JID from = iq.getFrom();
final JID subscriberJID = new JID(subscribeElement.attributeValue("jid"));
if (!from.toBareJID().equals(subscriberJID.toBareJID()) && !service.isServiceAdmin(from)) {
// JIDs do not match and requestor is not a service admin so return an error
Element pubsubError = DocumentHelper.createElement(
QName.get("invalid-jid", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return new ImmediateFuture<>();
}
// TODO Assumed that the owner of the subscription is the bare JID of the subscription JID. Waiting StPeter answer for explicit field.
final JID owner = subscriberJID.asBareJID();
// Check if the node's access model allows the subscription to proceed
final AccessModel accessModel = node.getAccessModel();
if (!accessModel.canSubscribe(node, owner, subscriberJID)) {
sendErrorPacket(iq, accessModel.getSubsriptionError(),
accessModel.getSubsriptionErrorDetail());
return new ImmediateFuture<>();
}
// Complete this asynchronously, as UserManager::isRegisteredUser(JID) blocks, waiting for a result which may come in on this thread
return TaskEngine.getInstance().submit(() -> subscribeNodeAsync(iq, subscriberJID, node, owner, service, from, childElement, accessModel));
}
private void subscribeNodeAsync(final IQ iq, final JID subscriberJID, final Node node, final JID owner, final PubSubService service, final JID from, final Element childElement, final AccessModel accessModel) {
// Check if the subscriber is an anonymous user.
if (SessionManager.getInstance().isAnonymousClientSession(subscriberJID)) {
// Anonymous users cannot subscribe to the node. Return forbidden error
// TODO OF-2506: figure out why anonymous users should not be allowed to subscribe. There is no way to check if remote users are anonymous anyway.
sendErrorPacket(iq, PacketError.Condition.forbidden, null);
return;
}
// Check if the subscription owner is a user with outcast affiliation
NodeAffiliate nodeAffiliate = node.getAffiliate(owner);
if (nodeAffiliate != null &&
nodeAffiliate.getAffiliation() == NodeAffiliate.Affiliation.outcast) {
// Subscriber is an outcast. Return forbidden error
sendErrorPacket(iq, PacketError.Condition.forbidden, null);
return;
}
// Check that subscriptions to the node are enabled
if (!node.isSubscriptionEnabled() && !service.isServiceAdmin(from)) {
// Sender is not a sysadmin and subscription is disabled so return an error
sendErrorPacket(iq, PacketError.Condition.not_allowed, null);
return;
}
// Get any configuration form included in the options element (if any)
DataForm optionsForm = null;
Element options = childElement.element("options");
if (options != null) {
Element formElement = options.element(QName.get("x", "jabber:x:data"));
if (formElement != null) {
optionsForm = new DataForm(formElement);
}
}
// Attempt to create a subscription and an affiliation, assuming none exist or duplicates are permissible.
node.createSubscription(iq, owner, subscriberJID, accessModel.isAuthorizationRequired(), optionsForm);
}
private void unsubscribeNode(PubSubService service, IQ iq, Element unsubscribeElement) {
String nodeID = unsubscribeElement.attributeValue("node");
String subID = unsubscribeElement.attributeValue("subid");
String jidAttribute = unsubscribeElement.attributeValue("jid");
// Check if the specified JID has a subscription with the node
if (jidAttribute == null) {
// No JID was specified so return an error indicating that jid is required
Element pubsubError = DocumentHelper.createElement(
QName.get("jid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
Node node;
if (nodeID == null) {
if (service.isCollectionNodesSupported()) {
// Entity unsubscribes from root collection node
node = service.getRootCollectionNode();
}
else {
// Service does not have a root collection node so return a nodeid-required error
Element pubsubError = DocumentHelper.createElement(QName.get(
"nodeid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
}
else {
// Look for the specified node
node = service.getNode(nodeID);
if (node == null) {
// Node does not exist. Return item-not-found error
sendErrorPacket(iq, PacketError.Condition.item_not_found, null);
return;
}
}
final JID subscriberJID = new JID(jidAttribute);
final NodeSubscription subscription = resolveSubscriptionOrError(iq, node, subID, subscriberJID);
if (subscription == null) {
return; // An appropriate error response has already been sent by resolveSubscriptionOrError().
}
JID from = iq.getFrom();
// Check that unsubscriptions to the node are enabled
if (!node.isSubscriptionEnabled() && !service.isServiceAdmin(from)) {
// Sender is not a sysadmin and unsubscription is disabled so return an error
sendErrorPacket(iq, PacketError.Condition.not_allowed, null);
return;
}
// A subscription was found so check if the user is allowed to cancel the subscription
if (!subscription.canModify(from)) {
// Requestor is prohibited from unsubscribing entity
sendErrorPacket(iq, PacketError.Condition.forbidden, null);
return;
}
// Cancel subscription
node.cancelSubscription(subscription);
// Send reply with success
router.route(IQ.createResultIQ(iq));
}
/**
* Resolves a subscription on the specified node for the provided subscriber.
*
* If a subscription ID is provided, this method verifies that it identifies an existing subscription on the node
* and returns that subscription. Otherwise, the subscription is inferred from the subscriber JID:
*
* <ul>
* <li>If the JID has exactly one subscription on the node, that subscription is returned.</li>
* <li>If the JID has no subscriptions, a {@code not-subscribed} PubSub error is returned.</li>
* <li>If the JID has multiple subscriptions, a {@code subid-required} PubSub error is returned.</li>
* </ul>
*
* When a provided subscription ID does not correspond to an existing subscription, an {@code invalid-subid} PubSub
* error is returned.
*
* @param iq the IQ stanza to which any error response should be sent.
* @param node the node on which the subscription is to be resolved.
* @param subID the subscription ID to resolve, or {@code null} to resolve the subscription based on the subscriber JID.
* @param subscriberJID the JID of the subscriber
* @return the resolved subscription, or {@code null} if no unique subscription could be resolved. In that case,
* an appropriate error response has already been sent.
*/
private NodeSubscription resolveSubscriptionOrError(@Nonnull final IQ iq, @Nonnull final Node node, @Nullable final String subID, @Nonnull final JID subscriberJID)
{
NodeSubscription subscription;
if (subID != null)
{
// Check if the specified subID belongs to an existing node subscription
subscription = node.getSubscription(subID);
if (subscription == null) {
Element pubsubError = DocumentHelper.createElement(QName.get("invalid-subid", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.not_acceptable, pubsubError);
return null;
}
// XEP-0060 6.2.3.5 SubID does not match JID
if (!subscription.getJID().equals(subscriberJID)) {
Element pubsubError = DocumentHelper.createElement(QName.get("invalid-subid", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.not_acceptable, pubsubError);
return null;
}
}
else
{
final Collection<NodeSubscription> subscriptionsByJID = node.getSubscriptionsByJID(subscriberJID);
switch (subscriptionsByJID.size()) {
case 0:
sendErrorPacket(iq, PacketError.Condition.unexpected_request, DocumentHelper.createElement(QName.get("not-subscribed", "http://jabber.org/protocol/pubsub#errors")));
return null;
case 1:
// Only one subscription exists for the specified JID, so use that one.
subscription = subscriptionsByJID.iterator().next();
break;
default:
// No subid was specified, and the node has multiple subscriptions.
sendErrorPacket(iq, PacketError.Condition.bad_request, DocumentHelper.createElement(QName.get("subid-required", "http://jabber.org/protocol/pubsub#errors")));
return null;
}
}
return subscription;
}
private void getSubscriptionConfiguration(PubSubService service, IQ iq,
Element childElement, Element optionsElement) {
String nodeID = optionsElement.attributeValue("node");
String subID = optionsElement.attributeValue("subid");
String jidAttribute = optionsElement.attributeValue("jid");
if (jidAttribute == null) {
// No JID was specified so return an error indicating that jid is required
Element pubsubError = DocumentHelper.createElement(
QName.get("jid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
Node node;
if (nodeID == null) {
if (service.isCollectionNodesSupported()) {
// Entity requests subscription options of root collection node
node = service.getRootCollectionNode();
}
else {
// Service does not have a root collection node so return a nodeid-required error
Element pubsubError = DocumentHelper.createElement(QName.get(
"nodeid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
}
else {
// Look for the specified node
node = service.getNode(nodeID);
if (node == null) {
// Node does not exist. Return item-not-found error
sendErrorPacket(iq, PacketError.Condition.item_not_found, null);
return;
}
}
final JID subscriberJID = new JID(jidAttribute);
final NodeSubscription subscription = resolveSubscriptionOrError(iq, node, subID, subscriberJID);
if (subscription == null) {
return; // An appropriate error response has already been sent by resolveSubscriptionOrError().
}
// A subscription was found so check if the user is allowed to get the subscription options
if (!subscription.canModify(iq.getFrom())) {
// Requestor is prohibited from getting the subscription options
sendErrorPacket(iq, PacketError.Condition.forbidden, null);
return;
}
// Return data form containing subscription configuration to the subscriber
final Locale preferredLocale = SessionManager.getInstance().getLocaleForSession(iq.getFrom());
IQ reply = IQ.createResultIQ(iq);
Element replyChildElement = childElement.createCopy();
reply.setChildElement(replyChildElement);
replyChildElement.element("options").add(subscription.getConfigurationForm(preferredLocale).getElement());
router.route(reply);
}
private void configureSubscription(PubSubService service, IQ iq, Element optionsElement) {
String nodeID = optionsElement.attributeValue("node");
String subID = optionsElement.attributeValue("subid");
String jidAttribute = optionsElement.attributeValue("jid");
if (jidAttribute == null) {
// No JID was specified so return an error indicating that jid is required
Element pubsubError = DocumentHelper.createElement(
QName.get("jid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
Node node;
if (nodeID == null) {
if (service.isCollectionNodesSupported()) {
// Entity submits new subscription options of root collection node
node = service.getRootCollectionNode();
}
else {
// Service does not have a root collection node so return a nodeid-required error
Element pubsubError = DocumentHelper.createElement(QName.get(
"nodeid-required", "http://jabber.org/protocol/pubsub#errors"));
sendErrorPacket(iq, PacketError.Condition.bad_request, pubsubError);
return;
}
}
else {
// Look for the specified node
node = service.getNode(nodeID);
if (node == null) {
// Node does not exist. Return item-not-found error
sendErrorPacket(iq, PacketError.Condition.item_not_found, null);
return;
}
}
final JID subscriberJID = new JID(jidAttribute);
final NodeSubscription subscription = resolveSubscriptionOrError(iq, node, subID, subscriberJID);
if (subscription == null) {
return; // An appropriate error response has already been sent by resolveSubscriptionOrError().
}
// A subscription was found so check if the user is allowed to submits
// new subscription options
if (!subscription.canModify(iq.getFrom())) {
// Requestor is prohibited from setting new subscription options
sendErrorPacket(iq, PacketError.Condition.forbidden, null);
return;
}
Element formElement = optionsElement.element(QName.get("x", "jabber:x:data"));
if (formElement != null) {
// Change the subscription configuration based on the completed form
subscription.configure(iq, new DataForm(formElement));
}
else {
// No data form was included so return bad request error