// // MLBasePaser.m // monalxmpp // // Created by Anurodh Pokharel on 4/11/20. // Copyright © 2020 Monal.im. All rights reserved. // #import "MLConstants.h" #import "MLBasePaser.h" //#define DebugParser(...) DDLogDebug(__VA_ARGS__) #define DebugParser(...) @interface MLXMLNode() @property (atomic, readwrite) MLXMLNode* parent; -(MLXMLNode*) addChildNodeWithoutCopy:(MLXMLNode*) child; @end @interface MLBasePaser () { //this stak is needed to hold strong references to all nodes until they are dispatched to our _completion callback //(the parent references of the MLXMLNodes are weak and don't hold the parents alive) NSMutableArray* _currentStack; stanza_completion_t _completion; NSMutableArray* _namespacePrefixes; } @end @implementation MLBasePaser -(id) initWithCompletion:(stanza_completion_t) completion { self = [super init]; _completion = completion; return self; } -(void) reset { _currentStack = [NSMutableArray new]; } -(void) parserDidStartDocument:(NSXMLParser*) parser { DDLogInfo(@"Document start"); [self reset]; } -(void) parser:(NSXMLParser*) parser didStartMappingPrefix:(NSString*) prefix toURI:(NSString*) namespaceURI { DebugParser(@"Got new namespace prefix mapping for '%@' to '%@'...", prefix, namespaceURI); } -(void) parser:(NSXMLParser*) parser didEndMappingPrefix:(NSString*) prefix { DebugParser(@"Namespace prefix '%@' now out of scope again...", prefix); } -(void) parser:(NSXMLParser*) parser didStartElement:(NSString*) elementName namespaceURI:(NSString*) namespaceURI qualifiedName:(NSString*) qName attributes:(NSDictionary*) attributeDict { NSInteger depth = [_currentStack count] + 1; //this makes the depth in here equal to the depth in didEndElement: DebugParser(@"Started element: %@ :: %@ (%@) depth %ld", elementName, namespaceURI, qName, depth); //use appropriate MLXMLNode child classes for iq, message and presence stanzas MLXMLNode* newNode; if(depth == 2 && [elementName isEqualToString:@"iq"] && [namespaceURI isEqualToString:@"jabber:client"]) newNode = [XMPPIQ alloc]; else if(depth == 2 && [elementName isEqualToString:@"message"] && [namespaceURI isEqualToString:@"jabber:client"]) newNode = [XMPPMessage alloc]; else if(depth == 2 && [elementName isEqualToString:@"presence"] && [namespaceURI isEqualToString:@"jabber:client"]) newNode = [XMPPPresence alloc]; else if(depth >= 3 && [elementName isEqualToString:@"x"] && [namespaceURI isEqualToString:@"jabber:x:data"]) newNode = [XMPPDataForm alloc]; else newNode = [MLXMLNode alloc]; newNode = [newNode initWithElement:elementName andNamespace:namespaceURI withAttributes:attributeDict andChildren:@[] andData:nil]; DebugParser(@"Current stack: %@", _currentStack); //add new node to tree (each node needs a prototype MLXMLNode element and a mutable string to hold its future //char data added to the MLXMLNode when the xml element is closed newNode.parent = [_currentStack lastObject][@"node"]; [_currentStack addObject:@{@"node": newNode, @"charData": [NSMutableString new]}]; } -(void) parser:(NSXMLParser*) parser foundCharacters:(NSString*) string { DebugParser(@"Got new xml character data: '%@'", string); NSInteger depth = [_currentStack count]; if(depth == 0) { DDLogError(@"Got xml character data outside of any element!"); [self fakeStreamError]; return; } [[_currentStack lastObject][@"charData"] appendString:string]; DebugParser(@"_currentCharData is now: '%@'", [_currentStack lastObject][@"charData"]); } -(void) parser:(NSXMLParser*) parser didEndElement:(NSString*) elementName namespaceURI:(NSString*) namespaceURI qualifiedName:(NSString*) qName { NSInteger depth = [_currentStack count]; NSDictionary* topmostStackElement = [_currentStack lastObject]; MLXMLNode* currentNode = ((MLXMLNode*)topmostStackElement[@"node"]); if([topmostStackElement[@"charData"] length]) currentNode.data = [topmostStackElement[@"charData"] copy]; DebugParser(@"Ended element: %@ :: %@ (%@) depth %ld", elementName, namespaceURI, qName, depth); MLXMLNode* parent = currentNode.parent; if(parent) { DebugParser(@"Ascending from child %@ to parent %@", currentNode.element, parent.element); if(depth > 2) //don't add all received stanzas/nonzas as childs to our stream header (that would create a memory leak!) { DebugParser(@"Adding %@ to parent %@", currentNode.element, parent.element); [parent addChildNodeWithoutCopy:currentNode]; } } [_currentStack removeLastObject]; //only call completion for stanzas, not for inner elements inside stanzas and not for our outermost stream start element if(depth == 2) _completion(currentNode); } -(void) parserDidEndDocument:(NSXMLParser*) parser { DDLogInfo(@"Document end"); } -(void) parser:(NSXMLParser*) parser foundIgnorableWhitespace:(NSString*) whitespaceString { DebugParser(@"Found ignorable whitespace: '%@'", whitespaceString); } -(void) parser:(NSXMLParser*) parser parseErrorOccurred:(NSError*) parseError { DDLogError(@"XML parse error occurred: line: %ld , col: %ld desc: %@ ",(long)[parser lineNumber], (long)[parser columnNumber], [parseError localizedDescription]); [self fakeStreamError]; } -(void) fakeStreamError { //fake stream error and let xmpp.m handle it _completion([[MLXMLNode alloc] initWithElement:@"error" andNamespace:@"http://etherx.jabber.org/streams" withAttributes:@{} andChildren:@[ [[MLXMLNode alloc] initWithElement:@"bad-format" andNamespace:@"urn:ietf:params:xml:ns:xmpp-streams" withAttributes:@{} andChildren:@[ [[MLXMLNode alloc] initWithElement:@"text" andNamespace:@"urn:ietf:params:xml:ns:xmpp-streams" withAttributes:@{} andChildren:@[] andData:@"Could not parse XML coming from server"] ] andData:nil] ] andData:nil]); } @end