event.rs

  1// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
  2//
  3// This Source Code Form is subject to the terms of the Mozilla Public
  4// License, v. 2.0. If a copy of the MPL was not distributed with this
  5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6
  7use crate::data_forms::DataForm;
  8use crate::date::DateTime;
  9use crate::util::error::Error;
 10use crate::ns;
 11use crate::pubsub::{ItemId, NodeName, Subscription, SubscriptionId, Item as PubSubItem};
 12use jid::Jid;
 13use minidom::Element;
 14use std::convert::TryFrom;
 15
 16/// Event wrapper for a PubSub `<item/>`.
 17#[derive(Debug, Clone)]
 18pub struct Item(pub PubSubItem);
 19
 20impl_pubsub_item!(Item, PUBSUB_EVENT);
 21
 22/// Represents an event happening to a PubSub node.
 23#[derive(Debug, Clone)]
 24pub enum PubSubEvent {
 25    /*
 26    Collection {
 27    },
 28    */
 29    /// This node’s configuration changed.
 30    Configuration {
 31        /// The node affected.
 32        node: NodeName,
 33
 34        /// The new configuration of this node.
 35        form: Option<DataForm>,
 36    },
 37
 38    /// This node has been deleted, with an optional redirect to another node.
 39    Delete {
 40        /// The node affected.
 41        node: NodeName,
 42
 43        /// The xmpp: URI of another node replacing this one.
 44        redirect: Option<String>,
 45    },
 46
 47    /// Some items have been published on this node.
 48    PublishedItems {
 49        /// The node affected.
 50        node: NodeName,
 51
 52        /// The list of published items.
 53        items: Vec<Item>,
 54    },
 55
 56    /// Some items have been removed from this node.
 57    RetractedItems {
 58        /// The node affected.
 59        node: NodeName,
 60
 61        /// The list of retracted items.
 62        items: Vec<ItemId>,
 63    },
 64
 65    /// All items of this node just got removed at once.
 66    Purge {
 67        /// The node affected.
 68        node: NodeName,
 69    },
 70
 71    /// The user’s subscription to this node has changed.
 72    Subscription {
 73        /// The node affected.
 74        node: NodeName,
 75
 76        /// The time at which this subscription will expire.
 77        expiry: Option<DateTime>,
 78
 79        /// The JID of the user affected.
 80        jid: Option<Jid>,
 81
 82        /// An identifier for this subscription.
 83        subid: Option<SubscriptionId>,
 84
 85        /// The state of this subscription.
 86        subscription: Option<Subscription>,
 87    },
 88}
 89
 90fn parse_items(elem: Element, node: NodeName) -> Result<PubSubEvent, Error> {
 91    let mut is_retract = None;
 92    let mut items = vec![];
 93    let mut retracts = vec![];
 94    for child in elem.children() {
 95        if child.is("item", ns::PUBSUB_EVENT) {
 96            match is_retract {
 97                None => is_retract = Some(false),
 98                Some(false) => (),
 99                Some(true) => {
100                    return Err(Error::ParseError(
101                        "Mix of item and retract in items element.",
102                    ));
103                }
104            }
105            items.push(Item::try_from(child.clone())?);
106        } else if child.is("retract", ns::PUBSUB_EVENT) {
107            match is_retract {
108                None => is_retract = Some(true),
109                Some(true) => (),
110                Some(false) => {
111                    return Err(Error::ParseError(
112                        "Mix of item and retract in items element.",
113                    ));
114                }
115            }
116            check_no_children!(child, "retract");
117            check_no_unknown_attributes!(child, "retract", ["id"]);
118            let id = get_attr!(child, "id", Required);
119            retracts.push(id);
120        } else {
121            return Err(Error::ParseError("Invalid child in items element."));
122        }
123    }
124    Ok(match is_retract {
125        Some(false) => PubSubEvent::PublishedItems { node, items },
126        Some(true) => PubSubEvent::RetractedItems {
127            node,
128            items: retracts,
129        },
130        None => return Err(Error::ParseError("Missing children in items element.")),
131    })
132}
133
134impl TryFrom<Element> for PubSubEvent {
135    type Error = Error;
136
137    fn try_from(elem: Element) -> Result<PubSubEvent, Error> {
138        check_self!(elem, "event", PUBSUB_EVENT);
139        check_no_attributes!(elem, "event");
140
141        let mut payload = None;
142        for child in elem.children() {
143            let node = get_attr!(child, "node", Required);
144            if child.is("configuration", ns::PUBSUB_EVENT) {
145                let mut payloads = child.children().cloned().collect::<Vec<_>>();
146                let item = payloads.pop();
147                if !payloads.is_empty() {
148                    return Err(Error::ParseError(
149                        "More than a single payload in configuration element.",
150                    ));
151                }
152                let form = match item {
153                    None => None,
154                    Some(payload) => Some(DataForm::try_from(payload)?),
155                };
156                payload = Some(PubSubEvent::Configuration { node, form });
157            } else if child.is("delete", ns::PUBSUB_EVENT) {
158                let mut redirect = None;
159                for item in child.children() {
160                    if item.is("redirect", ns::PUBSUB_EVENT) {
161                        if redirect.is_some() {
162                            return Err(Error::ParseError(
163                                "More than one redirect in delete element.",
164                            ));
165                        }
166                        let uri = get_attr!(item, "uri", Required);
167                        redirect = Some(uri);
168                    } else {
169                        return Err(Error::ParseError("Unknown child in delete element."));
170                    }
171                }
172                payload = Some(PubSubEvent::Delete { node, redirect });
173            } else if child.is("items", ns::PUBSUB_EVENT) {
174                payload = Some(parse_items(child.clone(), node)?);
175            } else if child.is("purge", ns::PUBSUB_EVENT) {
176                check_no_children!(child, "purge");
177                payload = Some(PubSubEvent::Purge { node });
178            } else if child.is("subscription", ns::PUBSUB_EVENT) {
179                check_no_children!(child, "subscription");
180                payload = Some(PubSubEvent::Subscription {
181                    node,
182                    expiry: get_attr!(child, "expiry", Option),
183                    jid: get_attr!(child, "jid", Option),
184                    subid: get_attr!(child, "subid", Option),
185                    subscription: get_attr!(child, "subscription", Option),
186                });
187            } else {
188                return Err(Error::ParseError("Unknown child in event element."));
189            }
190        }
191        Ok(payload.ok_or(Error::ParseError("No payload in event element."))?)
192    }
193}
194
195impl From<PubSubEvent> for Element {
196    fn from(event: PubSubEvent) -> Element {
197        let payload = match event {
198            PubSubEvent::Configuration { node, form } => Element::builder("configuration")
199                .ns(ns::PUBSUB_EVENT)
200                .attr("node", node)
201                .append_all(form.map(Element::from)),
202            PubSubEvent::Delete { node, redirect } => Element::builder("purge")
203                .ns(ns::PUBSUB_EVENT)
204                .attr("node", node)
205                .append_all(redirect.map(|redirect| {
206                    Element::builder("redirect")
207                        .ns(ns::PUBSUB_EVENT)
208                        .attr("uri", redirect)
209                })),
210            PubSubEvent::PublishedItems { node, items } => Element::builder("items")
211                .ns(ns::PUBSUB_EVENT)
212                .attr("node", node)
213                .append_all(items.into_iter()),
214            PubSubEvent::RetractedItems { node, items } => Element::builder("items")
215                .ns(ns::PUBSUB_EVENT)
216                .attr("node", node)
217                .append_all(
218                    items
219                        .into_iter()
220                        .map(|id| {
221                            Element::builder("retract")
222                                .ns(ns::PUBSUB_EVENT)
223                                .attr("id", id)
224                        })
225                ),
226            PubSubEvent::Purge { node } => Element::builder("purge")
227                .ns(ns::PUBSUB_EVENT)
228                .attr("node", node),
229            PubSubEvent::Subscription {
230                node,
231                expiry,
232                jid,
233                subid,
234                subscription,
235            } => Element::builder("subscription")
236                .ns(ns::PUBSUB_EVENT)
237                .attr("node", node)
238                .attr("expiry", expiry)
239                .attr("jid", jid)
240                .attr("subid", subid)
241                .attr("subscription", subscription),
242        };
243        Element::builder("event")
244            .ns(ns::PUBSUB_EVENT)
245            .append(payload)
246            .build()
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use crate::util::compare_elements::NamespaceAwareCompare;
254    use std::str::FromStr;
255
256    #[test]
257    fn missing_items() {
258        let elem: Element =
259            "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='coucou'/></event>"
260                .parse()
261                .unwrap();
262        let error = PubSubEvent::try_from(elem).unwrap_err();
263        let message = match error {
264            Error::ParseError(string) => string,
265            _ => panic!(),
266        };
267        assert_eq!(message, "Missing children in items element.");
268    }
269
270    #[test]
271    fn test_simple_items() {
272        let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='coucou'><item id='test' publisher='test@coucou'/></items></event>".parse().unwrap();
273        let event = PubSubEvent::try_from(elem).unwrap();
274        match event {
275            PubSubEvent::PublishedItems { node, items } => {
276                assert_eq!(node, NodeName(String::from("coucou")));
277                assert_eq!(items[0].id, Some(ItemId(String::from("test"))));
278                assert_eq!(
279                    items[0].publisher,
280                    Some(Jid::from_str("test@coucou").unwrap())
281                );
282                assert_eq!(items[0].payload, None);
283            }
284            _ => panic!(),
285        }
286    }
287
288    #[test]
289    fn test_simple_pep() {
290        let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='something'><item><foreign xmlns='example:namespace'/></item></items></event>".parse().unwrap();
291        let event = PubSubEvent::try_from(elem).unwrap();
292        match event {
293            PubSubEvent::PublishedItems { node, items } => {
294                assert_eq!(node, NodeName(String::from("something")));
295                assert_eq!(items[0].id, None);
296                assert_eq!(items[0].publisher, None);
297                match items[0].payload {
298                    Some(ref elem) => assert!(elem.is("foreign", "example:namespace")),
299                    _ => panic!(),
300                }
301            }
302            _ => panic!(),
303        }
304    }
305
306    #[test]
307    fn test_simple_retract() {
308        let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='something'><retract id='coucou'/><retract id='test'/></items></event>".parse().unwrap();
309        let event = PubSubEvent::try_from(elem).unwrap();
310        match event {
311            PubSubEvent::RetractedItems { node, items } => {
312                assert_eq!(node, NodeName(String::from("something")));
313                assert_eq!(items[0], ItemId(String::from("coucou")));
314                assert_eq!(items[1], ItemId(String::from("test")));
315            }
316            _ => panic!(),
317        }
318    }
319
320    #[test]
321    fn test_simple_delete() {
322        let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><delete node='coucou'><redirect uri='hello'/></delete></event>".parse().unwrap();
323        let event = PubSubEvent::try_from(elem).unwrap();
324        match event {
325            PubSubEvent::Delete { node, redirect } => {
326                assert_eq!(node, NodeName(String::from("coucou")));
327                assert_eq!(redirect, Some(String::from("hello")));
328            }
329            _ => panic!(),
330        }
331    }
332
333    #[test]
334    fn test_simple_purge() {
335        let elem: Element =
336            "<event xmlns='http://jabber.org/protocol/pubsub#event'><purge node='coucou'/></event>"
337                .parse()
338                .unwrap();
339        let event = PubSubEvent::try_from(elem).unwrap();
340        match event {
341            PubSubEvent::Purge { node } => {
342                assert_eq!(node, NodeName(String::from("coucou")));
343            }
344            _ => panic!(),
345        }
346    }
347
348    #[test]
349    fn test_simple_configure() {
350        let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><configuration node='coucou'><x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/pubsub#node_config</value></field></x></configuration></event>".parse().unwrap();
351        let event = PubSubEvent::try_from(elem).unwrap();
352        match event {
353            PubSubEvent::Configuration { node, form: _ } => {
354                assert_eq!(node, NodeName(String::from("coucou")));
355                //assert_eq!(form.type_, Result_);
356            }
357            _ => panic!(),
358        }
359    }
360
361    #[test]
362    fn test_invalid() {
363        let elem: Element =
364            "<event xmlns='http://jabber.org/protocol/pubsub#event'><coucou node='test'/></event>"
365                .parse()
366                .unwrap();
367        let error = PubSubEvent::try_from(elem).unwrap_err();
368        let message = match error {
369            Error::ParseError(string) => string,
370            _ => panic!(),
371        };
372        assert_eq!(message, "Unknown child in event element.");
373    }
374
375    #[cfg(not(feature = "disable-validation"))]
376    #[test]
377    fn test_invalid_attribute() {
378        let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event' coucou=''/>"
379            .parse()
380            .unwrap();
381        let error = PubSubEvent::try_from(elem).unwrap_err();
382        let message = match error {
383            Error::ParseError(string) => string,
384            _ => panic!(),
385        };
386        assert_eq!(message, "Unknown attribute in event element.");
387    }
388
389    #[test]
390    fn test_ex221_subscription() {
391        let elem: Element = r#"
392<event xmlns='http://jabber.org/protocol/pubsub#event'>
393  <subscription
394      expiry='2006-02-28T23:59:59+00:00'
395      jid='francisco@denmark.lit'
396      node='princely_musings'
397      subid='ba49252aaa4f5d320c24d3766f0bdcade78c78d3'
398      subscription='subscribed'/>
399</event>
400"#
401        .parse()
402        .unwrap();
403        let event = PubSubEvent::try_from(elem.clone()).unwrap();
404        match event.clone() {
405            PubSubEvent::Subscription {
406                node,
407                expiry,
408                jid,
409                subid,
410                subscription,
411            } => {
412                assert_eq!(node, NodeName(String::from("princely_musings")));
413                assert_eq!(
414                    subid,
415                    Some(SubscriptionId(String::from(
416                        "ba49252aaa4f5d320c24d3766f0bdcade78c78d3"
417                    )))
418                );
419                assert_eq!(subscription, Some(Subscription::Subscribed));
420                assert_eq!(jid, Some(Jid::from_str("francisco@denmark.lit").unwrap()));
421                assert_eq!(expiry, Some("2006-02-28T23:59:59Z".parse().unwrap()));
422            }
423            _ => panic!(),
424        }
425
426        let elem2: Element = event.into();
427        assert!(elem.compare_to(&elem2));
428    }
429}