// // XMLNode.m // Monal // // Created by Anurodh Pokharel on 6/29/13. // // #include #import "MLXMLNode.h" #import "HelperTools.h" #import "XMPPIQ.h" #import "XMPPMessage.h" #import "XMPPPresence.h" #import "XMPPDataForm.h" @import UIKit.UIApplication; //#define DEBUG_XMLQueryLanguage 1 //this is the required prototype from Holger's snprintf.c int rpl_vasprintf(char **, const char *, va_list *); @interface MLXMLNode() { NSMutableArray* _children; } @property (nonatomic, strong) NSCache* cache; @property (nonatomic, strong) NSCache* queryEntryCache; @property (atomic, strong, readwrite) NSString* element; @property (atomic, readwrite) NSMutableDictionary* attributes; @property (atomic, weak, readwrite) MLXMLNode* parent; @end @implementation MLXMLNode static NSRegularExpression* pathSplitterRegex; static NSRegularExpression* componentParserRegex; static NSRegularExpression* attributeFilterRegex; #ifdef QueryStatistics static NSMutableDictionary* statistics; #endif +(void) initialize { #ifdef QueryStatistics statistics = [NSMutableDictionary new]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(nowIdle:) name:kMonalIdle object:nil]; #endif //compile regexes only once (see https://unicode-org.github.io/icu/userguide/strings/regexp.html for syntax) pathSplitterRegex = [NSRegularExpression regularExpressionWithPattern:@"^(/?(\\{(\\*|[^}]+)\\})?([!a-zA-Z0-9_:-]+|\\*|\\.\\.)?((\\<[^=~]+[=~][^>]+\\>)*))((/((\\{(\\*|[^}]+)\\})?([!a-zA-Z0-9_:-]+|\\*|\\.\\.)?((\\<[^=~]+[=~][^>]+\\>)*)))*)((@[a-zA-Z0-9_:-]+|@@|#|\\$|\\\\[^\\\\]+\\\\)(\\|(bool|int|uint|double|datetime|base64|uuid|uuidcast))?)?$" options:NSRegularExpressionCaseInsensitive error:nil]; componentParserRegex = [NSRegularExpression regularExpressionWithPattern:@"^(\\{(\\*|[^}]+)\\})?([!a-zA-Z0-9_:-]+|\\*|\\.\\.)?((\\<[^=~]+[=~][^>]+\\>)*)((@[a-zA-Z0-9_:-]+|@@|#|\\$|\\\\[^\\\\]+\\\\)(\\|(bool|int|uint|double|datetime|base64|uuid|uuidcast))?)?$" options:NSRegularExpressionCaseInsensitive error:nil]; attributeFilterRegex = [NSRegularExpression regularExpressionWithPattern:@"\\<([^=~]+)([=~])([^>]+)\\>" options:NSRegularExpressionCaseInsensitive error:nil]; // testcases for stanza // SCRAM-SHA-1PLAINSCRAM-SHA-1-PLUS // [self print_debug:@"/*" inTree:parsedStanza]; // [self print_debug:@"{*}*" inTree:parsedStanza]; // [self print_debug:@"{*}*/*@xmlns" inTree:parsedStanza]; // [self print_debug:@"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms" inTree:parsedStanza]; // [self print_debug:@"{*}*@xmlns" inTree:parsedStanza]; // [self print_debug:@"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms/mechanism" inTree:parsedStanza]; // [self print_debug:@"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms/mechanism#" inTree:parsedStanza]; // [self print_debug:@"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms/*#" inTree:parsedStanza]; // [self print_debug:@"{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms/*@xmlns" inTree:parsedStanza]; // [self print_debug:@"/.." inTree:parsedStanza]; // [self print_debug:@"/../*" inTree:parsedStanza]; // [self print_debug:@"mechanisms/mechanism#" inTree:parsedStanza]; // [self print_debug:@"{jabber:client}iq@@" inTree:parsedStanza]; } +(void) nowIdle:(NSNotification*) notification { #ifdef QueryStatistics NSMutableDictionary* sortedStatistics = [NSMutableDictionary new]; @synchronized(statistics) { NSArray* sortedKeys = [statistics keysSortedByValueUsingComparator: ^(id obj1, id obj2) { if([obj1 integerValue] > [obj2 integerValue]) return (NSComparisonResult)NSOrderedDescending; if([obj1 integerValue] < [obj2 integerValue]) return (NSComparisonResult)NSOrderedAscending; return (NSComparisonResult)NSOrderedSame; }]; for(NSString* key in sortedKeys) DDLogDebug(@"STATISTICS: %@ = %@", key, statistics[key]); //sortedStatistics[key] = statistics[key]; } //DDLogDebug(@"XML QUERY STATISTICS: %@", sortedStatistics); #endif } -(void) internalInit { _attributes = [NSMutableDictionary new]; _children = [NSMutableArray new]; _parent = nil; _data = nil; _element = @""; self.cache = [NSCache new]; self.queryEntryCache = [NSCache new]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMemoryPressureNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; } -(id) init { self = [super init]; [self internalInit]; return self; } -(id) initWithElement:(NSString*) element { self = [super init]; [self internalInit]; _element = [element copy]; return self; } -(id) initWithElement:(NSString*) element andNamespace:(NSString*) xmlns { self = [self initWithElement:element]; [self setXMLNS:xmlns]; return self; } -(id) initWithElement:(NSString*) element andNamespace:(NSString*) xmlns withAttributes:(NSDictionary*) attributes andChildren:(NSArray*) children andData:(NSString*) data { self = [self initWithElement:element withAttributes:attributes andChildren:children andData:data]; [self setXMLNS:xmlns]; return self; } -(id) initWithElement:(NSString*) element withAttributes:(NSDictionary*) attributes andChildren:(NSArray*) children andData:(NSString*) data { self = [self initWithElement:element]; [_attributes addEntriesFromDictionary:[[NSDictionary alloc] initWithDictionary:attributes copyItems:YES]]; for(MLXMLNode* child in children) [self addChildNode:child]; _data = nil; if(data) _data = [data copy]; return self; } -(id) initWithElement:(NSString*) element andData:(NSString* _Nullable) data { self = [self initWithElement:element withAttributes:@{} andChildren:@[] andData:data]; return self; } -(id) initWithElement:(NSString*) element andNamespace:(NSString*) xmlns andData:(NSString* _Nullable) data { self = [self initWithElement:element withAttributes:@{} andChildren:@[] andData:data]; [self setXMLNS:xmlns]; return self; } -(void) dealloc { /* #ifdef IS_ALPHA DDLogVerbose(@"Dealloc of MLXMLNode: %@", self); #endif */ [[NSNotificationCenter defaultCenter] removeObserver:self]; [self.cache removeAllObjects]; [self.queryEntryCache removeAllObjects]; } -(id) initWithCoder:(NSCoder*) decoder { self = [super init]; if(!self) return nil; [self internalInit]; _element = [decoder decodeObjectOfClass:[NSString class] forKey:@"element"]; _attributes = [decoder decodeObjectOfClasses:[[NSSet alloc] initWithArray:@[[NSMutableDictionary class], [NSDictionary class], [NSMutableString class], [NSString class]]] forKey:@"attributes"]; NSArray* decodedChildren = [decoder decodeObjectOfClasses:[[NSSet alloc] initWithArray:@[[NSMutableArray class], [NSArray class], [MLXMLNode class], [XMPPIQ class], [XMPPMessage class], [XMPPPresence class], [XMPPDataForm class]]] forKey:@"children"]; for(MLXMLNode* child in decodedChildren) [self addChildNodeWithoutCopy:child]; _data = [decoder decodeObjectOfClass:[NSString class] forKey:@"data"]; return self; } -(void) encodeWithCoder:(NSCoder*) encoder { [encoder encodeObject:_element forKey:@"element"]; [encoder encodeObject:_attributes forKey:@"attributes"]; [encoder encodeObject:_children forKey:@"children"]; [encoder encodeObject:_data forKey:@"data"]; } +(BOOL) supportsSecureCoding { return YES; } -(id) copyWithZone:(NSZone*) zone { MLXMLNode* copy = [[[self class] alloc] initWithElement:[_element copy]]; copy.attributes = [[NSMutableDictionary alloc] initWithDictionary:_attributes copyItems:YES]; for(MLXMLNode* child in _children) [copy addChildNode:child]; copy.data = _data ? [_data copy] : nil; return copy; } -(id) shallowCopy { return [self shallowCopyWithData:NO]; } -(id) shallowCopyWithData:(BOOL) copyData { MLXMLNode* copy = [[[self class] alloc] initWithElement:[_element copy]]; copy.attributes = [[NSMutableDictionary alloc] initWithDictionary:_attributes copyItems:YES]; if(copyData) copy.data = _data ? [_data copy] : nil; return copy; } -(void) handleMemoryPressureNotification { [self.cache removeAllObjects]; [self.queryEntryCache removeAllObjects]; DDLogVerbose(@"Removed all cached objects in this MLXMLNode due to memory pressure"); DDLogVerbose(@"Node: %@", self); } -(void) setXMLNS:(NSString*) xmlns { [_attributes setObject:[xmlns copy] forKey:@"xmlns"]; } -(MLXMLNode*) addChildNode:(MLXMLNode*) child { if(nilExtractor(child) == nil) return nil; return [self addChildNodeWithoutCopy:[child copy]]; } //only used by MLBaseParser to add new childs without deep-copying the object -(MLXMLNode*) addChildNodeWithoutCopy:(MLXMLNode*) child { if(!child) return nil; MLXMLNode* insertedChild = child; insertedChild.parent = self; //namespace inheritance (will be stripped by XMLString later on) //we do this here to make sure manual created nodes always have a namespace like the nodes created by the xml parser do if(!insertedChild.attributes[@"xmlns"]) insertedChild.attributes[@"xmlns"] = [_attributes[@"xmlns"] copy]; [_children addObject:insertedChild]; [self invalidateUpstreamCache]; //this one can be removed if the query path component ".." is removed from our language [insertedChild invalidateDownstreamCache]; return insertedChild; } -(MLXMLNode*) removeChildNode:(MLXMLNode*) child { MLXMLNode* foundChild = nil; if(!child) return foundChild; NSInteger index = [_children indexOfObject:child]; if(index != NSNotFound) { foundChild = [_children objectAtIndex:index]; foundChild.parent = nil; [_children removeObjectAtIndex:index]; [self invalidateUpstreamCache]; } return foundChild; } -(NSArray*) children { return [NSArray arrayWithArray:_children]; } -(void) invalidateUpstreamCache { //invalidate caches of all nodes upstream in our tree for(MLXMLNode* node = self; node; node = node.parent) [node.cache removeAllObjects]; } -(void) invalidateDownstreamCache { [self.cache removeAllObjects]; for(MLXMLNode* node in _children) [node invalidateDownstreamCache]; } //query language similar to the one prosody uses (which in turn is loosely based on xpath) //this implements a strict superset of prosody's language which makes it possible to use queries from prosody directly //unlinke the language used in prosody, this returns *all* nodes mathching the query (use findFirst to get only the first match like prosody does) //see https://prosody.im/doc/developers/util/stanza (function stanza:find(path)) for examples and description //extensions to prosody's language: //we extended this language to automatically infer the namespace from the parent element, if no namespace was given explicitly in the query //we also added support for "*" as element name or namespace meaning "any nodename" / "any namespace" //the additional ".." element name can be used to ascend to the parent node and do a find() on this node using the rest of the query path //if you begin a path with "/" that means "begin with checking the current element", if your path does not begin with a "/" //this means "begin witch checking the children of this node" (normal prosody behaviour) //we also added additional extraction commands ("@attrName" and "#" are extraction commands defined within prosody): //extraction command "$" returns the name of the XML element (just like "#" returns its text content) //the argument "@" for extraction command "@" returns the full attribute dictionary of the XML element (full command: "@@") //we also added conversion commands that can be appended to a query string: //"|bool" --> convert xml string to NSNumber containing bool (XMPP defines "1"/"true" to be true and "0"/"false" to be false) //"|int" --> convert xml string to NSNumber containing NSInteger //"|uint" --> convert xml string to NSNumber containing NSUInteger //"|double" --> convert xml string to NSNumber containing double //"|datetime" --> convert xml datetime string to NSDate //"|base64" --> convert base64 encoded xml string to NSData //"|uuid" --> interprete xml string as NSUUID //"|uuidcast" --> try to interprete xml string as UUID and convert xml string to NSUUID via sha256 transformation, if not -(NSArray*) find:(NSString* _Nonnull) queryString, ... NS_FORMAT_FUNCTION(1, 2) { va_list args; va_start(args, queryString); NSArray* retval = [self find:queryString arguments:&args]; va_end(args); return retval; } //like find: above, but only return the first match -(id) findFirst:(NSString* _Nonnull) queryString, ... NS_FORMAT_FUNCTION(1, 2) { va_list args; va_start(args, queryString); id retval = [self find:queryString arguments:&args].firstObject; va_end(args); return retval; } //like findFirst, but only check if it would return something -(BOOL) check:(NSString* _Nonnull) queryString, ... NS_FORMAT_FUNCTION(1, 2) { va_list args; va_start(args, queryString); BOOL retval = [self find:queryString arguments:&args].firstObject != nil ? YES : NO; va_end(args); return retval; } -(NSArray*) find:(NSString* _Nonnull) queryString arguments:(va_list*) args { //return our own node if the query string is empty (this makes queries like "/.." possible which will return the parent node if(!queryString || [queryString isEqualToString:@""]) return @[self]; va_list cacheKeyArgs; va_copy(cacheKeyArgs, *args); NSString* cacheKey = [NSString stringWithFormat:@"%@§§%@", queryString, [[NSString alloc] initWithFormat:queryString arguments:cacheKeyArgs]]; va_end(cacheKeyArgs); #ifdef DEBUG_XMLQueryLanguage DDLogVerbose(@"Cache key: %@", cacheKey); #endif //return results from cache if possible NSArray* cacheObj = nil; WeakContainer* cacheEntryContainer = [self.cache objectForKey:cacheKey]; if(cacheEntryContainer != nil) cacheObj = cacheEntryContainer.obj; if(cacheObj != nil) { #ifdef DEBUG_XMLQueryLanguage DDLogVerbose(@"Returning cached result: %@", cacheObj); #endif return cacheObj; } #ifdef QueryStatistics @synchronized(statistics) { if(!statistics[queryString]) statistics[queryString] = @0; statistics[queryString] = [NSNumber numberWithInteger:[statistics[queryString] integerValue] + 1]; } #endif //shortcut syntax for queries operating directly on this node //this translates "/@attr", "/#" or "/$" into their correct form "/{*}*@attr", "/{*}*#" or "/{*}*$" if( [queryString characterAtIndex:0] == '/' && [queryString length] >=2 && [[NSCharacterSet characterSetWithCharactersInString:@"@#$<"] characterIsMember:[queryString characterAtIndex:1]] ) queryString = [NSString stringWithFormat:@"/{*}*%@", [queryString substringFromIndex:1]]; NSArray* results; //check if the current element our our children should be queried "/" makes the path "absolute" instead of "relative" if([[queryString substringToIndex:1] isEqualToString:@"/"]) results = [self find:[queryString substringFromIndex:1] inNodeList:@[self] arguments:args]; //absolute path (check self first) else results = [self find:queryString inNodeList:_children arguments:args]; //relative path (check childs first) //update cache and return results [self.cache setObject:[[WeakContainer alloc] initWithObj:results] forKey:cacheKey]; //use weak container to break retain circle return results; } -(NSArray*) find:(NSString* _Nonnull) queryString inNodeList:(NSArray* _Nonnull) nodesToCheck arguments:(va_list*) args { //shortcut for empty nodesToCheck if(![nodesToCheck count]) return @[]; NSMutableArray* results = [NSMutableArray new]; //split our path into first component and rest NSArray* matches = [pathSplitterRegex matchesInString:queryString options:0 range:NSMakeRange(0, [queryString length])]; if(![matches count]) @throw [NSException exceptionWithName:@"RuntimeException" reason:@"XML query has syntax errors (no matches for path splitter regex)!" userInfo:@{ @"self": self, @"queryString": queryString, }]; NSTextCheckingResult* match = matches.firstObject; NSRange pathComponent1Range = [match rangeAtIndex:1]; NSRange pathComponent2Range = [match rangeAtIndex:7]; NSRange pathComponent3Range = [match rangeAtIndex:15]; NSString* pathComponent1 = @""; NSString* pathComponent2 = @""; NSString* pathComponent3 = @""; if(pathComponent1Range.location != NSNotFound && pathComponent1Range.length > 0) pathComponent1 = [queryString substringWithRange:pathComponent1Range]; if(pathComponent2Range.location != NSNotFound && pathComponent2Range.length > 0) pathComponent2 = [queryString substringWithRange:pathComponent2Range]; if(pathComponent3Range.location != NSNotFound && pathComponent3Range.length > 0) pathComponent3 = [queryString substringWithRange:pathComponent3Range]; NSString* pathComponent = pathComponent1; NSString* rest = @""; if(![pathComponent2 length]) pathComponent = [NSString stringWithFormat:@"%@%@", pathComponent1, pathComponent3]; else rest = [NSString stringWithFormat:@"%@%@", [pathComponent2 substringFromIndex:1], pathComponent3]; NSMutableDictionary* parsedEntry = [self parseQueryEntry:pathComponent arguments:args]; //check if the parent element was selected and ask our parent to check the rest of our query path if needed if([pathComponent isEqualToString:@".."]) { MLXMLNode* parent = self.parent; if(!parent) @throw [NSException exceptionWithName:@"RuntimeException" reason:@"XML query tries to ascend to non-existent parent element!" userInfo:@{ @"self": self, @"queryString": queryString, @"pathComponent": pathComponent, @"parsedEntry": parsedEntry }]; return [parent find:rest arguments:args]; } //shortcut for dataform subqueries: allow empty element names and namespaces, they get autofilled with {jabber:x:data}x if(!parsedEntry[@"elementName"] && !parsedEntry[@"namespace"] && [parsedEntry[@"extractionCommand"] isEqualToString:@"\\"]) { parsedEntry[@"elementName"] = @"x"; parsedEntry[@"namespace"] = @"jabber:x:data"; } if(!parsedEntry[@"elementName"] && !parsedEntry[@"namespace"]) @throw [NSException exceptionWithName:@"RuntimeException" reason:@"XML queries must not contain a path component having neither element name nor namespace!" userInfo:@{ @"self": self, @"queryString": queryString, @"pathComponent": pathComponent, @"parsedEntry": parsedEntry }]; //"*" is just syntactic sugar for an empty element name //(but empty element names are not allowed if no namespace was given, which makes this sugar mandatory in this case) if(parsedEntry[@"elementName"] && [parsedEntry[@"elementName"] isEqualToString:@"*"]) [parsedEntry removeObjectForKey:@"elementName"]; //if no namespace was given, use the parent one (no namespace means the namespace will be inherited) //this will allow all namespaces "{*}" if the nodes in nodesToCheck don't have a parent at all if((!parsedEntry[@"namespace"] || [parsedEntry[@"namespace"] isEqualToString:@""]) && ((MLXMLNode*)nodesToCheck[0]).parent) parsedEntry[@"namespace"] = ((MLXMLNode*)nodesToCheck[0]).parent.attributes[@"xmlns"]; //all nodesToCheck have the same parent, just pick the first one //"*" is just syntactic sugar for an empty namespace name which means "any namespace allowed" //(but empty namespaces are only allowed in internal methods, which makes this sugar mandatory) //don't confuse this with a query without namespace which will result in a query using the parent's namespace, not "any namespace allowed"! if(parsedEntry[@"namespace"] && [parsedEntry[@"namespace"] isEqualToString:@"*"]) [parsedEntry removeObjectForKey:@"namespace"]; //element names can be negated BOOL negatedElementName = NO; if(parsedEntry[@"elementName"] && [parsedEntry[@"elementName"] characterAtIndex:0] == '!') { negatedElementName = YES; parsedEntry[@"elementName"] = [parsedEntry[@"elementName"] substringFromIndex:1]; } //iterate through nodesToCheck (containing only us, our parent's children or our own children) //and check if they match the current path component (e.g. parsedEntry) for(MLXMLNode* node in nodesToCheck) { //check element name and namespace (if given) if( ( (negatedElementName && ![parsedEntry[@"elementName"] isEqualToString:node.element]) || (!parsedEntry[@"elementName"] || [parsedEntry[@"elementName"] isEqualToString:node.element]) ) && (!parsedEntry[@"namespace"] || [parsedEntry[@"namespace"] isEqualToString:node.attributes[@"xmlns"]]) ) { //check for attribute filters (if given) if(parsedEntry[@"attributeFilters"] && [parsedEntry[@"attributeFilters"] count]) { BOOL ok = YES; for(NSDictionary* filter in parsedEntry[@"attributeFilters"]) { if(node.attributes[filter[@"name"]]) { NSArray* matches = [filter[@"value"] matchesInString:node.attributes[filter[@"name"]] options:0 range:NSMakeRange(0, [node.attributes[filter[@"name"]] length])]; if(![matches count]) { ok = NO; //this node does *not* fullfill the attribute filter regex break; } } else { ok = NO; break; } } if(!ok) continue; //this node does *not* fullfill the attribute filter regex } //check if we should process an extraction command (only allowed if we're at the end of the query) if(parsedEntry[@"extractionCommand"]) { //sanity check if([rest length] > 0) @throw [NSException exceptionWithName:@"RuntimeException" reason:@"Extraction commands are only allowed for terminal nodes of XML queries!" userInfo:@{ @"self": self, @"node": node, @"queryString": queryString, @"pathComponent": pathComponent, @"parsedEntry": parsedEntry }]; id singleResult = nil; if([parsedEntry[@"extractionCommand"] isEqualToString:@"#"] && node.data) singleResult = [self processConversionCommand:parsedEntry[@"conversionCommand"] forXMLString:node.data]; else if([parsedEntry[@"extractionCommand"] isEqualToString:@"@"] && node.attributes[parsedEntry[@"attribute"]]) singleResult = [self processConversionCommand:parsedEntry[@"conversionCommand"] forXMLString:node.attributes[parsedEntry[@"attribute"]]]; else if([parsedEntry[@"extractionCommand"] isEqualToString:@"$"] && node.element) singleResult = [self processConversionCommand:parsedEntry[@"conversionCommand"] forXMLString:node.element]; else if([parsedEntry[@"extractionCommand"] isEqualToString:@"\\"]) { if(![node respondsToSelector:NSSelectorFromString(@"processDataFormQuery:")]) @throw [NSException exceptionWithName:@"RuntimeException" reason:@"Data form extractions can only be used on data forms! This exception means you have a bug somewhere else in your code (probably at the source of the element you are trying to use in your data form query)!" userInfo:@{ @"self": self, @"node": node, @"queryString": queryString, @"pathComponent": pathComponent, @"parsedEntry": parsedEntry }]; //faster than NSMethodInvocation, but way less readable, see https://stackoverflow.com/a/20058585/3528174 id extraction = ((id (*)(id, SEL, NSString*))[node methodForSelector:NSSelectorFromString(@"processDataFormQuery:")])(node, NSSelectorFromString(@"processDataFormQuery:"), parsedEntry[@"dataFormQuery"]); if(extraction) //only add this to our results if the data form query succeeded { //check if we try to operate a conversion command on something not a single extracted simple form field of type NSString if(parsedEntry[@"conversionCommand"] && ![extraction isKindOfClass:[NSString class]]) @throw [NSException exceptionWithName:@"RuntimeException" reason:@"Conversion commands can not be used on data form extractions returning the whole data form or an NSArray/NSDictionary!" userInfo:@{ @"self": self, @"node": node, @"queryString": queryString, @"pathComponent": pathComponent, @"parsedEntry": parsedEntry }]; singleResult = [self processConversionCommand:parsedEntry[@"conversionCommand"] forXMLString:(NSString*)extraction]; } } else if([parsedEntry[@"extractionCommand"] isEqualToString:@"@"] && [parsedEntry[@"attribute"] isEqualToString:@"@"]) { if(parsedEntry[@"conversionCommand"]) @throw [NSException exceptionWithName:@"RuntimeException" reason:@"Conversion commands can not be used on attribute dict extractions (e.g. extraction command '@@')!" userInfo:@{ @"self": self, @"node": node, @"queryString": queryString, @"pathComponent": pathComponent, @"parsedEntry": parsedEntry }]; singleResult = node.attributes; } if(singleResult) [results addObject:singleResult]; } else { if(parsedEntry[@"conversionCommand"]) @throw [NSException exceptionWithName:@"RuntimeException" reason:@"Conversion commands are only allowed for terminal nodes of XML queries that use an extraction command!" userInfo:@{ @"self": self, @"node": node, @"queryString": queryString, @"pathComponent": pathComponent, @"parsedEntry": parsedEntry }]; if([rest length] > 0) //we should descent to this node [results addObjectsFromArray:[node find:rest arguments:args]]; //this will cache the subquery on this node, too else //we should not descent to this node (we reached the end of our query) [results addObject:node]; } } } //DDLogVerbose(@"*** DEBUG(%@)[%@] ***\n%@\n%@\n%@", queryString, pathComponent, parsedEntry, results, nodesToCheck); return [results copy]; //return readonly copy of results } -(id) processConversionCommand:(NSString*) command forXMLString:(NSString* _Nonnull) string { if(!string) return nil; if([command isEqualToString:@"bool"]) { //xml bools as defined in xmpp core RFC if([string isEqualToString:@"1"] || [string isEqualToString:@"true"]) return @YES; else if([string isEqualToString:@"0"] || [string isEqualToString:@"false"]) return @NO; else return @NO; //no bool at all, return false } else if([command isEqualToString:@"int"]) return [NSNumber numberWithInteger:(NSInteger)[string integerValue]]; else if([command isEqualToString:@"uint"]) return [NSNumber numberWithUnsignedInteger:(NSUInteger)[string longLongValue]]; else if([command isEqualToString:@"double"]) return [NSNumber numberWithDouble:[string doubleValue]]; else if([command isEqualToString:@"datetime"]) return [HelperTools parseDateTimeString:string]; else if([command isEqualToString:@"base64"]) return [HelperTools dataWithBase64EncodedString:string]; else if([command isEqualToString:@"uuid"]) return [[NSUUID alloc] initWithUUIDString:string]; else if([command isEqualToString:@"uuidcast"]) { NSUUID* uuid = [[NSUUID alloc] initWithUUIDString:string]; if(uuid != nil) return uuid; return [HelperTools stringToUUID:string]; } else return string; } -(NSMutableDictionary*) parseQueryEntry:(NSString* _Nonnull) entry arguments:(va_list*) args { va_list cacheKeyArgs; va_copy(cacheKeyArgs, *args); NSString* cacheKey = [NSString stringWithFormat:@"%@§§%@", entry, [[NSString alloc] initWithFormat:entry arguments:cacheKeyArgs]]; va_end(cacheKeyArgs); #ifdef DEBUG_XMLQueryLanguage DDLogVerbose(@"Cache key: %@", cacheKey); #endif //return results from cache if possible NSDictionary* cacheEntry = [self.queryEntryCache objectForKey:cacheKey]; if(cacheEntry != nil) { #ifdef DEBUG_XMLQueryLanguage DDLogDebug(@"Returning cached result: %@", cacheEntry); #endif return [cacheEntry mutableCopy]; } NSMutableDictionary* retval = [NSMutableDictionary new]; NSArray* matches = [componentParserRegex matchesInString:entry options:0 range:NSMakeRange(0, [entry length])]; if(![matches count]) @throw [NSException exceptionWithName:@"RuntimeException" reason:@"Could not parse path component!" userInfo:@{ @"self": self, @"queryEntry": entry }]; NSTextCheckingResult* match = matches.firstObject; NSRange namespaceRange = [match rangeAtIndex:2]; NSRange elementNameRange = [match rangeAtIndex:3]; NSRange attributeFilterRange = [match rangeAtIndex:4]; NSRange extractionCommandRange = [match rangeAtIndex:7]; NSRange conversionCommandRange = [match rangeAtIndex:9]; if(namespaceRange.location != NSNotFound) retval[@"namespace"] = [entry substringWithRange:namespaceRange]; if(elementNameRange.location != NSNotFound) retval[@"elementName"] = [entry substringWithRange:elementNameRange]; if(attributeFilterRange.location != NSNotFound && attributeFilterRange.length > 0) { retval[@"attributeFilters"] = [NSMutableArray new]; NSString* attributeFilters = [entry substringWithRange:attributeFilterRange]; #ifdef DEBUG_XMLQueryLanguage DDLogDebug(@"Extracting attribute filters: '%@'...", attributeFilters); #endif NSArray* attributeFilterMatches = [attributeFilterRegex matchesInString:attributeFilters options:0 range:NSMakeRange(0, [attributeFilters length])]; for(NSTextCheckingResult* attributeFilterMatch in attributeFilterMatches) { NSRange attributeFilterNameRange = [attributeFilterMatch rangeAtIndex:1]; NSRange attributeFilterTypeRange = [attributeFilterMatch rangeAtIndex:2]; NSRange attributeFilterValueRange = [attributeFilterMatch rangeAtIndex:3]; if(attributeFilterNameRange.location == NSNotFound || attributeFilterTypeRange.location == NSNotFound || attributeFilterValueRange.location == NSNotFound) @throw [NSException exceptionWithName:@"RuntimeException" reason:@"Attribute filter not complete!" userInfo:@{ @"self": self, @"queryEntry": entry, @"attributeFilters": attributeFilters }]; NSString* attributeFilterName = [attributeFilters substringWithRange:attributeFilterNameRange]; unichar attributeFilterType = [[attributeFilters substringWithRange:attributeFilterTypeRange] characterAtIndex:0]; NSString* attributeFilterValue = [attributeFilters substringWithRange:attributeFilterValueRange]; NSString* attributeFilterValueRegexPattern; if(attributeFilterType == '=') //verbatim comparison using format string interpolation { //substitute format string specifiers inside of our attribute filter string. //use Holger's vsnprintf() which got pimped up to support %@ format specifier. //use va_list* everywhere to make sure we move the same va_list pointer in every invocation here //instead of starting with a fresh copy (which would always extract only the first variadic argument //regardless of the position in the format string we are at). char* dest = NULL; if(rpl_vasprintf(&dest, [attributeFilterValue UTF8String], args) == -1) [NSException raise:@"NSInternalInconsistencyException" format:@"failed malloc in MLXMLNode's usage of rpl_vasprintf" arguments:nil]; MLAssert(dest != NULL, @"dest should *never* be NULL!"); NSString* unescapedAttributeFilterValue = [NSString stringWithUTF8String:dest]; free(dest); NSString* escapedAttributeFilterValue = [NSRegularExpression escapedPatternForString:unescapedAttributeFilterValue]; attributeFilterValueRegexPattern = [NSString stringWithFormat:@"^%@$", escapedAttributeFilterValue]; #ifdef DEBUG_XMLQueryLanguage DDLogDebug(@"unescapedAttributeFilterValue: '%@'", unescapedAttributeFilterValue); DDLogDebug(@"escapedAttributeFilterValue: '%@'", escapedAttributeFilterValue); DDLogDebug(@"attributeFilterValueRegexPattern: '%@'", attributeFilterValueRegexPattern); #endif } else if(attributeFilterType == '~') //raw regex comparison *without* format string interpolation //you will have to include sring-start and string-end markers yourself as well as all other regex stuff attributeFilterValueRegexPattern = attributeFilterValue; else @throw [NSException exceptionWithName:@"RuntimeException" reason:@"Internal attribute filter bug, this should never happen!" userInfo:@{ @"self": self, @"queryEntry": entry, @"attributeFilters": attributeFilters }]; NSError* error; [retval[@"attributeFilters"] addObject:@{ @"name": attributeFilterName, //this regex will be cached in parsed form in the local cache of this method @"value": [NSRegularExpression regularExpressionWithPattern:attributeFilterValueRegexPattern options:NSRegularExpressionCaseInsensitive error:&error] }]; if(error) @throw [NSException exceptionWithName:@"RuntimeException" reason:@"Attribute filter regex can not be compiled!" userInfo:@{ @"self": self, @"queryEntry": entry, @"filterType": @(attributeFilterType), @"filterName": attributeFilterName, @"filterValue": attributeFilterValue, @"error": error }]; } #ifdef DEBUG_XMLQueryLanguage DDLogDebug(@"Done extracting, attributeFilters are now: %@", retval[@"attributeFilters"]); #endif } if(extractionCommandRange.location != NSNotFound) { NSString* extractionCommand = [entry substringWithRange:extractionCommandRange]; retval[@"extractionCommand"] = [extractionCommand substringToIndex:1]; unichar command = [extractionCommand characterAtIndex:0]; if(command == '@') retval[@"attribute"] = [extractionCommand substringFromIndex:1]; if(command == '\\') retval[@"dataFormQuery"] = [extractionCommand substringWithRange:NSMakeRange(1, extractionCommandRange.length-2)]; } if(conversionCommandRange.location != NSNotFound) retval[@"conversionCommand"] = [entry substringWithRange:conversionCommandRange]; [self.queryEntryCache setObject:[retval copy] forKey:cacheKey]; return retval; } +(NSString*) escapeForXMPP:(NSString*) targetString { NSMutableString* mutable = [targetString mutableCopy]; [mutable replaceOccurrencesOfString:@"&" withString:@"&" options:NSLiteralSearch range:NSMakeRange(0, mutable.length)]; [mutable replaceOccurrencesOfString:@"<" withString:@"<" options:NSLiteralSearch range:NSMakeRange(0, mutable.length)]; [mutable replaceOccurrencesOfString:@">" withString:@">" options:NSLiteralSearch range:NSMakeRange(0, mutable.length)]; [mutable replaceOccurrencesOfString:@"'" withString:@"'" options:NSLiteralSearch range:NSMakeRange(0, mutable.length)]; [mutable replaceOccurrencesOfString:@"\"" withString:@""" options:NSLiteralSearch range:NSMakeRange(0, mutable.length)]; return [mutable copy]; } -(NSString*) XMLString { if(!_element) return @""; // sanity check //special handling of xml start tag if([_element isEqualToString:@"__xml"]) return [NSString stringWithFormat:@""]; NSMutableString* outputString = [NSMutableString new]; [outputString appendString:[NSString stringWithFormat:@"<%@", _element]]; //set attributes MLXMLNode* parent = self.parent; for(NSString* key in [_attributes allKeys]) { //handle xmlns inheritance (don't add namespace to childs if it should be the same like the parent's one) if([key isEqualToString:@"xmlns"] && parent && [[NSString stringWithFormat:@"%@", _attributes[@"xmlns"]] isEqualToString:[NSString stringWithFormat:@"%@", parent.attributes[@"xmlns"]]]) continue; [outputString appendString:[NSString stringWithFormat:@" %@='%@'", key, [MLXMLNode escapeForXMPP:[NSString stringWithFormat:@"%@", _attributes[key]]]]]; } if([_children count] || (_data && ![_data isEqualToString:@""])) { [outputString appendString:[NSString stringWithFormat:@">"]]; //set children here for(MLXMLNode* child in _children) [outputString appendString:[child XMLString]]; if(_data) [outputString appendString:[MLXMLNode escapeForXMPP:_data]]; //dont close stream element if(![_element isEqualToString:@"stream:stream"] && ![_element isEqualToString:@"/stream:stream"]) [outputString appendString:[NSString stringWithFormat:@"", _element]]; } else { //dont close stream element if(![_element isEqualToString:@"stream:stream"] && ![_element isEqualToString:@"/stream:stream"]) [outputString appendString:[NSString stringWithFormat:@"/>"]]; else [outputString appendString:[NSString stringWithFormat:@">"]]; } return (NSString*)outputString; } -(NSString*) description { return [self XMLString]; } @end