From 5b074a517693d383eefc11ea87c08f48632bab07 Mon Sep 17 00:00:00 2001 From: Giuseppe Nucifora Date: Wed, 24 Feb 2016 16:56:39 +0100 Subject: [PATCH] - Update --- .../Anonymous/XMPPAnonymousAuthentication.h | 45 + .../Anonymous/XMPPAnonymousAuthentication.m | 131 + .../XMPPDeprecatedDigestAuthentication.h | 22 + .../XMPPDeprecatedDigestAuthentication.m | 162 + .../XMPPDeprecatedPlainAuthentication.h | 22 + .../XMPPDeprecatedPlainAuthentication.m | 156 + .../Digest-MD5/XMPPDigestMD5Authentication.h | 22 + .../Digest-MD5/XMPPDigestMD5Authentication.m | 336 + .../Plain/XMPPPlainAuthentication.h | 22 + .../Plain/XMPPPlainAuthentication.m | 114 + .../SCRAM-SHA-1/XMPPSCRAMSHA1Authentication.h | 21 + .../SCRAM-SHA-1/XMPPSCRAMSHA1Authentication.m | 342 + .../XMPPXFacebookPlatformAuthentication.h | 56 + .../XMPPXFacebookPlatformAuthentication.m | 327 + .../X-OAuth2-Google/XMPPXOAuth2Google.h | 28 + .../X-OAuth2-Google/XMPPXOAuth2Google.m | 180 + Authentication/XMPPCustomBinding.h | 93 + Authentication/XMPPSASLAuthentication.h | 102 + Categories/NSData+XMPP.h | 34 + Categories/NSData+XMPP.m | 240 + Categories/NSNumber+XMPP.h | 46 + Categories/NSNumber+XMPP.m | 265 + Categories/NSXMLElement+XMPP.h | 157 + Categories/NSXMLElement+XMPP.m | 663 ++ Core/XMPP.h | 31 + Core/XMPPConstants.h | 16 + Core/XMPPConstants.m | 10 + Core/XMPPElement.h | 44 + Core/XMPPElement.m | 192 + Core/XMPPIQ.h | 83 + Core/XMPPIQ.m | 222 + Core/XMPPInternal.h | 118 + Core/XMPPJID.h | 74 + Core/XMPPJID.m | 603 ++ Core/XMPPLogging.h | 191 + Core/XMPPMessage.h | 55 + Core/XMPPMessage.m | 267 + Core/XMPPModule.h | 43 + Core/XMPPModule.m | 224 + Core/XMPPParser.h | 40 + Core/XMPPParser.m | 850 ++ Core/XMPPPresence.h | 38 + Core/XMPPPresence.m | 145 + Core/XMPPStream.h | 1111 +++ Core/XMPPStream.m | 5105 +++++++++++ .../PNXMPPFramework.xcodeproj/project.pbxproj | 4 +- Example/Podfile.lock | 153 +- .../CocoaLumberjack/Classes/CocoaLumberjack.h | 81 - .../Classes/CocoaLumberjack.swift | 91 - .../CocoaLumberjack/Classes/DDASLLogCapture.h | 48 - .../CocoaLumberjack/Classes/DDASLLogCapture.m | 230 - .../CocoaLumberjack/Classes/DDASLLogger.h | 58 - .../CocoaLumberjack/Classes/DDASLLogger.m | 121 - .../Classes/DDAbstractDatabaseLogger.m | 660 -- .../CocoaLumberjack/Classes/DDAssertMacros.h | 26 - .../CocoaLumberjack/Classes/DDLegacyMacros.h | 75 - .../Pods/CocoaLumberjack/Classes/DDLog+LOGV.h | 83 - Example/Pods/CocoaLumberjack/Classes/DDLog.h | 743 -- .../CocoaLumberjack/Classes/DDLogMacros.h | 82 - .../CocoaLumberjack/Classes/DDTTYLogger.m | 1481 ---- .../Extensions/DDContextFilterLogFormatter.h | 117 - .../Extensions/DDContextFilterLogFormatter.m | 191 - .../Extensions/DDDispatchQueueLogFormatter.h | 178 - .../Extensions/DDDispatchQueueLogFormatter.m | 277 - .../Classes/Extensions/DDMultiFormatter.h | 56 - .../Lumberjack/CocoaLumberjack.modulemap | 36 - Example/Pods/CocoaLumberjack/LICENSE.txt | 2 +- .../Lumberjack/DDASLLogCapture.h | 23 + .../Lumberjack/DDASLLogCapture.m | 188 + .../CocoaLumberjack/Lumberjack/DDASLLogger.h | 37 + .../CocoaLumberjack/Lumberjack/DDASLLogger.m | 95 + .../DDAbstractDatabaseLogger.h | 105 +- .../Lumberjack/DDAbstractDatabaseLogger.m | 727 ++ .../CocoaLumberjack/Lumberjack/DDAssert.h | 16 + .../{Classes => Lumberjack}/DDFileLogger.h | 417 +- .../{Classes => Lumberjack}/DDFileLogger.m | 1345 +-- .../CocoaLumberjack/Lumberjack/DDLog+LOGV.h | 99 + .../Pods/CocoaLumberjack/Lumberjack/DDLog.h | 692 ++ .../{Classes => Lumberjack}/DDLog.m | 1250 +-- .../CocoaLumberjack/Lumberjack/DDTTYLogger.h | 188 + .../CocoaLumberjack/Lumberjack/DDTTYLogger.m | 1510 ++++ .../Extensions/DDContextFilterLogFormatter.h | 63 + .../Extensions/DDContextFilterLogFormatter.m | 191 + .../Extensions/DDDispatchQueueLogFormatter.h | 128 + .../Extensions/DDDispatchQueueLogFormatter.m | 260 + .../Lumberjack/Extensions/DDMultiFormatter.h | 30 + .../Extensions/DDMultiFormatter.m | 66 +- .../Lumberjack/Extensions/README.txt | 7 + Example/Pods/CocoaLumberjack/README.md | 162 +- .../PNXMPPFramework.podspec.json | 511 +- Example/Pods/Manifest.lock | 153 +- Example/Pods/Pods.xcodeproj/project.pbxproj | 3899 +++++++-- .../xcschemes/PNXMPPFramework.xcscheme | 2 +- .../CocoaLumberjack-umbrella.h | 28 + .../CocoaLumberjack/CocoaLumberjack.modulemap | 38 +- .../CocoaLumberjack/Info.plist | 2 +- .../PNXMPPFramework-prefix.pch | 1 + .../PNXMPPFramework-umbrella.h | 140 + .../PNXMPPFramework/PNXMPPFramework.xcconfig | 6 +- ...ramework_Example-acknowledgements.markdown | 2 +- ...PPFramework_Example-acknowledgements.plist | 2 +- ...ods-PNXMPPFramework_Example.debug.xcconfig | 6 +- ...s-PNXMPPFramework_Example.release.xcconfig | 6 +- ...PFramework_Tests-acknowledgements.markdown | 2 +- ...XMPPFramework_Tests-acknowledgements.plist | 2 +- .../Pods-PNXMPPFramework_Tests.debug.xcconfig | 6 +- ...ods-PNXMPPFramework_Tests.release.xcconfig | 6 +- .../BandwidthMonitor/XMPPBandwidthMonitor.h | 19 + .../BandwidthMonitor/XMPPBandwidthMonitor.m | 191 + .../CoreDataStorage/XMPPCoreDataStorage.h | 167 + .../CoreDataStorage/XMPPCoreDataStorage.m | 1019 +++ .../XMPPCoreDataStorageProtected.h | 334 + Extensions/FileTransfer/XMPPFileTransfer.h | 80 + Extensions/FileTransfer/XMPPFileTransfer.m | 11 + .../FileTransfer/XMPPIncomingFileTransfer.h | 80 + .../FileTransfer/XMPPIncomingFileTransfer.m | 960 ++ .../FileTransfer/XMPPOutgoingFileTransfer.h | 140 + .../FileTransfer/XMPPOutgoingFileTransfer.m | 2188 +++++ .../XMPPGoogleSharedStatus.h | 51 + .../XMPPGoogleSharedStatus.m | 369 + Extensions/ProcessOne/XMPPProcessOne.h | 117 + Extensions/ProcessOne/XMPPProcessOne.m | 418 + Extensions/Reconnect/XMPPReconnect.h | 183 + Extensions/Reconnect/XMPPReconnect.m | 651 ++ .../XMPPGroupCoreDataStorageObject.h | 28 + .../XMPPGroupCoreDataStorageObject.m | 115 + .../XMPPResourceCoreDataStorageObject.h | 42 + .../XMPPResourceCoreDataStorageObject.m | 217 + .../XMPPRoster.xcdatamodel/elements | Bin 0 -> 109097 bytes .../XMPPRoster.xcdatamodel/layout | Bin 0 -> 12354 bytes .../XMPPRosterCoreDataStorage.h | 75 + .../XMPPRosterCoreDataStorage.m | 575 ++ .../XMPPUserCoreDataStorageObject.h | 79 + .../XMPPUserCoreDataStorageObject.m | 493 ++ .../XMPPResourceMemoryStorageObject.h | 28 + .../XMPPResourceMemoryStorageObject.m | 206 + .../MemoryStorage/XMPPRosterMemoryStorage.h | 128 + .../MemoryStorage/XMPPRosterMemoryStorage.m | 860 ++ .../XMPPRosterMemoryStoragePrivate.h | 50 + .../XMPPUserMemoryStorageObject.h | 82 + .../XMPPUserMemoryStorageObject.m | 512 ++ Extensions/Roster/XMPPResource.h | 15 + Extensions/Roster/XMPPRoster.h | 406 + Extensions/Roster/XMPPRoster.m | 1006 +++ Extensions/Roster/XMPPRosterPrivate.h | 16 + Extensions/Roster/XMPPUser.h | 21 + .../XMPPSystemInputActivityMonitor.h | 56 + .../XMPPSystemInputActivityMonitor.m | 202 + Extensions/XEP-0009/XMPPIQ+JabberRPC.h | 77 + Extensions/XEP-0009/XMPPIQ+JabberRPC.m | 219 + Extensions/XEP-0009/XMPPIQ+JabberRPCResonse.h | 66 + Extensions/XEP-0009/XMPPIQ+JabberRPCResonse.m | 290 + Extensions/XEP-0009/XMPPJabberRPCModule.h | 50 + Extensions/XEP-0009/XMPPJabberRPCModule.m | 341 + Extensions/XEP-0012/XMPPIQ+LastActivity.h | 94 + Extensions/XEP-0012/XMPPIQ+LastActivity.m | 97 + Extensions/XEP-0012/XMPPLastActivity.h | 119 + Extensions/XEP-0012/XMPPLastActivity.m | 222 + Extensions/XEP-0016/XMPPPrivacy.h | 231 + Extensions/XEP-0016/XMPPPrivacy.m | 1001 +++ .../XMPPRoom.xcdatamodeld/.xccurrentversion | 8 + .../XMPPRoom.xcdatamodel/contents | 37 + .../CoreDataStorage/XMPPRoomCoreDataStorage.h | 125 + .../CoreDataStorage/XMPPRoomCoreDataStorage.m | 999 +++ .../XMPPRoomMessageCoreDataStorageObject.h | 47 + .../XMPPRoomMessageCoreDataStorageObject.m | 205 + .../XMPPRoomOccupantCoreDataStorageObject.h | 39 + .../XMPPRoomOccupantCoreDataStorageObject.m | 234 + .../.xccurrentversion | 8 + .../XMPPRoomHybrid.xcdatamodel/contents | 21 + .../HybridStorage/XMPPRoomHybridStorage.h | 197 + .../HybridStorage/XMPPRoomHybridStorage.m | 990 +++ .../XMPPRoomHybridStorageProtected.h | 91 + ...PPRoomMessageHybridCoreDataStorageObject.h | 47 + ...PPRoomMessageHybridCoreDataStorageObject.m | 206 + ...MPPRoomOccupantHybridMemoryStorageObject.h | 41 + ...MPPRoomOccupantHybridMemoryStorageObject.m | 237 + .../MemoryStorage/XMPPRoomMemoryStorage.h | 130 + .../MemoryStorage/XMPPRoomMemoryStorage.m | 707 ++ .../XMPPRoomMessageMemoryStorageObject.h | 34 + .../XMPPRoomMessageMemoryStorageObject.m | 175 + .../XMPPRoomOccupantMemoryStorageObject.h | 33 + .../XMPPRoomOccupantMemoryStorageObject.m | 196 + Extensions/XEP-0045/XMPPMUC.h | 131 + Extensions/XEP-0045/XMPPMUC.m | 423 + Extensions/XEP-0045/XMPPMessage+XEP0045.h | 11 + Extensions/XEP-0045/XMPPMessage+XEP0045.m | 36 + Extensions/XEP-0045/XMPPRoom.h | 314 + Extensions/XEP-0045/XMPPRoom.m | 1174 +++ Extensions/XEP-0045/XMPPRoomMessage.h | 58 + Extensions/XEP-0045/XMPPRoomOccupant.h | 57 + Extensions/XEP-0045/XMPPRoomPrivate.h | 19 + .../XMPPvCard.xcdatamodeld/.xccurrentversion | 8 + .../XMPPvCard.xcdatamodel/elements | Bin 0 -> 36011 bytes .../XMPPvCard.xcdatamodel/layout | Bin 0 -> 10268 bytes .../XMPPvCardAvatarCoreDataStorageObject.h | 20 + .../XMPPvCardAvatarCoreDataStorageObject.m | 17 + .../XMPPvCardCoreDataStorage.h | 47 + .../XMPPvCardCoreDataStorage.m | 235 + .../XMPPvCardCoreDataStorageObject.h | 73 + .../XMPPvCardCoreDataStorageObject.m | 180 + .../XMPPvCardTempCoreDataStorageObject.h | 23 + .../XMPPvCardTempCoreDataStorageObject.m | 17 + Extensions/XEP-0054/XMPPvCardTemp.h | 120 + Extensions/XEP-0054/XMPPvCardTemp.m | 932 ++ Extensions/XEP-0054/XMPPvCardTempAdr.h | 28 + Extensions/XEP-0054/XMPPvCardTempAdr.m | 136 + Extensions/XEP-0054/XMPPvCardTempAdrTypes.h | 28 + Extensions/XEP-0054/XMPPvCardTempAdrTypes.m | 103 + Extensions/XEP-0054/XMPPvCardTempBase.h | 69 + Extensions/XEP-0054/XMPPvCardTempBase.m | 102 + Extensions/XEP-0054/XMPPvCardTempEmail.h | 29 + Extensions/XEP-0054/XMPPvCardTempEmail.m | 125 + Extensions/XEP-0054/XMPPvCardTempLabel.h | 25 + Extensions/XEP-0054/XMPPvCardTempLabel.m | 93 + Extensions/XEP-0054/XMPPvCardTempModule.h | 117 + Extensions/XEP-0054/XMPPvCardTempModule.m | 286 + Extensions/XEP-0054/XMPPvCardTempTel.h | 37 + Extensions/XEP-0054/XMPPvCardTempTel.m | 204 + Extensions/XEP-0059/NSXMLElement+XEP_0059.h | 16 + Extensions/XEP-0059/NSXMLElement+XEP_0059.m | 47 + Extensions/XEP-0059/XMPPResultSet.h | 96 + Extensions/XEP-0059/XMPPResultSet.m | 230 + Extensions/XEP-0060/XMPPIQ+XEP_0060.h | 35 + Extensions/XEP-0060/XMPPIQ+XEP_0060.m | 25 + Extensions/XEP-0060/XMPPPubSub.h | 374 + Extensions/XEP-0060/XMPPPubSub.m | 1053 +++ Extensions/XEP-0065/TURNSocket.h | 80 + Extensions/XEP-0065/TURNSocket.m | 1535 ++++ Extensions/XEP-0066/XMPPIQ+XEP_0066.h | 45 + Extensions/XEP-0066/XMPPIQ+XEP_0066.m | 229 + Extensions/XEP-0066/XMPPMessage+XEP_0066.h | 14 + Extensions/XEP-0066/XMPPMessage+XEP_0066.m | 97 + Extensions/XEP-0077/XMPPRegistration.h | 94 + Extensions/XEP-0077/XMPPRegistration.m | 237 + .../XEP-0082/NSDate+XMPPDateTimeProfiles.h | 26 + .../XEP-0082/NSDate+XMPPDateTimeProfiles.m | 80 + Extensions/XEP-0082/XMPPDateTimeProfiles.h | 17 + Extensions/XEP-0082/XMPPDateTimeProfiles.m | 295 + Extensions/XEP-0085/XMPPMessage+XEP_0085.h | 23 + Extensions/XEP-0085/XMPPMessage+XEP_0085.m | 69 + Extensions/XEP-0092/XMPPSoftwareVersion.h | 24 + Extensions/XEP-0092/XMPPSoftwareVersion.m | 122 + Extensions/XEP-0100/XMPPTransports.h | 23 + Extensions/XEP-0100/XMPPTransports.m | 148 + Extensions/XEP-0106/NSString+XEP_0106.h | 9 + Extensions/XEP-0106/NSString+XEP_0106.m | 57 + .../XMPPCapabilities.xcdatamodel/elements | Bin 0 -> 45046 bytes .../XMPPCapabilities.xcdatamodel/layout | Bin 0 -> 7630 bytes .../XMPPCapabilitiesCoreDataStorage.h | 54 + .../XMPPCapabilitiesCoreDataStorage.m | 587 ++ .../XMPPCapsCoreDataStorageObject.h | 30 + .../XMPPCapsCoreDataStorageObject.m | 30 + .../XMPPCapsResourceCoreDataStorageObject.h | 23 + .../XMPPCapsResourceCoreDataStorageObject.m | 36 + Extensions/XEP-0115/XMPPCapabilities.h | 380 + Extensions/XEP-0115/XMPPCapabilities.m | 1781 ++++ .../.xccurrentversion | 8 + .../XMPPMessageArchiving.xcdatamodel/contents | 27 + .../XMPPMessageArchivingCoreDataStorage.h | 68 + .../XMPPMessageArchivingCoreDataStorage.m | 488 ++ ...PMessageArchiving_Contact_CoreDataObject.h | 36 + ...PMessageArchiving_Contact_CoreDataObject.m | 94 + ...PMessageArchiving_Message_CoreDataObject.h | 52 + ...PMessageArchiving_Message_CoreDataObject.m | 171 + Extensions/XEP-0136/XMPPMessageArchiving.h | 117 + Extensions/XEP-0136/XMPPMessageArchiving.m | 431 + Extensions/XEP-0147/XMPPURI.h | 69 + Extensions/XEP-0147/XMPPURI.m | 108 + Extensions/XEP-0153/XMPPvCardAvatarModule.h | 93 + Extensions/XEP-0153/XMPPvCardAvatarModule.m | 305 + Extensions/XEP-0172/XMPPMessage+XEP_0172.h | 8 + Extensions/XEP-0172/XMPPMessage+XEP_0172.m | 13 + Extensions/XEP-0172/XMPPPresence+XEP_0172.h | 8 + Extensions/XEP-0172/XMPPPresence+XEP_0172.m | 12 + Extensions/XEP-0184/XMPPMessage+XEP_0184.h | 14 + Extensions/XEP-0184/XMPPMessage+XEP_0184.m | 64 + .../XEP-0184/XMPPMessageDeliveryReceipts.h | 32 + .../XEP-0184/XMPPMessageDeliveryReceipts.m | 210 + Extensions/XEP-0191/XMPPBlocking.h | 132 + Extensions/XEP-0191/XMPPBlocking.m | 644 ++ .../XMPPStreamManagementMemoryStorage.h | 14 + .../XMPPStreamManagementMemoryStorage.m | 211 + .../Private/XMPPStreamManagementStanzas.h | 35 + .../Private/XMPPStreamManagementStanzas.m | 85 + Extensions/XEP-0198/XMPPStreamManagement.h | 617 ++ Extensions/XEP-0198/XMPPStreamManagement.m | 1929 ++++ Extensions/XEP-0199/XMPPAutoPing.h | 102 + Extensions/XEP-0199/XMPPAutoPing.m | 416 + Extensions/XEP-0199/XMPPPing.h | 51 + Extensions/XEP-0199/XMPPPing.m | 314 + Extensions/XEP-0202/XMPPAutoTime.h | 124 + Extensions/XEP-0202/XMPPAutoTime.m | 488 ++ Extensions/XEP-0202/XMPPTime.h | 90 + Extensions/XEP-0202/XMPPTime.m | 505 ++ Extensions/XEP-0203/NSXMLElement+XEP_0203.h | 12 + Extensions/XEP-0203/NSXMLElement+XEP_0203.m | 84 + Extensions/XEP-0223/XEP_0223.h | 21 + Extensions/XEP-0223/XEP_0223.m | 20 + Extensions/XEP-0224/XMPPAttentionModule.h | 24 + Extensions/XEP-0224/XMPPAttentionModule.m | 127 + Extensions/XEP-0224/XMPPMessage+XEP_0224.h | 8 + Extensions/XEP-0224/XMPPMessage+XEP_0224.m | 24 + Extensions/XEP-0280/XMPPMessage+XEP_0280.h | 19 + Extensions/XEP-0280/XMPPMessage+XEP_0280.m | 118 + Extensions/XEP-0280/XMPPMessageCarbons.h | 65 + Extensions/XEP-0280/XMPPMessageCarbons.m | 293 + Extensions/XEP-0297/NSXMLElement+XEP_0297.h | 39 + Extensions/XEP-0297/NSXMLElement+XEP_0297.m | 138 + Extensions/XEP-0308/XMPPMessage+XEP_0308.h | 14 + Extensions/XEP-0308/XMPPMessage+XEP_0308.m | 88 + Extensions/XEP-0333/XMPPMessage+XEP_0333.h | 28 + Extensions/XEP-0333/XMPPMessage+XEP_0333.m | 142 + Extensions/XEP-0335/NSXMLElement+XEP_0335.h | 21 + Extensions/XEP-0335/NSXMLElement+XEP_0335.m | 87 + Extensions/XEP-0352/NSXMLElement+XEP_0352.h | 9 + Extensions/XEP-0352/NSXMLElement+XEP_0352.m | 18 + PNXMPPFramework.podspec | 277 +- Utilities/DDList.h | 61 + Utilities/DDList.m | 297 + Utilities/GCDMulticastDelegate.h | 56 + Utilities/GCDMulticastDelegate.m | 654 ++ Utilities/RFImageToDataTransformer.h | 19 + Utilities/RFImageToDataTransformer.m | 48 + Utilities/XMPPIDTracker.h | 210 + Utilities/XMPPIDTracker.m | 347 + Utilities/XMPPSRVResolver.h | 86 + Utilities/XMPPSRVResolver.m | 687 ++ Utilities/XMPPStringPrep.h | 41 + Utilities/XMPPStringPrep.m | 67 + Utilities/XMPPTimer.h | 41 + Utilities/XMPPTimer.m | 86 + Vendor/CocoaAsyncSocket/GCDAsyncSocket.h | 1179 +++ Vendor/CocoaAsyncSocket/GCDAsyncSocket.m | 7719 +++++++++++++++++ Vendor/CocoaLumberjack/DDASLLogger.h | 41 + Vendor/CocoaLumberjack/DDASLLogger.m | 100 + .../DDAbstractDatabaseLogger.h | 102 + .../DDAbstractDatabaseLogger.m | 727 ++ Vendor/CocoaLumberjack/DDFileLogger.h | 369 + Vendor/CocoaLumberjack/DDFileLogger.m | 1539 ++++ Vendor/CocoaLumberjack/DDLog+LOGV.h | 99 + Vendor/CocoaLumberjack/DDLog.h | 634 ++ Vendor/CocoaLumberjack/DDLog.m | 1208 +++ .../CocoaLumberjack}/DDTTYLogger.h | 211 +- Vendor/CocoaLumberjack/DDTTYLogger.m | 1520 ++++ .../Extensions/DDContextFilterLogFormatter.h | 63 + .../Extensions/DDContextFilterLogFormatter.m | 191 + .../Extensions/DDDispatchQueueLogFormatter.h | 128 + .../Extensions/DDDispatchQueueLogFormatter.m | 253 + .../Extensions/DDMultiFormatter.h | 30 + .../Extensions/DDMultiFormatter.m | 127 + Vendor/CocoaLumberjack/Extensions/README.txt | 7 + Vendor/KissXML/Categories/NSString+DDXML.h | 18 + Vendor/KissXML/Categories/NSString+DDXML.m | 31 + Vendor/KissXML/DDXML.h | 196 + Vendor/KissXML/DDXMLDocument.h | 84 + Vendor/KissXML/DDXMLDocument.m | 140 + Vendor/KissXML/DDXMLElement.h | 66 + Vendor/KissXML/DDXMLElement.m | 801 ++ Vendor/KissXML/DDXMLNode.h | 161 + Vendor/KissXML/DDXMLNode.m | 2905 +++++++ Vendor/KissXML/Private/DDXMLPrivate.h | 229 + Vendor/facebook-ios-sdk/.gitignore | 22 + Vendor/facebook-ios-sdk/.gitmodules | 9 + Vendor/facebook-ios-sdk/CONTRIBUTING.mdown | 15 + Vendor/facebook-ios-sdk/LICENSE | 201 + Vendor/facebook-ios-sdk/README | 32 + Vendor/facebook-ios-sdk/README.mdown | 32 + .../project.pbxproj | 343 + .../FriendPickerSample/Default-568h@2x.png | Bin 0 -> 18594 bytes .../FriendPickerSample/Default.png | Bin 0 -> 6540 bytes .../FriendPickerSample/Default@2x.png | Bin 0 -> 16107 bytes .../FriendPickerSample/FPAppDelegate.h | 35 + .../FriendPickerSample/FPAppDelegate.m | 90 + .../FriendPickerSample/FPViewController.h | 23 + .../FriendPickerSample/FPViewController.m | 122 + .../FriendPickerSample-Info.plist | 85 + .../FriendPickerSample-Prefix.pch | 14 + .../FriendPickerSample/Icon-72.png | Bin 0 -> 11777 bytes .../FriendPickerSample/Icon-72@2x.png | Bin 0 -> 25250 bytes .../FriendPickerSample/Icon.png | Bin 0 -> 10152 bytes .../FriendPickerSample/Icon@2x.png | Bin 0 -> 21380 bytes .../en.lproj/FPViewController_iPad.xib | 289 + .../en.lproj/FPViewController_iPhone.xib | 253 + .../en.lproj/InfoPlist.strings | 2 + .../FriendPickerSample/main.m | 26 + .../samples/FriendPickerSample/ReadMe.txt | 7 + .../GraphApiSample.xcodeproj/project.pbxproj | 345 + .../GraphApiSample/Default-568h@2x.png | Bin 0 -> 18594 bytes .../GraphApiSample/GraphApiSample/Default.png | Bin 0 -> 6540 bytes .../GraphApiSample/Default@2x.png | Bin 0 -> 16107 bytes .../GraphApiSample/GraphApiAppDelegate.h | 33 + .../GraphApiSample/GraphApiAppDelegate.m | 66 + .../GraphApiSample/GraphApiSample-Info.plist | 78 + .../GraphApiSample/GraphApiSample-Prefix.pch | 16 + .../GraphApiSample/GraphApiViewController.h | 21 + .../GraphApiSample/GraphApiViewController.m | 209 + .../GraphApiSample/GraphApiSample/Icon-72.png | Bin 0 -> 12227 bytes .../GraphApiSample/Icon-72@2x.png | Bin 0 -> 26408 bytes .../GraphApiSample/GraphApiSample/Icon.png | Bin 0 -> 9760 bytes .../GraphApiSample/GraphApiSample/Icon@2x.png | Bin 0 -> 22116 bytes .../en.lproj/GraphApiViewController_iPad.xib | 263 + .../GraphApiViewController_iPhone.xib | 263 + .../GraphApiSample/en.lproj/InfoPlist.strings | 2 + .../GraphApiSample/GraphApiSample/main.m | 26 + .../samples/GraphApiSample/ReadMe.txt | 7 + .../project.pbxproj | 347 + .../HelloFacebookSample/Default-568h@2x.png | Bin 0 -> 18594 bytes .../HelloFacebookSample/Default.png | Bin 0 -> 6540 bytes .../HelloFacebookSample/Default@2x.png | Bin 0 -> 16107 bytes .../HelloFacebookSample/HFAppDelegate.h | 39 + .../HelloFacebookSample/HFAppDelegate.m | 79 + .../HelloFacebookSample/HFViewController.h | 22 + .../HelloFacebookSample/HFViewController.m | 390 + .../HelloFacebookSample-Info.plist | 78 + .../HelloFacebookSample-Prefix.pch | 14 + .../HelloFacebookSample/Icon-72.png | Bin 0 -> 11367 bytes .../HelloFacebookSample/Icon-72@2x.png | Bin 0 -> 24421 bytes .../HelloFacebookSample/Icon.png | Bin 0 -> 9838 bytes .../HelloFacebookSample/Icon@2x.png | Bin 0 -> 20971 bytes .../en.lproj/HFViewController_iPad.xib | 483 ++ .../en.lproj/HFViewController_iPhone.xib | 394 + .../en.lproj/InfoPlist.strings | 2 + .../HelloFacebookSample/main.m | 26 + .../samples/HelloFacebookSample/ReadMe.txt | 12 + .../project.pbxproj | 351 + .../PlacePickerSample/Default-568h@2x.png | Bin 0 -> 18594 bytes .../PlacePickerSample/Default.png | Bin 0 -> 6540 bytes .../PlacePickerSample/Default@2x.png | Bin 0 -> 16107 bytes .../PlacePickerSample/Icon-72.png | Bin 0 -> 12110 bytes .../PlacePickerSample/Icon-72@2x.png | Bin 0 -> 26248 bytes .../PlacePickerSample/Icon.png | Bin 0 -> 9697 bytes .../PlacePickerSample/Icon@2x.png | Bin 0 -> 21741 bytes .../PlacePickerSample/PPAppDelegate.h | 35 + .../PlacePickerSample/PPAppDelegate.m | 62 + .../PlacePickerSample/PPViewController.h | 23 + .../PlacePickerSample/PPViewController.m | 213 + .../PlacePickerSample-Info.plist | 78 + .../PlacePickerSample-Prefix.pch | 14 + .../en.lproj/InfoPlist.strings | 2 + .../en.lproj/PPViewController_iPad.xib | 303 + .../en.lproj/PPViewController_iPhone.xib | 245 + .../PlacePickerSample/main.m | 26 + .../samples/PlacePickerSample/ReadMe.txt | 9 + .../project.pbxproj | 351 + .../ProfilePictureSample/Default-568h@2x.png | Bin 0 -> 18594 bytes .../ProfilePictureSample/Default.png | Bin 0 -> 6540 bytes .../ProfilePictureSample/Default@2x.png | Bin 0 -> 16107 bytes .../ProfilePictureSample/Icon-72.png | Bin 0 -> 11742 bytes .../ProfilePictureSample/Icon-72@2x.png | Bin 0 -> 25521 bytes .../ProfilePictureSample/Icon.png | Bin 0 -> 10050 bytes .../ProfilePictureSample/Icon@2x.png | Bin 0 -> 21594 bytes .../ProfilePictureSample/PPAppDelegate.h | 31 + .../ProfilePictureSample/PPAppDelegate.m | 46 + .../ProfilePictureSample/PPViewController.h | 41 + .../ProfilePictureSample/PPViewController.m | 152 + .../ProfilePictureSample-Info.plist | 76 + .../ProfilePictureSample-Prefix.pch | 14 + .../en.lproj/InfoPlist.strings | 2 + .../en.lproj/PPViewController_iPad.xib | 711 ++ .../en.lproj/PPViewController_iPhone.xib | 710 ++ .../ProfilePictureSample/main.m | 26 + .../samples/ProfilePictureSample/ReadMe.txt | 8 + .../RPSSample.xcodeproj/project.pbxproj | 449 + .../RPSSample/RPSSample/Default-568h@2x.png | Bin 0 -> 18594 bytes .../samples/RPSSample/RPSSample/Default.png | Bin 0 -> 6540 bytes .../RPSSample/RPSSample/Default@2x.png | Bin 0 -> 16107 bytes .../samples/RPSSample/RPSSample/Icon-72.png | Bin 0 -> 11802 bytes .../RPSSample/RPSSample/Icon-72@2x.png | Bin 0 -> 25600 bytes .../samples/RPSSample/RPSSample/Icon.png | Bin 0 -> 10236 bytes .../samples/RPSSample/RPSSample/Icon@2x.png | Bin 0 -> 21540 bytes .../RPSSample/IconFacebook-72@2x.png | Bin 0 -> 5752 bytes .../samples/RPSSample/RPSSample/OGProtocols.h | 73 + .../RPSSample/RPSSample/RPSAppDelegate.h | 34 + .../RPSSample/RPSSample/RPSAppDelegate.m | 71 + .../RPSSample/RPSSample/RPSCommonObjects.m | 20 + .../RPSSample/RPSFriendsViewController.h | 28 + .../RPSSample/RPSFriendsViewController.m | 235 + .../RPSSample/RPSGameViewController.h | 44 + .../RPSSample/RPSGameViewController.m | 517 ++ .../RPSSample/RPSSample/RPSSample-Info.plist | 79 + .../RPSSample/RPSSample/RPSSample-Prefix.pch | 14 + .../RPSSample/en.lproj/InfoPlist.strings | 2 + .../RPSFriendsViewController_iPad.xib | 351 + .../RPSFriendsViewController_iPhone.xib | 310 + .../en.lproj/RPSGameViewController_iPad.xib | 888 ++ .../en.lproj/RPSGameViewController_iPhone.xib | 782 ++ .../RPSSample/RPSSample/left-paper-128.png | Bin 0 -> 22313 bytes .../RPSSample/RPSSample/left-paper-88.png | Bin 0 -> 10601 bytes .../RPSSample/RPSSample/left-rock-128.png | Bin 0 -> 23221 bytes .../RPSSample/RPSSample/left-rock-88.png | Bin 0 -> 10955 bytes .../RPSSample/RPSSample/left-scissors-128.png | Bin 0 -> 21733 bytes .../RPSSample/RPSSample/left-scissors-88.png | Bin 0 -> 10238 bytes .../samples/RPSSample/RPSSample/main.m | 26 + .../RPSSample/RPSSample/right-paper-128.png | Bin 0 -> 25363 bytes .../RPSSample/RPSSample/right-paper-88.png | Bin 0 -> 12070 bytes .../RPSSample/RPSSample/right-rock-128.png | Bin 0 -> 24530 bytes .../RPSSample/RPSSample/right-rock-88.png | Bin 0 -> 11598 bytes .../RPSSample/right-scissors-128.png | Bin 0 -> 25691 bytes .../RPSSample/RPSSample/right-scissors-88.png | Bin 0 -> 12232 bytes .../samples/RPSSample/ReadMe.txt | 13 + .../samples/RPSSample/post_app_objects.sh | 117 + .../samples/Scrumptious/Icon-72.png | Bin 0 -> 9990 bytes .../samples/Scrumptious/Icon.png | Bin 0 -> 8981 bytes .../samples/Scrumptious/Icon@2x.png | Bin 0 -> 13792 bytes .../samples/Scrumptious/ReadMe.txt | 29 + .../Scrumptious.xcodeproj/project.pbxproj | 426 + .../scrumptious/Default-568h@2x.png | Bin 0 -> 3667 bytes .../Scrumptious/scrumptious/Default.png | Bin 0 -> 1331 bytes .../Scrumptious/scrumptious/Default@2x.png | Bin 0 -> 3835 bytes .../FacebookSDKImages/FBLoginViewButton.png | Bin 0 -> 710 bytes .../FBLoginViewButton@2x.png | Bin 0 -> 1479 bytes .../en.lproj/Localizable.strings | Bin 0 -> 4002 bytes .../he.lproj/Localizable.strings | Bin 0 -> 4000 bytes .../Scrumptious/scrumptious/SCAppDelegate.h | 36 + .../Scrumptious/scrumptious/SCAppDelegate.m | 135 + .../scrumptious/SCLoginViewController.h | 26 + .../scrumptious/SCLoginViewController.m | 155 + .../scrumptious/SCLoginViewController.xib | 419 + .../scrumptious/SCPhotoViewController.h | 32 + .../scrumptious/SCPhotoViewController.m | 79 + .../scrumptious/SCPhotoViewController.xib | 264 + .../Scrumptious/scrumptious/SCProtocols.h | 38 + .../scrumptious/SCViewController.h | 28 + .../scrumptious/SCViewController.m | 886 ++ .../scrumptious/en.lproj/InfoPlist.strings | 2 + .../scrumptious/en.lproj/SCViewController.xib | 367 + .../scrumptious/en.lproj/Scrumptious.strings | 4 + .../scrumptious/he.lproj/Scrumptious.strings | 4 + .../scrumptious/images/action-eating.png | Bin 0 -> 2763 bytes .../scrumptious/images/action-location.png | Bin 0 -> 3144 bytes .../scrumptious/images/action-people.png | Bin 0 -> 2592 bytes .../scrumptious/images/action-photo.png | Bin 0 -> 3100 bytes .../scrumptious/images/facebook.png | Bin 0 -> 3156 bytes .../samples/Scrumptious/scrumptious/main.m | 27 + .../scrumptious/scrumptious-Info.plist | 63 + .../scrumptious/scrumptious-Prefix.pch | 24 + .../samples/SessionLoginSample/ReadMe.txt | 8 + .../project.pbxproj | 353 + .../SessionLoginSample/Default-568h@2x.png | Bin 0 -> 18594 bytes .../SessionLoginSample/Default.png | Bin 0 -> 6540 bytes .../SessionLoginSample/Default@2x.png | Bin 0 -> 16107 bytes .../SessionLoginSample/Icon-72.png | Bin 0 -> 11578 bytes .../SessionLoginSample/Icon-72@2x.png | Bin 0 -> 24822 bytes .../SessionLoginSample/Icon.png | Bin 0 -> 10029 bytes .../SessionLoginSample/Icon@2x.png | Bin 0 -> 21305 bytes .../SessionLoginSample/SLAppDelegate.h | 43 + .../SessionLoginSample/SLAppDelegate.m | 98 + .../SessionLoginSample/SLViewController.h | 21 + .../SessionLoginSample/SLViewController.m | 130 + .../SessionLoginSample-Info.plist | 78 + .../SessionLoginSample-Prefix.pch | 14 + .../en.lproj/InfoPlist.strings | 2 + .../en.lproj/SLViewController_iPad.xib | 217 + .../en.lproj/SLViewController_iPhone.xib | 216 + .../SessionLoginSample/main.m | 26 + .../samples/SwitchUserSample/ReadMe.txt | 13 + .../project.pbxproj | 378 + .../SwitchUserSample/Default-568h@2x.png | Bin 0 -> 18594 bytes .../SwitchUserSample/Default.png | Bin 0 -> 6540 bytes .../SwitchUserSample/Default@2x.png | Bin 0 -> 16107 bytes .../SwitchUserSample/Icon-72.png | Bin 0 -> 12101 bytes .../SwitchUserSample/Icon-72@2x.png | Bin 0 -> 26057 bytes .../SwitchUserSample/Icon.png | Bin 0 -> 10285 bytes .../SwitchUserSample/Icon@2x.png | Bin 0 -> 21608 bytes .../SwitchUserSample/SUAppDelegate.h | 40 + .../SwitchUserSample/SUAppDelegate.m | 79 + .../SwitchUserSample/SUProfileTableViewCell.h | 25 + .../SwitchUserSample/SUProfileTableViewCell.m | 124 + .../SUSettingsViewController.h | 23 + .../SUSettingsViewController.m | 317 + .../SUSettingsViewController_iPad.xib | 198 + .../SUSettingsViewController_iPhone.xib | 202 + .../SwitchUserSample/SUUserManager.h | 44 + .../SwitchUserSample/SUUserManager.m | 176 + .../SwitchUserSample/SUUsingViewController.h | 21 + .../SwitchUserSample/SUUsingViewController.m | 145 + .../SUUsingViewController_iPad.xib | 270 + .../SUUsingViewController_iPhone.xib | 268 + .../SwitchUserSample-Info.plist | 78 + .../SwitchUserSample-Prefix.pch | 14 + .../en.lproj/InfoPlist.strings | 2 + .../SwitchUserSample/first.png | Bin 0 -> 253 bytes .../SwitchUserSample/first@2x.png | Bin 0 -> 402 bytes .../SwitchUserSample/SwitchUserSample/main.m | 26 + .../SwitchUserSample/second.png | Bin 0 -> 128 bytes .../SwitchUserSample/second@2x.png | Bin 0 -> 130 bytes Vendor/facebook-ios-sdk/scripts/build_all.sh | 43 + .../scripts/build_distribution.sh | 110 + .../scripts/build_documentation.sh | 87 + .../scripts/build_framework.sh | 185 + .../facebook-ios-sdk/scripts/build_samples.sh | 75 + Vendor/facebook-ios-sdk/scripts/common.sh | 144 + .../configure_simulator_for_unit_tests.sh | 44 + .../facebook-ios-sdk/scripts/image_to_code.py | 142 + .../scripts/label_version_number.sh | 49 + .../facebook-ios-sdk/scripts/rm_test_users.py | 66 + Vendor/facebook-ios-sdk/scripts/run_tests.sh | 70 + Vendor/facebook-ios-sdk/src/Base64/FBBase64.h | 25 + Vendor/facebook-ios-sdk/src/Base64/FBBase64.m | 203 + .../src/Cryptography/FBCrypto.h | 53 + .../src/Cryptography/FBCrypto.m | 273 + .../src/FBAccessTokenData+Internal.h | 32 + .../facebook-ios-sdk/src/FBAccessTokenData.h | 139 + .../facebook-ios-sdk/src/FBAccessTokenData.m | 223 + Vendor/facebook-ios-sdk/src/FBAppBridge.h | 39 + Vendor/facebook-ios-sdk/src/FBAppBridge.m | 645 ++ .../src/FBAppBridgeTypeToJSONConverter.h | 52 + .../src/FBAppBridgeTypeToJSONConverter.m | 177 + .../facebook-ios-sdk/src/FBAppCall+Internal.h | 49 + Vendor/facebook-ios-sdk/src/FBAppCall.h | 231 + Vendor/facebook-ios-sdk/src/FBAppCall.m | 455 + .../src/FBAppEvents+Internal.h | 123 + Vendor/facebook-ios-sdk/src/FBAppEvents.h | 472 + Vendor/facebook-ios-sdk/src/FBAppEvents.m | 1192 +++ .../src/FBAppLinkData+Internal.h | 27 + Vendor/facebook-ios-sdk/src/FBAppLinkData.h | 45 + Vendor/facebook-ios-sdk/src/FBAppLinkData.m | 109 + .../facebook-ios-sdk/src/FBCacheDescriptor.h | 42 + .../facebook-ios-sdk/src/FBCacheDescriptor.m | 26 + Vendor/facebook-ios-sdk/src/FBCacheIndex.h | 73 + Vendor/facebook-ios-sdk/src/FBCacheIndex.m | 693 ++ Vendor/facebook-ios-sdk/src/FBConnect.h | 21 + Vendor/facebook-ios-sdk/src/FBDataDiskCache.h | 44 + Vendor/facebook-ios-sdk/src/FBDataDiskCache.m | 230 + Vendor/facebook-ios-sdk/src/FBDialog.h | 165 + Vendor/facebook-ios-sdk/src/FBDialog.m | 715 ++ .../facebook-ios-sdk/src/FBDialogs+Internal.h | 51 + Vendor/facebook-ios-sdk/src/FBDialogs.h | 491 ++ Vendor/facebook-ios-sdk/src/FBDialogs.m | 383 + .../src/FBDialogsData+Internal.h | 28 + Vendor/facebook-ios-sdk/src/FBDialogsData.h | 35 + Vendor/facebook-ios-sdk/src/FBDialogsData.m | 72 + .../src/FBDialogsParams+Internal.h | 36 + Vendor/facebook-ios-sdk/src/FBDialogsParams.h | 28 + Vendor/facebook-ios-sdk/src/FBDialogsParams.m | 42 + .../src/FBDynamicFrameworkLoader.h | 128 + .../src/FBDynamicFrameworkLoader.m | 220 + Vendor/facebook-ios-sdk/src/FBError.h | 367 + Vendor/facebook-ios-sdk/src/FBError.m | 53 + .../src/FBErrorUtility+Internal.h | 50 + Vendor/facebook-ios-sdk/src/FBErrorUtility.h | 66 + Vendor/facebook-ios-sdk/src/FBErrorUtility.m | 315 + .../src/FBFetchedAppSettings.h | 31 + .../src/FBFetchedAppSettings.m | 48 + .../src/FBFrictionlessDialogSupportDelegate.h | 29 + .../src/FBFrictionlessRecipientCache.h | 86 + .../src/FBFrictionlessRecipientCache.m | 127 + .../src/FBFrictionlessRequestSettings.h | 85 + .../src/FBFrictionlessRequestSettings.m | 170 + .../src/FBFriendPickerCacheDescriptor.h | 91 + .../src/FBFriendPickerCacheDescriptor.m | 137 + .../FBFriendPickerViewController+Internal.h | 33 + .../src/FBFriendPickerViewController.h | 290 + .../src/FBFriendPickerViewController.m | 529 ++ Vendor/facebook-ios-sdk/src/FBGraphLocation.h | 77 + Vendor/facebook-ios-sdk/src/FBGraphObject.h | 269 + Vendor/facebook-ios-sdk/src/FBGraphObject.m | 503 ++ .../src/FBGraphObjectPagingLoader.h | 63 + .../src/FBGraphObjectPagingLoader.m | 317 + .../src/FBGraphObjectTableCell.h | 36 + .../src/FBGraphObjectTableCell.m | 230 + .../src/FBGraphObjectTableDataSource.h | 102 + .../src/FBGraphObjectTableDataSource.m | 585 ++ .../src/FBGraphObjectTableSelection.h | 38 + .../src/FBGraphObjectTableSelection.m | 212 + Vendor/facebook-ios-sdk/src/FBGraphPlace.h | 60 + Vendor/facebook-ios-sdk/src/FBGraphUser.h | 90 + .../src/FBImageResourceLoader.h | 32 + .../src/FBImageResourceLoader.m | 60 + Vendor/facebook-ios-sdk/src/FBInsights.h | 56 + Vendor/facebook-ios-sdk/src/FBInsights.m | 71 + Vendor/facebook-ios-sdk/src/FBLogger.h | 86 + Vendor/facebook-ios-sdk/src/FBLogger.m | 221 + Vendor/facebook-ios-sdk/src/FBLoginDialog.h | 48 + Vendor/facebook-ios-sdk/src/FBLoginDialog.m | 94 + .../src/FBLoginDialogParams.h | 44 + .../src/FBLoginDialogParams.m | 97 + Vendor/facebook-ios-sdk/src/FBLoginView.h | 179 + Vendor/facebook-ios-sdk/src/FBLoginView.m | 544 ++ Vendor/facebook-ios-sdk/src/FBNativeDialogs.h | 108 + Vendor/facebook-ios-sdk/src/FBNativeDialogs.m | 93 + .../facebook-ios-sdk/src/FBOpenGraphAction.h | 127 + .../src/FBOpenGraphActionShareDialogParams.h | 42 + .../src/FBOpenGraphActionShareDialogParams.m | 190 + .../facebook-ios-sdk/src/FBOpenGraphObject.h | 76 + .../src/FBPlacePickerCacheDescriptor.h | 36 + .../src/FBPlacePickerCacheDescriptor.m | 114 + .../FBPlacePickerViewController+Internal.h | 38 + .../src/FBPlacePickerViewController.h | 257 + .../src/FBPlacePickerViewController.m | 556 ++ .../src/FBProfilePictureView.h | 80 + .../src/FBProfilePictureView.m | 239 + .../facebook-ios-sdk/src/FBRequest+Internal.h | 32 + Vendor/facebook-ios-sdk/src/FBRequest.h | 668 ++ Vendor/facebook-ios-sdk/src/FBRequest.m | 551 ++ Vendor/facebook-ios-sdk/src/FBRequestBody.h | 41 + Vendor/facebook-ios-sdk/src/FBRequestBody.m | 114 + .../src/FBRequestConnection+Internal.h | 35 + .../src/FBRequestConnection.h | 622 ++ .../src/FBRequestConnection.m | 1588 ++++ .../src/FBRequestConnectionRetryManager.h | 74 + .../src/FBRequestConnectionRetryManager.m | 175 + .../src/FBRequestHandlerFactory.h | 18 + .../src/FBRequestHandlerFactory.m | 146 + .../facebook-ios-sdk/src/FBRequestMetadata.h | 45 + .../facebook-ios-sdk/src/FBRequestMetadata.m | 87 + Vendor/facebook-ios-sdk/src/FBSDKVersion.h | 3 + .../src/FBSession+FBSessionLoginStrategy.h | 45 + .../facebook-ios-sdk/src/FBSession+Internal.h | 64 + .../src/FBSession+Protected.h | 43 + Vendor/facebook-ios-sdk/src/FBSession.h | 772 ++ Vendor/facebook-ios-sdk/src/FBSession.m | 2040 +++++ .../src/FBSessionAppEventsState.h | 36 + .../src/FBSessionAppEventsState.m | 104 + .../src/FBSessionAppSwitchingLoginStategy.h | 19 + .../src/FBSessionAppSwitchingLoginStategy.m | 86 + .../src/FBSessionAuthLogger.h | 95 + .../src/FBSessionAuthLogger.m | 172 + .../FBSessionFacebookAppNativeLoginStategy.h | 19 + .../FBSessionFacebookAppNativeLoginStategy.m | 88 + .../src/FBSessionFacebookAppWebLoginStategy.h | 19 + .../src/FBSessionFacebookAppWebLoginStategy.m | 40 + .../src/FBSessionInlineWebViewLoginStategy.h | 19 + .../src/FBSessionInlineWebViewLoginStategy.m | 41 + .../src/FBSessionLoginStrategy.h | 65 + .../src/FBSessionLoginStrategyParams.h | 31 + .../src/FBSessionLoginStrategyParams.m | 28 + .../src/FBSessionManualTokenCachingStrategy.h | 32 + .../src/FBSessionManualTokenCachingStrategy.m | 50 + .../src/FBSessionSafariLoginStategy.h | 19 + .../src/FBSessionSafariLoginStategy.m | 41 + .../src/FBSessionSystemLoginStategy.h | 20 + .../src/FBSessionSystemLoginStategy.m | 49 + .../src/FBSessionTokenCachingStrategy.h | 159 + .../src/FBSessionTokenCachingStrategy.m | 152 + .../facebook-ios-sdk/src/FBSessionUtility.h | 37 + .../facebook-ios-sdk/src/FBSessionUtility.m | 190 + .../src/FBSettings+Internal.h | 53 + Vendor/facebook-ios-sdk/src/FBSettings.h | 297 + Vendor/facebook-ios-sdk/src/FBSettings.m | 401 + .../src/FBShareDialogParams.h | 66 + .../src/FBShareDialogParams.m | 141 + .../src/FBSystemAccountStoreAdapter.h | 95 + .../src/FBSystemAccountStoreAdapter.m | 319 + Vendor/facebook-ios-sdk/src/FBTask+Private.h | 29 + Vendor/facebook-ios-sdk/src/FBTask.h | 123 + Vendor/facebook-ios-sdk/src/FBTask.m | 353 + .../src/FBTaskCompletionSource.h | 85 + .../src/FBTaskCompletionSource.m | 87 + .../src/FBTestSession+Internal.h | 25 + Vendor/facebook-ios-sdk/src/FBTestSession.h | 138 + Vendor/facebook-ios-sdk/src/FBTestSession.m | 584 ++ Vendor/facebook-ios-sdk/src/FBURLConnection.h | 36 + Vendor/facebook-ios-sdk/src/FBURLConnection.m | 292 + .../src/FBUserSettingsViewController.h | 127 + .../src/FBUserSettingsViewController.m | 436 + .../Resources/en.lproj/Localizable.strings | Bin 0 -> 1732 bytes .../Resources/he.lproj/Localizable.strings | Bin 0 -> 1702 bytes .../images/facebook-logo.png | Bin 0 -> 6836 bytes .../images/facebook-logo@2x.png | Bin 0 -> 15092 bytes .../images/loginBackgroundIPadLandscape.jpg | Bin 0 -> 162667 bytes .../loginBackgroundIPadLandscape@2x.jpg | Bin 0 -> 340316 bytes .../images/loginBackgroundIPadPortrait.jpg | Bin 0 -> 99241 bytes .../images/loginBackgroundIPadPortrait@2x.jpg | Bin 0 -> 344708 bytes .../images/loginBackgroundIPhonePortrait.jpg | Bin 0 -> 27134 bytes .../loginBackgroundIPhonePortrait@2x.jpg | Bin 0 -> 65517 bytes .../images/silver-button-normal.png | Bin 0 -> 961 bytes .../images/silver-button-normal@2x.png | Bin 0 -> 2258 bytes .../images/silver-button-pressed.png | Bin 0 -> 978 bytes .../images/silver-button-pressed@2x.png | Bin 0 -> 2293 bytes Vendor/facebook-ios-sdk/src/FBUtility.h | 100 + Vendor/facebook-ios-sdk/src/FBUtility.m | 521 ++ .../src/FBViewController+Internal.h | 22 + .../facebook-ios-sdk/src/FBViewController.h | 118 + .../facebook-ios-sdk/src/FBViewController.m | 310 + Vendor/facebook-ios-sdk/src/FBWebDialogs.h | 234 + Vendor/facebook-ios-sdk/src/FBWebDialogs.m | 324 + Vendor/facebook-ios-sdk/src/Facebook.h | 281 + Vendor/facebook-ios-sdk/src/Facebook.m | 822 ++ Vendor/facebook-ios-sdk/src/FacebookSDK.h | 145 + .../FBAccessTokenDataTests.h | 22 + .../FBAccessTokenDataTests.m | 360 + .../FBAppEventsIntegrationTests.h | 21 + .../FBAppEventsIntegrationTests.m | 155 + .../FBBatchRequestIntegrationTests.h | 22 + .../FBBatchRequestIntegrationTests.m | 351 + .../FBCacheIntegrationTests.h | 29 + .../FBCacheIntegrationTests.m | 440 + .../FBIntegrationTests.h | 80 + .../FBIntegrationTests.m | 355 + .../FBOpenGraphActionTests.h | 39 + .../FBOpenGraphActionTests.m | 154 + .../FBRequestConnectionIntegrationTests.h | 22 + .../FBRequestConnectionIntegrationTests.m | 405 + .../FBRequestIntegrationTests.h | 24 + .../FBRequestIntegrationTests.m | 631 ++ .../FBSessionIntegrationTests.h | 21 + .../FBSessionIntegrationTests.m | 218 + .../FBTestSessionTests.h | 21 + .../FBTestSessionTests.m | 100 + .../FacebookSDKIntegrationTests-Info.plist | 22 + .../FacebookSDKIntegrationTests-Prefix.pch | 8 + .../en.lproj/InfoPlist.strings | 2 + .../src/FacebookSDKResources.bundle.README | 44 + .../src/Framework/Resources/Info.plist | 20 + .../ImageResources/FBDialog/FBDialogClose.png | Bin 0 -> 4401 bytes .../FBDialog/FBDialogClose@2x.png | Bin 0 -> 5554 bytes .../FBFriendPickerViewDefault.png | Bin 0 -> 834 bytes .../FBLoginView/FBLoginViewButton.png | Bin 0 -> 710 bytes .../FBLoginView/FBLoginViewButton@2x.png | Bin 0 -> 1479 bytes .../FBLoginView/FBLoginViewButtonPressed.png | Bin 0 -> 694 bytes .../FBLoginViewButtonPressed@2x.png | Bin 0 -> 1321 bytes .../FBPlacePickerViewGenericPlace.png | Bin 0 -> 813 bytes ...ProfilePictureViewBlankProfilePortrait.png | Bin 0 -> 3945 bytes ...FBProfilePictureViewBlankProfileSquare.png | Bin 0 -> 3784 bytes Vendor/facebook-ios-sdk/src/NSError+FBError.h | 59 + Vendor/facebook-ios-sdk/src/NSError+FBError.m | 35 + .../FacebookSDK.pmdoc/01package-contents.xml | 1 + .../Package/FacebookSDK.pmdoc/01package.xml | 1 + .../src/Package/FacebookSDK.pmdoc/index.xml | 1 + .../project.pbxproj | 2296 +++++ .../FacebookSDKIntegrationTests.xcscheme | 109 + .../xcschemes/facebook-ios-sdk-tests.xcscheme | 143 + .../xcschemes/facebook-ios-sdk.xcscheme | 95 + .../src/facebook_ios_sdk_Prefix.pch | 7 + .../src/tests/FBAppBridgeTests.h | 21 + .../src/tests/FBAppBridgeTests.m | 651 ++ .../src/tests/FBAppCallTests.h | 21 + .../src/tests/FBAppCallTests.m | 96 + .../src/tests/FBAppLinkDataTests.h | 21 + .../src/tests/FBAppLinkDataTests.m | 45 + .../src/tests/FBAuthenticationTests.h | 56 + .../src/tests/FBAuthenticationTests.m | 242 + .../src/tests/FBBatchRequestTests.h | 22 + .../src/tests/FBBatchRequestTests.m | 51 + .../facebook-ios-sdk/src/tests/FBCacheTests.h | 24 + .../facebook-ios-sdk/src/tests/FBCacheTests.m | 45 + .../tests/FBFacebookAppAuthenticationTests.h | 21 + .../tests/FBFacebookAppAuthenticationTests.m | 464 + .../src/tests/FBGraphObjectTests.h | 22 + .../src/tests/FBGraphObjectTests.m | 342 + .../FBIsStringRepresentingJSONDictionary.h | 33 + .../FBIsStringRepresentingJSONDictionary.m | 85 + .../src/tests/FBIsURLHavingQueryParams.h | 33 + .../src/tests/FBIsURLHavingQueryParams.m | 85 + .../tests/FBLoginDialogAuthenticationTests.h | 21 + .../tests/FBLoginDialogAuthenticationTests.m | 29 + .../src/tests/FBRequestConnectionTests.h | 22 + .../src/tests/FBRequestConnectionTests.m | 460 + .../src/tests/FBRequestTests.h | 21 + .../src/tests/FBRequestTests.m | 442 + .../src/tests/FBSafariAuthenticationTests.h | 21 + .../src/tests/FBSafariAuthenticationTests.m | 281 + .../src/tests/FBSessionTests.h | 23 + .../src/tests/FBSessionTests.m | 1401 +++ .../src/tests/FBSettingsTests.h | 21 + .../src/tests/FBSettingsTests.m | 48 + .../FBSystemAccountAuthenticationTests.h | 21 + .../FBSystemAccountAuthenticationTests.m | 234 + .../src/tests/FBTestBlocker.h | 45 + .../src/tests/FBTestBlocker.m | 116 + Vendor/facebook-ios-sdk/src/tests/FBTests.h | 42 + Vendor/facebook-ios-sdk/src/tests/FBTests.m | 117 + .../src/tests/FBURLConnectionTests.h | 21 + .../src/tests/FBURLConnectionTests.m | 622 ++ .../src/tests/FacebookSDKTests-Info.plist | 22 + .../src/tests/FacebookSDKTests-Prefix.pch | 15 + .../src/tests/FacebookSDKTests.xcconfig | 18 + .../src/tests/en.lproj/InfoPlist.strings | 2 + .../vendor/OCHamcrest/.gitignore | 7 + .../vendor/OCHamcrest/.gitmodules | 3 + .../vendor/OCHamcrest/CHANGES.txt | 209 + .../vendor/OCHamcrest/Documentation/Doxyfile | 1637 ++++ .../vendor/OCHamcrest/Documentation/Makefile | 9 + .../OCHamcrest/Documentation/README.txt | 3 + .../project.pbxproj | 270 + .../CustomDateMatcher/Example-Info.plist | 20 + .../CustomDateMatcher/IsGivenDayOfWeek.h | 17 + .../CustomDateMatcher/IsGivenDayOfWeek.m | 42 + .../Examples/CustomDateMatcher/SampleTest.m | 34 + .../Examples/MacExample/Example-Info.plist | 20 + .../OCHamcrest/Examples/MacExample/Example.m | 24 + .../MacExample.xcodeproj/project.pbxproj | 251 + .../Examples/iOSExample/Example-Info.plist | 20 + .../OCHamcrest/Examples/iOSExample/Example.m | 24 + .../iOSExample.xcodeproj/project.pbxproj | 238 + .../vendor/OCHamcrest/LICENSE.txt | 27 + .../vendor/OCHamcrest/README.md | 314 + .../OCHamcrest/Source/Core/HCAssertThat.h | 42 + .../OCHamcrest/Source/Core/HCAssertThat.m | 89 + .../Source/Core/HCBaseDescription.h | 33 + .../Source/Core/HCBaseDescription.m | 107 + .../OCHamcrest/Source/Core/HCBaseMatcher.h | 27 + .../OCHamcrest/Source/Core/HCBaseMatcher.m | 55 + .../OCHamcrest/Source/Core/HCDescription.h | 48 + .../vendor/OCHamcrest/Source/Core/HCMatcher.h | 56 + .../OCHamcrest/Source/Core/HCSelfDescribing.h | 32 + .../Source/Core/HCStringDescription.h | 43 + .../Source/Core/HCStringDescription.m | 47 + .../Source/Core/Helpers/HCCollectMatchers.h | 23 + .../Source/Core/Helpers/HCCollectMatchers.m | 27 + .../Source/Core/Helpers/HCInvocationMatcher.h | 57 + .../Source/Core/Helpers/HCInvocationMatcher.m | 85 + .../Core/Helpers/HCRequireNonNilObject.h | 19 + .../Core/Helpers/HCRequireNonNilObject.m | 21 + .../Core/Helpers/HCRequireNonNilString.h | 21 + .../Core/Helpers/HCRequireNonNilString.m | 21 + .../Source/Core/Helpers/HCWrapInMatcher.h | 22 + .../Source/Core/Helpers/HCWrapInMatcher.m | 24 + .../vendor/OCHamcrest/Source/CustomMatchers.h | 120 + .../Source/Library/Collection/HCHasCount.h | 63 + .../Source/Library/Collection/HCHasCount.m | 70 + .../Collection/HCIsCollectionContaining.h | 69 + .../Collection/HCIsCollectionContaining.m | 76 + .../HCIsCollectionContainingInAnyOrder.h | 47 + .../HCIsCollectionContainingInAnyOrder.m | 142 + .../HCIsCollectionContainingInOrder.h | 45 + .../HCIsCollectionContainingInOrder.m | 161 + .../Collection/HCIsCollectionOnlyContaining.h | 52 + .../Collection/HCIsCollectionOnlyContaining.m | 72 + .../Collection/HCIsDictionaryContaining.h | 54 + .../Collection/HCIsDictionaryContaining.m | 66 + .../HCIsDictionaryContainingEntries.h | 53 + .../HCIsDictionaryContainingEntries.m | 141 + .../Collection/HCIsDictionaryContainingKey.h | 49 + .../Collection/HCIsDictionaryContainingKey.m | 56 + .../HCIsDictionaryContainingValue.h | 49 + .../HCIsDictionaryContainingValue.m | 56 + .../Library/Collection/HCIsEmptyCollection.h | 36 + .../Library/Collection/HCIsEmptyCollection.m | 47 + .../Source/Library/Collection/HCIsIn.h | 42 + .../Source/Library/Collection/HCIsIn.m | 56 + .../Source/Library/Decorator/HCDescribedAs.h | 50 + .../Source/Library/Decorator/HCDescribedAs.m | 130 + .../Source/Library/Decorator/HCIs.h | 54 + .../Source/Library/Decorator/HCIs.m | 54 + .../Source/Library/Logical/HCAllOf.h | 45 + .../Source/Library/Logical/HCAllOf.m | 73 + .../Source/Library/Logical/HCAnyOf.h | 45 + .../Source/Library/Logical/HCAnyOf.m | 57 + .../Source/Library/Logical/HCIsAnything.h | 63 + .../Source/Library/Logical/HCIsAnything.m | 64 + .../Source/Library/Logical/HCIsNot.h | 47 + .../Source/Library/Logical/HCIsNot.m | 49 + .../Source/Library/Number/HCBoxNumber.h | 95 + .../Source/Library/Number/HCIsCloseTo.h | 47 + .../Source/Library/Number/HCIsCloseTo.m | 70 + .../Source/Library/Number/HCIsEqualToNumber.h | 325 + .../Source/Library/Number/HCIsEqualToNumber.m | 35 + .../Source/Library/Number/HCNumberAssert.h | 387 + .../Source/Library/Number/HCNumberAssert.m | 36 + .../Library/Number/HCOrderingComparison.h | 115 + .../Library/Number/HCOrderingComparison.m | 103 + .../Library/Object/HCConformsToProtocol.h | 43 + .../Library/Object/HCConformsToProtocol.m | 55 + .../Source/Library/Object/HCHasDescription.h | 44 + .../Source/Library/Object/HCHasDescription.m | 39 + .../Source/Library/Object/HCHasProperty.h | 49 + .../Source/Library/Object/HCHasProperty.m | 150 + .../Source/Library/Object/HCIsEqual.h | 44 + .../Source/Library/Object/HCIsEqual.m | 58 + .../Source/Library/Object/HCIsInstanceOf.h | 45 + .../Source/Library/Object/HCIsInstanceOf.m | 52 + .../Source/Library/Object/HCIsNil.h | 47 + .../Source/Library/Object/HCIsNil.m | 46 + .../Source/Library/Object/HCIsSame.h | 42 + .../Source/Library/Object/HCIsSame.m | 57 + .../Library/Text/HCIsEqualIgnoringCase.h | 49 + .../Library/Text/HCIsEqualIgnoringCase.m | 55 + .../Text/HCIsEqualIgnoringWhiteSpace.h | 50 + .../Text/HCIsEqualIgnoringWhiteSpace.m | 97 + .../Source/Library/Text/HCStringContains.h | 45 + .../Source/Library/Text/HCStringContains.m | 41 + .../Library/Text/HCStringContainsInOrder.h | 50 + .../Library/Text/HCStringContainsInOrder.m | 85 + .../Source/Library/Text/HCStringEndsWith.h | 45 + .../Source/Library/Text/HCStringEndsWith.m | 41 + .../Source/Library/Text/HCStringStartsWith.h | 45 + .../Source/Library/Text/HCStringStartsWith.m | 41 + .../Source/Library/Text/HCSubstringMatcher.h | 20 + .../Source/Library/Text/HCSubstringMatcher.m | 41 + .../vendor/OCHamcrest/Source/MainPage.h | 216 + .../OCHamcrest/Source/MakeDistribution.sh | 57 + .../OCHamcrest/Source/MakeDocumentation.sh | 13 + .../OCHamcrest/Source/MakeIOSFramework.sh | 48 + .../OCHamcrest/Source/OCHamcrest-Info.plist | 30 + .../OCHamcrest/Source/OCHamcrest-Prefix.pch | 10 + .../vendor/OCHamcrest/Source/OCHamcrest.h | 137 + .../OCHamcrest.xcodeproj/project.pbxproj | 1490 ++++ .../xcschemes/OCHamcrest.xcscheme | 69 + .../xcschemes/libochamcrest.xcscheme | 69 + .../vendor/OCHamcrest/Source/Tests-Info.plist | 20 + .../Source/Tests/Collection/FakeWithCount.h | 23 + .../Source/Tests/Collection/FakeWithCount.m | 36 + .../Tests/Collection/FakeWithoutCount.h | 17 + .../Tests/Collection/FakeWithoutCount.m | 25 + .../Source/Tests/Collection/HasCountTest.m | 128 + .../IsCollectionContainingInAnyOrderTest.m | 114 + .../IsCollectionContainingInOrderTest.m | 111 + .../Collection/IsCollectionContainingTest.m | 162 + .../IsCollectionOnlyContainingTest.m | 94 + .../IsDictionaryContainingEntriesTest.m | 135 + .../IsDictionaryContainingKeyTest.m | 97 + .../Collection/IsDictionaryContainingTest.m | 84 + .../IsDictionaryContainingValueTest.m | 97 + .../Tests/Collection/IsEmptyCollectionTest.m | 61 + .../Source/Tests/Collection/IsInTest.m | 69 + .../Source/Tests/Core/AbstractMatcherTest.h | 51 + .../Source/Tests/Core/AbstractMatcherTest.m | 133 + .../Source/Tests/Core/AssertThatTest.m | 131 + .../Source/Tests/Core/BaseMatcherTest.m | 86 + .../Source/Tests/Core/InvocationMatcherTest.m | 152 + .../Source/Tests/Core/StringDescriptionTest.m | 195 + .../Source/Tests/Decorator/DescribedAsTest.m | 107 + .../Source/Tests/Decorator/IsTest.m | 73 + .../Source/Tests/Decorator/NeverMatch.h | 20 + .../Source/Tests/Decorator/NeverMatch.m | 37 + .../Source/Tests/Logical/AllOfTest.m | 103 + .../Source/Tests/Logical/AnyOfTest.m | 105 + .../Source/Tests/Logical/IsAnythingTest.m | 51 + .../Source/Tests/Logical/IsNotTest.m | 63 + .../Source/Tests/Number/IsCloseToTest.m | 81 + .../Source/Tests/Number/IsEqualToNumberTest.m | 390 + .../Source/Tests/Number/NumberAssertTest.m | 317 + .../Tests/Number/OrderingComparisonTest.m | 111 + .../Tests/Object/ConformsToProtocolTest.m | 78 + .../Source/Tests/Object/HasDescriptionTest.m | 84 + .../Source/Tests/Object/HasPropertyTest.m | 300 + .../Source/Tests/Object/IsEqualTest.m | 111 + .../Source/Tests/Object/IsInstanceOfTest.m | 64 + .../Source/Tests/Object/IsNilTest.m | 108 + .../Source/Tests/Object/IsSameTest.m | 99 + .../Tests/Text/IsEqualIgnoringCaseTest.m | 88 + .../Text/IsEqualIgnoringWhiteSpaceTest.m | 93 + .../Tests/Text/StringContainsInOrderTest.m | 96 + .../Source/Tests/Text/StringContainsTest.m | 92 + .../Source/Tests/Text/StringEndsWithTest.m | 92 + .../Source/Tests/Text/StringStartsWithTest.m | 92 + .../OCHamcrest/Source/Warnings.xcconfig | 67 + .../Source/XcodeCoverage/.gitignore | 2 + .../Source/XcodeCoverage/LICENSE.txt | 21 + .../OCHamcrest/Source/XcodeCoverage/README.md | 59 + .../OCHamcrest/Source/XcodeCoverage/cleancov | 10 + .../OCHamcrest/Source/XcodeCoverage/envcov.sh | 13 + .../Source/XcodeCoverage/exportenv.sh | 7 + .../OCHamcrest/Source/XcodeCoverage/getcov | 48 + .../facebook-ios-sdk/vendor/OCMock/.gitignore | 5 + .../ArcExample.xcodeproj/project.pbxproj | 244 + .../ArcExample/ArcExample-Prefix.pch | 7 + .../ArcExample/ArcExample/ArcExample.1 | 79 + .../Examples/ArcExample/ArcExample/main.m | 34 + .../iOS5Example.xcodeproj/project.pbxproj | 505 ++ .../iOS5Example/iOS5Example/AppDelegate.h | 12 + .../iOS5Example/iOS5Example/AppDelegate.m | 62 + .../iOS5Example/DetailViewController.h | 14 + .../iOS5Example/DetailViewController.m | 112 + .../iOS5Example/MasterViewController.h | 14 + .../iOS5Example/MasterViewController.m | 116 + .../iOS5Example/en.lproj/InfoPlist.strings | 2 + .../en.lproj/MainStoryboard_iPad.storyboard | 130 + .../en.lproj/MainStoryboard_iPhone.storyboard | 101 + .../iOS5Example/iOS5Example-Info.plist | 51 + .../iOS5Example/iOS5Example-Prefix.pch | 14 + .../Examples/iOS5Example/iOS5Example/main.m | 15 + .../iOS5ExampleTests/ProtocolTests.h | 10 + .../iOS5ExampleTests/ProtocolTests.m | 56 + .../en.lproj/InfoPlist.strings | 2 + .../iOS5ExampleTests-Info.plist | 22 + .../iOS5ExampleTests/iOS5ExampleTests.h | 10 + .../iOS5ExampleTests/iOS5ExampleTests.m | 47 + .../NSNotificationCenter+OCMAdditions.h | 15 + .../iOS5Example/usr/include/OCMock/OCMArg.h | 33 + .../usr/include/OCMock/OCMConstraint.h | 64 + .../iOS5Example/usr/include/OCMock/OCMock.h | 10 + .../usr/include/OCMock/OCMockObject.h | 43 + .../usr/include/OCMock/OCMockRecorder.h | 32 + .../Classes/RootViewController.h | 14 + .../Classes/RootViewController.m | 62 + .../Classes/iPhoneExampleAppDelegate.h | 21 + .../Classes/iPhoneExampleAppDelegate.m | 90 + .../NSNotificationCenter+OCMAdditions.h | 15 + .../Libraries/Headers/OCMock/OCMArg.h | 33 + .../Libraries/Headers/OCMock/OCMConstraint.h | 64 + .../Libraries/Headers/OCMock/OCMock.h | 10 + .../Libraries/Headers/OCMock/OCMockObject.h | 41 + .../Libraries/Headers/OCMock/OCMockRecorder.h | 31 + .../iPhoneExample/Libraries/libOCMock.a | Bin 0 -> 861228 bytes .../Examples/iPhoneExample/MainWindow.xib | 542 ++ .../Examples/iPhoneExample/OCMockLogo.png | Bin 0 -> 1804 bytes .../iPhoneExample/RootViewController.xib | 384 + .../Tests/RootViewControllerTests.h | 17 + .../Tests/RootViewControllerTests.m | 75 + .../iPhoneExample/iPhoneExample-Info.plist | 30 + .../iPhoneExample.xcodeproj/project.pbxproj | 536 ++ .../iPhoneExampleTests-Info.plist | 20 + .../iPhoneExample/iPhoneExample_Prefix.pch | 14 + .../OCMock/Examples/iPhoneExample/main.m | 17 + .../facebook-ios-sdk/vendor/OCMock/README.md | 10 + .../vendor/OCMock/Source/Changes.txt | 276 + .../Source/Frameworks/OCHamcrest.tar.bz2 | Bin 0 -> 81491 bytes .../vendor/OCMock/Source/License.txt | 15 + .../Source/OCMock.xcodeproj/project.pbxproj | 989 +++ .../Source/OCMock/NSInvocation+OCMAdditions.h | 34 + .../Source/OCMock/NSInvocation+OCMAdditions.m | 337 + .../OCMock/NSMethodSignature+OCMAdditions.h | 18 + .../OCMock/NSMethodSignature+OCMAdditions.m | 19 + .../NSNotificationCenter+OCMAdditions.h | 15 + .../NSNotificationCenter+OCMAdditions.m | 17 + .../OCMock/Source/OCMock/OCClassMockObject.h | 21 + .../OCMock/Source/OCMock/OCClassMockObject.m | 160 + .../vendor/OCMock/Source/OCMock/OCMArg.h | 34 + .../vendor/OCMock/Source/OCMock/OCMArg.m | 79 + .../OCMock/Source/OCMock/OCMBlockCaller.h | 21 + .../OCMock/Source/OCMock/OCMBlockCaller.m | 32 + .../OCMock/OCMBoxedReturnValueProvider.h | 12 + .../OCMock/OCMBoxedReturnValueProvider.m | 21 + .../OCMock/Source/OCMock/OCMConstraint.h | 64 + .../OCMock/Source/OCMock/OCMConstraint.m | 142 + .../OCMock/OCMExceptionReturnValueProvider.h | 12 + .../OCMock/OCMExceptionReturnValueProvider.m | 16 + .../OCMock/OCMIndirectReturnValueProvider.h | 18 + .../OCMock/OCMIndirectReturnValueProvider.m | 33 + .../Source/OCMock/OCMNotificationPoster.h | 17 + .../Source/OCMock/OCMNotificationPoster.m | 30 + .../Source/OCMock/OCMObserverRecorder.h | 19 + .../Source/OCMock/OCMObserverRecorder.m | 75 + .../OCMock/Source/OCMock/OCMPassByRefSetter.h | 17 + .../OCMock/Source/OCMock/OCMPassByRefSetter.m | 29 + .../Source/OCMock/OCMRealObjectForwarder.h | 14 + .../Source/OCMock/OCMRealObjectForwarder.m | 29 + .../Source/OCMock/OCMReturnValueProvider.h | 17 + .../Source/OCMock/OCMReturnValueProvider.m | 47 + .../OCMock/Source/OCMock/OCMock-Info.plist | 30 + .../OCMock/Source/OCMock/OCMock-Prefix.pch | 7 + .../vendor/OCMock/Source/OCMock/OCMock.h | 10 + .../OCMock/Source/OCMock/OCMockObject.h | 46 + .../OCMock/Source/OCMock/OCMockObject.m | 269 + .../OCMock/Source/OCMock/OCMockRecorder.h | 36 + .../OCMock/Source/OCMock/OCMockRecorder.m | 229 + .../Source/OCMock/OCObserverMockObject.h | 22 + .../Source/OCMock/OCObserverMockObject.m | 83 + .../Source/OCMock/OCPartialMockObject.h | 25 + .../Source/OCMock/OCPartialMockObject.m | 190 + .../Source/OCMock/OCPartialMockRecorder.h | 12 + .../Source/OCMock/OCPartialMockRecorder.m | 27 + .../Source/OCMock/OCProtocolMockObject.h | 16 + .../Source/OCMock/OCProtocolMockObject.m | 53 + .../Source/OCMock/en.lproj/InfoPlist.strings | 2 + .../Source/OCMockLib/OCMockLib-Prefix.pch | 7 + .../NSInvocationOCMAdditionsTests.h | 12 + .../NSInvocationOCMAdditionsTests.m | 346 + .../Source/OCMockTests/OCMConstraintTests.h | 14 + .../Source/OCMockTests/OCMConstraintTests.m | 131 + .../OCMockObjectClassMethodMockingTests.h | 10 + .../OCMockObjectClassMethodMockingTests.m | 119 + .../OCMockObjectForwardingTargetTests.h | 10 + .../OCMockObjectForwardingTargetTests.m | 136 + .../OCMockTests/OCMockObjectHamcrestTests.h | 13 + .../OCMockTests/OCMockObjectHamcrestTests.mm | 31 + .../OCMockObjectPartialMocksTests.h | 10 + .../OCMockObjectPartialMocksTests.m | 174 + .../Source/OCMockTests/OCMockObjectTests.h | 13 + .../Source/OCMockTests/OCMockObjectTests.m | 812 ++ .../Source/OCMockTests/OCMockRecorderTests.h | 13 + .../Source/OCMockTests/OCMockRecorderTests.m | 81 + .../Source/OCMockTests/OCMockTests-Info.plist | 22 + .../OCMockTests/OCObserverMockObjectTests.h | 16 + .../OCMockTests/OCObserverMockObjectTests.m | 127 + .../OCMockTests/en.lproj/InfoPlist.strings | 2 + .../vendor/OCMock/Tools/build.rb | 165 + .../vendor/OHHTTPStubs/.gitignore | 13 + .../vendor/OHHTTPStubs/.gitmodules | 3 + .../vendor/OHHTTPStubs/LICENSE | 29 + .../OHHTTPStubs/OHHTTPStubs/OHHTTPStubs.h | 70 + .../OHHTTPStubs/OHHTTPStubs/OHHTTPStubs.m | 302 + .../OHHTTPStubs.xcodeproj/project.pbxproj | 423 + .../xcschemes/OHHTTPStubs.xcscheme | 74 + .../OHHTTPStubs/OHHTTPStubsResponse.h | 86 + .../OHHTTPStubs/OHHTTPStubsResponse.m | 142 + .../OHHTTPStubs/PortableLibrary.xcconfig | 19 + .../UnitTests/AFNetworking/.gitignore | 18 + .../AFNetworking/AFNetworking.podspec | 28 + .../AFNetworking/AFNetworking/AFHTTPClient.h | 581 ++ .../AFNetworking/AFNetworking/AFHTTPClient.m | 1208 +++ .../AFNetworking/AFHTTPRequestOperation.h | 133 + .../AFNetworking/AFHTTPRequestOperation.m | 369 + .../AFNetworking/AFImageRequestOperation.h | 108 + .../AFNetworking/AFImageRequestOperation.m | 233 + .../AFNetworking/AFJSONRequestOperation.h | 71 + .../AFNetworking/AFJSONRequestOperation.m | 137 + .../AFNetworkActivityIndicatorManager.h | 75 + .../AFNetworkActivityIndicatorManager.m | 131 + .../AFNetworking/AFNetworking/AFNetworking.h | 43 + .../AFPropertyListRequestOperation.h | 68 + .../AFPropertyListRequestOperation.m | 141 + .../AFNetworking/AFURLConnectionOperation.h | 309 + .../AFNetworking/AFURLConnectionOperation.m | 712 ++ .../AFNetworking/AFXMLRequestOperation.h | 89 + .../AFNetworking/AFXMLRequestOperation.m | 165 + .../AFNetworking/UIImageView+AFNetworking.h | 78 + .../AFNetworking/UIImageView+AFNetworking.m | 180 + .../UnitTests/AFNetworking/CHANGES | 513 ++ .../Example/AFNetworking Example.entitlements | 12 + .../project.pbxproj | 361 + .../project.pbxproj | 447 + .../AFNetworking/Example/AppDelegate.h | 44 + .../AFNetworking/Example/AppDelegate.m | 93 + .../Example/Classes/AFAppDotNetAPIClient.h | 30 + .../Example/Classes/AFAppDotNetAPIClient.m | 55 + .../GlobalTimelineViewController.h | 27 + .../GlobalTimelineViewController.m | 114 + .../Example/Classes/Models/Post.h | 38 + .../Example/Classes/Models/Post.m | 68 + .../Example/Classes/Models/User.h | 39 + .../Example/Classes/Models/User.m | 97 + .../Example/Classes/Views/PostTableViewCell.h | 33 + .../Example/Classes/Views/PostTableViewCell.m | 81 + .../AFNetworking/Example/Default-568h@2x.png | Bin 0 -> 18594 bytes .../AFNetworking/Example/Default.png | Bin 0 -> 942 bytes .../AFNetworking/Example/Default@2x.png | Bin 0 -> 5104 bytes .../UnitTests/AFNetworking/Example/Icon.png | Bin 0 -> 6547 bytes .../AFNetworking/Example/Icon@2x.png | Bin 0 -> 10055 bytes .../Images/profile-image-placeholder.png | Bin 0 -> 1098 bytes .../Images/profile-image-placeholder@2x.png | Bin 0 -> 2767 bytes .../AFNetworking/Example/Mac-Info.plist | 34 + .../AFNetworking/Example/MainMenu.xib | 2070 +++++ .../UnitTests/AFNetworking/Example/Prefix.pch | 20 + .../Example/en.lproj/MainMenu.xib | 4587 ++++++++++ .../AFNetworking/Example/iOS-Info.plist | 56 + .../UnitTests/AFNetworking/Example/main.m | 37 + .../UnitTests/AFNetworking/LICENSE | 19 + .../UnitTests/AFNetworking/README.md | 179 + .../OHHTTPStubs/UnitTests/AsyncSenTestCase.h | 34 + .../OHHTTPStubs/UnitTests/AsyncSenTestCase.m | 82 + .../UnitTests/Test Suites/AFNetworkingTests.m | 61 + .../NSURLConnectionDelegateTests.m | 228 + .../Test Suites/NSURLConnectionTests.m | 173 + .../Test Suites/WithContentsOfURLTests.m | 110 + .../UnitTests/UnitTests-Info.plist | 22 + .../UnitTests/UnitTests-Prefix.pch | 7 + .../contents.xcworkspacedata | 10 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../OHHTTPStubsDemo/Default-568h@2x.png | Bin 0 -> 18594 bytes .../OHHTTPStubsDemo/MainViewController.h | 24 + .../OHHTTPStubsDemo/MainViewController.m | 171 + .../OHHTTPStubsDemo/MainViewController.xib | 725 ++ .../OHHTTPStubsDemo-Info.plist | 40 + .../OHHTTPStubsDemo-Prefix.pch | 14 + .../OHHTTPStubsDemo.xcodeproj/project.pbxproj | 282 + .../xcschemes/OHHTTPStubsDemo.xcscheme | 101 + .../vendor/OHHTTPStubs/OHHTTPStubsDemo/main.m | 17 + .../OHHTTPStubs/OHHTTPStubsDemo/stub.jpg | Bin 0 -> 46859 bytes .../OHHTTPStubs/OHHTTPStubsDemo/stub.txt | 1 + .../vendor/OHHTTPStubs/README.md | 277 + Vendor/libidn/build-libidn.sh | 184 + Vendor/libidn/idn-int.h | 638 ++ Vendor/libidn/libidn.a | Bin 0 -> 1651132 bytes Vendor/libidn/stringprep.h | 241 + XMPPFramework.h | 1 + module/module.modulemap | 21 + 1261 files changed, 199158 insertions(+), 7303 deletions(-) create mode 100644 Authentication/Anonymous/XMPPAnonymousAuthentication.h create mode 100644 Authentication/Anonymous/XMPPAnonymousAuthentication.m create mode 100644 Authentication/Deprecated-Digest/XMPPDeprecatedDigestAuthentication.h create mode 100644 Authentication/Deprecated-Digest/XMPPDeprecatedDigestAuthentication.m create mode 100644 Authentication/Deprecated-Plain/XMPPDeprecatedPlainAuthentication.h create mode 100644 Authentication/Deprecated-Plain/XMPPDeprecatedPlainAuthentication.m create mode 100644 Authentication/Digest-MD5/XMPPDigestMD5Authentication.h create mode 100644 Authentication/Digest-MD5/XMPPDigestMD5Authentication.m create mode 100644 Authentication/Plain/XMPPPlainAuthentication.h create mode 100644 Authentication/Plain/XMPPPlainAuthentication.m create mode 100644 Authentication/SCRAM-SHA-1/XMPPSCRAMSHA1Authentication.h create mode 100644 Authentication/SCRAM-SHA-1/XMPPSCRAMSHA1Authentication.m create mode 100644 Authentication/X-Facebook-Platform/XMPPXFacebookPlatformAuthentication.h create mode 100644 Authentication/X-Facebook-Platform/XMPPXFacebookPlatformAuthentication.m create mode 100644 Authentication/X-OAuth2-Google/XMPPXOAuth2Google.h create mode 100644 Authentication/X-OAuth2-Google/XMPPXOAuth2Google.m create mode 100644 Authentication/XMPPCustomBinding.h create mode 100644 Authentication/XMPPSASLAuthentication.h create mode 100644 Categories/NSData+XMPP.h create mode 100644 Categories/NSData+XMPP.m create mode 100644 Categories/NSNumber+XMPP.h create mode 100644 Categories/NSNumber+XMPP.m create mode 100644 Categories/NSXMLElement+XMPP.h create mode 100644 Categories/NSXMLElement+XMPP.m create mode 100644 Core/XMPP.h create mode 100644 Core/XMPPConstants.h create mode 100644 Core/XMPPConstants.m create mode 100644 Core/XMPPElement.h create mode 100644 Core/XMPPElement.m create mode 100644 Core/XMPPIQ.h create mode 100644 Core/XMPPIQ.m create mode 100644 Core/XMPPInternal.h create mode 100644 Core/XMPPJID.h create mode 100644 Core/XMPPJID.m create mode 100644 Core/XMPPLogging.h create mode 100644 Core/XMPPMessage.h create mode 100644 Core/XMPPMessage.m create mode 100644 Core/XMPPModule.h create mode 100644 Core/XMPPModule.m create mode 100644 Core/XMPPParser.h create mode 100644 Core/XMPPParser.m create mode 100644 Core/XMPPPresence.h create mode 100644 Core/XMPPPresence.m create mode 100644 Core/XMPPStream.h create mode 100644 Core/XMPPStream.m delete mode 100644 Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.h delete mode 100644 Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.swift delete mode 100644 Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.h delete mode 100644 Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.m delete mode 100644 Example/Pods/CocoaLumberjack/Classes/DDASLLogger.h delete mode 100644 Example/Pods/CocoaLumberjack/Classes/DDASLLogger.m delete mode 100644 Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.m delete mode 100644 Example/Pods/CocoaLumberjack/Classes/DDAssertMacros.h delete mode 100644 Example/Pods/CocoaLumberjack/Classes/DDLegacyMacros.h delete mode 100644 Example/Pods/CocoaLumberjack/Classes/DDLog+LOGV.h delete mode 100644 Example/Pods/CocoaLumberjack/Classes/DDLog.h delete mode 100644 Example/Pods/CocoaLumberjack/Classes/DDLogMacros.h delete mode 100644 Example/Pods/CocoaLumberjack/Classes/DDTTYLogger.m delete mode 100644 Example/Pods/CocoaLumberjack/Classes/Extensions/DDContextFilterLogFormatter.h delete mode 100644 Example/Pods/CocoaLumberjack/Classes/Extensions/DDContextFilterLogFormatter.m delete mode 100644 Example/Pods/CocoaLumberjack/Classes/Extensions/DDDispatchQueueLogFormatter.h delete mode 100644 Example/Pods/CocoaLumberjack/Classes/Extensions/DDDispatchQueueLogFormatter.m delete mode 100644 Example/Pods/CocoaLumberjack/Classes/Extensions/DDMultiFormatter.h delete mode 100644 Example/Pods/CocoaLumberjack/Framework/Lumberjack/CocoaLumberjack.modulemap create mode 100644 Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogCapture.h create mode 100644 Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogCapture.m create mode 100755 Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogger.h create mode 100755 Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogger.m rename Example/Pods/CocoaLumberjack/{Classes => Lumberjack}/DDAbstractDatabaseLogger.h (68%) create mode 100644 Example/Pods/CocoaLumberjack/Lumberjack/DDAbstractDatabaseLogger.m create mode 100644 Example/Pods/CocoaLumberjack/Lumberjack/DDAssert.h rename Example/Pods/CocoaLumberjack/{Classes => Lumberjack}/DDFileLogger.h (51%) rename Example/Pods/CocoaLumberjack/{Classes => Lumberjack}/DDFileLogger.m (65%) create mode 100644 Example/Pods/CocoaLumberjack/Lumberjack/DDLog+LOGV.h create mode 100755 Example/Pods/CocoaLumberjack/Lumberjack/DDLog.h rename Example/Pods/CocoaLumberjack/{Classes => Lumberjack}/DDLog.m (57%) mode change 100644 => 100755 create mode 100755 Example/Pods/CocoaLumberjack/Lumberjack/DDTTYLogger.h create mode 100755 Example/Pods/CocoaLumberjack/Lumberjack/DDTTYLogger.m create mode 100644 Example/Pods/CocoaLumberjack/Lumberjack/Extensions/DDContextFilterLogFormatter.h create mode 100644 Example/Pods/CocoaLumberjack/Lumberjack/Extensions/DDContextFilterLogFormatter.m create mode 100644 Example/Pods/CocoaLumberjack/Lumberjack/Extensions/DDDispatchQueueLogFormatter.h create mode 100644 Example/Pods/CocoaLumberjack/Lumberjack/Extensions/DDDispatchQueueLogFormatter.m create mode 100644 Example/Pods/CocoaLumberjack/Lumberjack/Extensions/DDMultiFormatter.h rename Example/Pods/CocoaLumberjack/{Classes => Lumberjack}/Extensions/DDMultiFormatter.m (73%) create mode 100644 Example/Pods/CocoaLumberjack/Lumberjack/Extensions/README.txt create mode 100644 Example/Pods/Target Support Files/CocoaLumberjack/CocoaLumberjack-umbrella.h create mode 100644 Extensions/BandwidthMonitor/XMPPBandwidthMonitor.h create mode 100644 Extensions/BandwidthMonitor/XMPPBandwidthMonitor.m create mode 100644 Extensions/CoreDataStorage/XMPPCoreDataStorage.h create mode 100644 Extensions/CoreDataStorage/XMPPCoreDataStorage.m create mode 100644 Extensions/CoreDataStorage/XMPPCoreDataStorageProtected.h create mode 100644 Extensions/FileTransfer/XMPPFileTransfer.h create mode 100644 Extensions/FileTransfer/XMPPFileTransfer.m create mode 100644 Extensions/FileTransfer/XMPPIncomingFileTransfer.h create mode 100644 Extensions/FileTransfer/XMPPIncomingFileTransfer.m create mode 100644 Extensions/FileTransfer/XMPPOutgoingFileTransfer.h create mode 100644 Extensions/FileTransfer/XMPPOutgoingFileTransfer.m create mode 100644 Extensions/GoogleSharedStatus/XMPPGoogleSharedStatus.h create mode 100644 Extensions/GoogleSharedStatus/XMPPGoogleSharedStatus.m create mode 100644 Extensions/ProcessOne/XMPPProcessOne.h create mode 100644 Extensions/ProcessOne/XMPPProcessOne.m create mode 100644 Extensions/Reconnect/XMPPReconnect.h create mode 100644 Extensions/Reconnect/XMPPReconnect.m create mode 100644 Extensions/Roster/CoreDataStorage/XMPPGroupCoreDataStorageObject.h create mode 100644 Extensions/Roster/CoreDataStorage/XMPPGroupCoreDataStorageObject.m create mode 100644 Extensions/Roster/CoreDataStorage/XMPPResourceCoreDataStorageObject.h create mode 100644 Extensions/Roster/CoreDataStorage/XMPPResourceCoreDataStorageObject.m create mode 100644 Extensions/Roster/CoreDataStorage/XMPPRoster.xcdatamodel/elements create mode 100644 Extensions/Roster/CoreDataStorage/XMPPRoster.xcdatamodel/layout create mode 100644 Extensions/Roster/CoreDataStorage/XMPPRosterCoreDataStorage.h create mode 100644 Extensions/Roster/CoreDataStorage/XMPPRosterCoreDataStorage.m create mode 100644 Extensions/Roster/CoreDataStorage/XMPPUserCoreDataStorageObject.h create mode 100644 Extensions/Roster/CoreDataStorage/XMPPUserCoreDataStorageObject.m create mode 100644 Extensions/Roster/MemoryStorage/XMPPResourceMemoryStorageObject.h create mode 100644 Extensions/Roster/MemoryStorage/XMPPResourceMemoryStorageObject.m create mode 100644 Extensions/Roster/MemoryStorage/XMPPRosterMemoryStorage.h create mode 100644 Extensions/Roster/MemoryStorage/XMPPRosterMemoryStorage.m create mode 100644 Extensions/Roster/MemoryStorage/XMPPRosterMemoryStoragePrivate.h create mode 100644 Extensions/Roster/MemoryStorage/XMPPUserMemoryStorageObject.h create mode 100644 Extensions/Roster/MemoryStorage/XMPPUserMemoryStorageObject.m create mode 100644 Extensions/Roster/XMPPResource.h create mode 100644 Extensions/Roster/XMPPRoster.h create mode 100644 Extensions/Roster/XMPPRoster.m create mode 100644 Extensions/Roster/XMPPRosterPrivate.h create mode 100644 Extensions/Roster/XMPPUser.h create mode 100644 Extensions/SystemInputActivityMonitor/XMPPSystemInputActivityMonitor.h create mode 100644 Extensions/SystemInputActivityMonitor/XMPPSystemInputActivityMonitor.m create mode 100644 Extensions/XEP-0009/XMPPIQ+JabberRPC.h create mode 100644 Extensions/XEP-0009/XMPPIQ+JabberRPC.m create mode 100644 Extensions/XEP-0009/XMPPIQ+JabberRPCResonse.h create mode 100644 Extensions/XEP-0009/XMPPIQ+JabberRPCResonse.m create mode 100644 Extensions/XEP-0009/XMPPJabberRPCModule.h create mode 100644 Extensions/XEP-0009/XMPPJabberRPCModule.m create mode 100644 Extensions/XEP-0012/XMPPIQ+LastActivity.h create mode 100644 Extensions/XEP-0012/XMPPIQ+LastActivity.m create mode 100644 Extensions/XEP-0012/XMPPLastActivity.h create mode 100644 Extensions/XEP-0012/XMPPLastActivity.m create mode 100644 Extensions/XEP-0016/XMPPPrivacy.h create mode 100644 Extensions/XEP-0016/XMPPPrivacy.m create mode 100644 Extensions/XEP-0045/CoreDataStorage/XMPPRoom.xcdatamodeld/.xccurrentversion create mode 100644 Extensions/XEP-0045/CoreDataStorage/XMPPRoom.xcdatamodeld/XMPPRoom.xcdatamodel/contents create mode 100644 Extensions/XEP-0045/CoreDataStorage/XMPPRoomCoreDataStorage.h create mode 100644 Extensions/XEP-0045/CoreDataStorage/XMPPRoomCoreDataStorage.m create mode 100644 Extensions/XEP-0045/CoreDataStorage/XMPPRoomMessageCoreDataStorageObject.h create mode 100644 Extensions/XEP-0045/CoreDataStorage/XMPPRoomMessageCoreDataStorageObject.m create mode 100644 Extensions/XEP-0045/CoreDataStorage/XMPPRoomOccupantCoreDataStorageObject.h create mode 100644 Extensions/XEP-0045/CoreDataStorage/XMPPRoomOccupantCoreDataStorageObject.m create mode 100644 Extensions/XEP-0045/HybridStorage/XMPPRoomHybrid.xcdatamodeld/.xccurrentversion create mode 100644 Extensions/XEP-0045/HybridStorage/XMPPRoomHybrid.xcdatamodeld/XMPPRoomHybrid.xcdatamodel/contents create mode 100644 Extensions/XEP-0045/HybridStorage/XMPPRoomHybridStorage.h create mode 100644 Extensions/XEP-0045/HybridStorage/XMPPRoomHybridStorage.m create mode 100644 Extensions/XEP-0045/HybridStorage/XMPPRoomHybridStorageProtected.h create mode 100644 Extensions/XEP-0045/HybridStorage/XMPPRoomMessageHybridCoreDataStorageObject.h create mode 100644 Extensions/XEP-0045/HybridStorage/XMPPRoomMessageHybridCoreDataStorageObject.m create mode 100644 Extensions/XEP-0045/HybridStorage/XMPPRoomOccupantHybridMemoryStorageObject.h create mode 100644 Extensions/XEP-0045/HybridStorage/XMPPRoomOccupantHybridMemoryStorageObject.m create mode 100644 Extensions/XEP-0045/MemoryStorage/XMPPRoomMemoryStorage.h create mode 100644 Extensions/XEP-0045/MemoryStorage/XMPPRoomMemoryStorage.m create mode 100644 Extensions/XEP-0045/MemoryStorage/XMPPRoomMessageMemoryStorageObject.h create mode 100644 Extensions/XEP-0045/MemoryStorage/XMPPRoomMessageMemoryStorageObject.m create mode 100644 Extensions/XEP-0045/MemoryStorage/XMPPRoomOccupantMemoryStorageObject.h create mode 100644 Extensions/XEP-0045/MemoryStorage/XMPPRoomOccupantMemoryStorageObject.m create mode 100644 Extensions/XEP-0045/XMPPMUC.h create mode 100644 Extensions/XEP-0045/XMPPMUC.m create mode 100644 Extensions/XEP-0045/XMPPMessage+XEP0045.h create mode 100644 Extensions/XEP-0045/XMPPMessage+XEP0045.m create mode 100644 Extensions/XEP-0045/XMPPRoom.h create mode 100644 Extensions/XEP-0045/XMPPRoom.m create mode 100644 Extensions/XEP-0045/XMPPRoomMessage.h create mode 100644 Extensions/XEP-0045/XMPPRoomOccupant.h create mode 100644 Extensions/XEP-0045/XMPPRoomPrivate.h create mode 100644 Extensions/XEP-0054/CoreDataStorage/XMPPvCard.xcdatamodeld/.xccurrentversion create mode 100644 Extensions/XEP-0054/CoreDataStorage/XMPPvCard.xcdatamodeld/XMPPvCard.xcdatamodel/elements create mode 100644 Extensions/XEP-0054/CoreDataStorage/XMPPvCard.xcdatamodeld/XMPPvCard.xcdatamodel/layout create mode 100644 Extensions/XEP-0054/CoreDataStorage/XMPPvCardAvatarCoreDataStorageObject.h create mode 100644 Extensions/XEP-0054/CoreDataStorage/XMPPvCardAvatarCoreDataStorageObject.m create mode 100644 Extensions/XEP-0054/CoreDataStorage/XMPPvCardCoreDataStorage.h create mode 100644 Extensions/XEP-0054/CoreDataStorage/XMPPvCardCoreDataStorage.m create mode 100644 Extensions/XEP-0054/CoreDataStorage/XMPPvCardCoreDataStorageObject.h create mode 100755 Extensions/XEP-0054/CoreDataStorage/XMPPvCardCoreDataStorageObject.m create mode 100644 Extensions/XEP-0054/CoreDataStorage/XMPPvCardTempCoreDataStorageObject.h create mode 100644 Extensions/XEP-0054/CoreDataStorage/XMPPvCardTempCoreDataStorageObject.m create mode 100644 Extensions/XEP-0054/XMPPvCardTemp.h create mode 100644 Extensions/XEP-0054/XMPPvCardTemp.m create mode 100644 Extensions/XEP-0054/XMPPvCardTempAdr.h create mode 100644 Extensions/XEP-0054/XMPPvCardTempAdr.m create mode 100644 Extensions/XEP-0054/XMPPvCardTempAdrTypes.h create mode 100644 Extensions/XEP-0054/XMPPvCardTempAdrTypes.m create mode 100644 Extensions/XEP-0054/XMPPvCardTempBase.h create mode 100644 Extensions/XEP-0054/XMPPvCardTempBase.m create mode 100644 Extensions/XEP-0054/XMPPvCardTempEmail.h create mode 100644 Extensions/XEP-0054/XMPPvCardTempEmail.m create mode 100644 Extensions/XEP-0054/XMPPvCardTempLabel.h create mode 100644 Extensions/XEP-0054/XMPPvCardTempLabel.m create mode 100755 Extensions/XEP-0054/XMPPvCardTempModule.h create mode 100755 Extensions/XEP-0054/XMPPvCardTempModule.m create mode 100644 Extensions/XEP-0054/XMPPvCardTempTel.h create mode 100644 Extensions/XEP-0054/XMPPvCardTempTel.m create mode 100644 Extensions/XEP-0059/NSXMLElement+XEP_0059.h create mode 100644 Extensions/XEP-0059/NSXMLElement+XEP_0059.m create mode 100644 Extensions/XEP-0059/XMPPResultSet.h create mode 100644 Extensions/XEP-0059/XMPPResultSet.m create mode 100644 Extensions/XEP-0060/XMPPIQ+XEP_0060.h create mode 100644 Extensions/XEP-0060/XMPPIQ+XEP_0060.m create mode 100644 Extensions/XEP-0060/XMPPPubSub.h create mode 100644 Extensions/XEP-0060/XMPPPubSub.m create mode 100644 Extensions/XEP-0065/TURNSocket.h create mode 100644 Extensions/XEP-0065/TURNSocket.m create mode 100644 Extensions/XEP-0066/XMPPIQ+XEP_0066.h create mode 100644 Extensions/XEP-0066/XMPPIQ+XEP_0066.m create mode 100644 Extensions/XEP-0066/XMPPMessage+XEP_0066.h create mode 100644 Extensions/XEP-0066/XMPPMessage+XEP_0066.m create mode 100644 Extensions/XEP-0077/XMPPRegistration.h create mode 100644 Extensions/XEP-0077/XMPPRegistration.m create mode 100644 Extensions/XEP-0082/NSDate+XMPPDateTimeProfiles.h create mode 100644 Extensions/XEP-0082/NSDate+XMPPDateTimeProfiles.m create mode 100644 Extensions/XEP-0082/XMPPDateTimeProfiles.h create mode 100644 Extensions/XEP-0082/XMPPDateTimeProfiles.m create mode 100644 Extensions/XEP-0085/XMPPMessage+XEP_0085.h create mode 100644 Extensions/XEP-0085/XMPPMessage+XEP_0085.m create mode 100644 Extensions/XEP-0092/XMPPSoftwareVersion.h create mode 100644 Extensions/XEP-0092/XMPPSoftwareVersion.m create mode 100644 Extensions/XEP-0100/XMPPTransports.h create mode 100644 Extensions/XEP-0100/XMPPTransports.m create mode 100644 Extensions/XEP-0106/NSString+XEP_0106.h create mode 100644 Extensions/XEP-0106/NSString+XEP_0106.m create mode 100644 Extensions/XEP-0115/CoreDataStorage/XMPPCapabilities.xcdatamodel/elements create mode 100644 Extensions/XEP-0115/CoreDataStorage/XMPPCapabilities.xcdatamodel/layout create mode 100644 Extensions/XEP-0115/CoreDataStorage/XMPPCapabilitiesCoreDataStorage.h create mode 100644 Extensions/XEP-0115/CoreDataStorage/XMPPCapabilitiesCoreDataStorage.m create mode 100644 Extensions/XEP-0115/CoreDataStorage/XMPPCapsCoreDataStorageObject.h create mode 100644 Extensions/XEP-0115/CoreDataStorage/XMPPCapsCoreDataStorageObject.m create mode 100644 Extensions/XEP-0115/CoreDataStorage/XMPPCapsResourceCoreDataStorageObject.h create mode 100644 Extensions/XEP-0115/CoreDataStorage/XMPPCapsResourceCoreDataStorageObject.m create mode 100644 Extensions/XEP-0115/XMPPCapabilities.h create mode 100644 Extensions/XEP-0115/XMPPCapabilities.m create mode 100644 Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving.xcdatamodeld/.xccurrentversion create mode 100644 Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving.xcdatamodeld/XMPPMessageArchiving.xcdatamodel/contents create mode 100644 Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchivingCoreDataStorage.h create mode 100644 Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchivingCoreDataStorage.m create mode 100644 Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving_Contact_CoreDataObject.h create mode 100644 Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving_Contact_CoreDataObject.m create mode 100644 Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving_Message_CoreDataObject.h create mode 100644 Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving_Message_CoreDataObject.m create mode 100644 Extensions/XEP-0136/XMPPMessageArchiving.h create mode 100644 Extensions/XEP-0136/XMPPMessageArchiving.m create mode 100644 Extensions/XEP-0147/XMPPURI.h create mode 100644 Extensions/XEP-0147/XMPPURI.m create mode 100644 Extensions/XEP-0153/XMPPvCardAvatarModule.h create mode 100755 Extensions/XEP-0153/XMPPvCardAvatarModule.m create mode 100644 Extensions/XEP-0172/XMPPMessage+XEP_0172.h create mode 100644 Extensions/XEP-0172/XMPPMessage+XEP_0172.m create mode 100644 Extensions/XEP-0172/XMPPPresence+XEP_0172.h create mode 100644 Extensions/XEP-0172/XMPPPresence+XEP_0172.m create mode 100644 Extensions/XEP-0184/XMPPMessage+XEP_0184.h create mode 100644 Extensions/XEP-0184/XMPPMessage+XEP_0184.m create mode 100644 Extensions/XEP-0184/XMPPMessageDeliveryReceipts.h create mode 100644 Extensions/XEP-0184/XMPPMessageDeliveryReceipts.m create mode 100644 Extensions/XEP-0191/XMPPBlocking.h create mode 100644 Extensions/XEP-0191/XMPPBlocking.m create mode 100644 Extensions/XEP-0198/Memory Storage/XMPPStreamManagementMemoryStorage.h create mode 100644 Extensions/XEP-0198/Memory Storage/XMPPStreamManagementMemoryStorage.m create mode 100644 Extensions/XEP-0198/Private/XMPPStreamManagementStanzas.h create mode 100644 Extensions/XEP-0198/Private/XMPPStreamManagementStanzas.m create mode 100644 Extensions/XEP-0198/XMPPStreamManagement.h create mode 100644 Extensions/XEP-0198/XMPPStreamManagement.m create mode 100644 Extensions/XEP-0199/XMPPAutoPing.h create mode 100644 Extensions/XEP-0199/XMPPAutoPing.m create mode 100644 Extensions/XEP-0199/XMPPPing.h create mode 100644 Extensions/XEP-0199/XMPPPing.m create mode 100644 Extensions/XEP-0202/XMPPAutoTime.h create mode 100644 Extensions/XEP-0202/XMPPAutoTime.m create mode 100644 Extensions/XEP-0202/XMPPTime.h create mode 100644 Extensions/XEP-0202/XMPPTime.m create mode 100644 Extensions/XEP-0203/NSXMLElement+XEP_0203.h create mode 100644 Extensions/XEP-0203/NSXMLElement+XEP_0203.m create mode 100644 Extensions/XEP-0223/XEP_0223.h create mode 100644 Extensions/XEP-0223/XEP_0223.m create mode 100644 Extensions/XEP-0224/XMPPAttentionModule.h create mode 100644 Extensions/XEP-0224/XMPPAttentionModule.m create mode 100644 Extensions/XEP-0224/XMPPMessage+XEP_0224.h create mode 100644 Extensions/XEP-0224/XMPPMessage+XEP_0224.m create mode 100644 Extensions/XEP-0280/XMPPMessage+XEP_0280.h create mode 100644 Extensions/XEP-0280/XMPPMessage+XEP_0280.m create mode 100644 Extensions/XEP-0280/XMPPMessageCarbons.h create mode 100644 Extensions/XEP-0280/XMPPMessageCarbons.m create mode 100644 Extensions/XEP-0297/NSXMLElement+XEP_0297.h create mode 100644 Extensions/XEP-0297/NSXMLElement+XEP_0297.m create mode 100644 Extensions/XEP-0308/XMPPMessage+XEP_0308.h create mode 100644 Extensions/XEP-0308/XMPPMessage+XEP_0308.m create mode 100644 Extensions/XEP-0333/XMPPMessage+XEP_0333.h create mode 100644 Extensions/XEP-0333/XMPPMessage+XEP_0333.m create mode 100644 Extensions/XEP-0335/NSXMLElement+XEP_0335.h create mode 100644 Extensions/XEP-0335/NSXMLElement+XEP_0335.m create mode 100644 Extensions/XEP-0352/NSXMLElement+XEP_0352.h create mode 100644 Extensions/XEP-0352/NSXMLElement+XEP_0352.m create mode 100644 Utilities/DDList.h create mode 100644 Utilities/DDList.m create mode 100644 Utilities/GCDMulticastDelegate.h create mode 100644 Utilities/GCDMulticastDelegate.m create mode 100644 Utilities/RFImageToDataTransformer.h create mode 100644 Utilities/RFImageToDataTransformer.m create mode 100644 Utilities/XMPPIDTracker.h create mode 100644 Utilities/XMPPIDTracker.m create mode 100644 Utilities/XMPPSRVResolver.h create mode 100644 Utilities/XMPPSRVResolver.m create mode 100644 Utilities/XMPPStringPrep.h create mode 100644 Utilities/XMPPStringPrep.m create mode 100644 Utilities/XMPPTimer.h create mode 100644 Utilities/XMPPTimer.m create mode 100644 Vendor/CocoaAsyncSocket/GCDAsyncSocket.h create mode 100644 Vendor/CocoaAsyncSocket/GCDAsyncSocket.m create mode 100755 Vendor/CocoaLumberjack/DDASLLogger.h create mode 100755 Vendor/CocoaLumberjack/DDASLLogger.m create mode 100755 Vendor/CocoaLumberjack/DDAbstractDatabaseLogger.h create mode 100755 Vendor/CocoaLumberjack/DDAbstractDatabaseLogger.m create mode 100755 Vendor/CocoaLumberjack/DDFileLogger.h create mode 100755 Vendor/CocoaLumberjack/DDFileLogger.m create mode 100755 Vendor/CocoaLumberjack/DDLog+LOGV.h create mode 100755 Vendor/CocoaLumberjack/DDLog.h create mode 100755 Vendor/CocoaLumberjack/DDLog.m rename {Example/Pods/CocoaLumberjack/Classes => Vendor/CocoaLumberjack}/DDTTYLogger.h (53%) mode change 100644 => 100755 create mode 100755 Vendor/CocoaLumberjack/DDTTYLogger.m create mode 100755 Vendor/CocoaLumberjack/Extensions/DDContextFilterLogFormatter.h create mode 100755 Vendor/CocoaLumberjack/Extensions/DDContextFilterLogFormatter.m create mode 100755 Vendor/CocoaLumberjack/Extensions/DDDispatchQueueLogFormatter.h create mode 100755 Vendor/CocoaLumberjack/Extensions/DDDispatchQueueLogFormatter.m create mode 100755 Vendor/CocoaLumberjack/Extensions/DDMultiFormatter.h create mode 100755 Vendor/CocoaLumberjack/Extensions/DDMultiFormatter.m create mode 100755 Vendor/CocoaLumberjack/Extensions/README.txt create mode 100644 Vendor/KissXML/Categories/NSString+DDXML.h create mode 100644 Vendor/KissXML/Categories/NSString+DDXML.m create mode 100644 Vendor/KissXML/DDXML.h create mode 100644 Vendor/KissXML/DDXMLDocument.h create mode 100644 Vendor/KissXML/DDXMLDocument.m create mode 100644 Vendor/KissXML/DDXMLElement.h create mode 100644 Vendor/KissXML/DDXMLElement.m create mode 100644 Vendor/KissXML/DDXMLNode.h create mode 100644 Vendor/KissXML/DDXMLNode.m create mode 100644 Vendor/KissXML/Private/DDXMLPrivate.h create mode 100644 Vendor/facebook-ios-sdk/.gitignore create mode 100644 Vendor/facebook-ios-sdk/.gitmodules create mode 100644 Vendor/facebook-ios-sdk/CONTRIBUTING.mdown create mode 100644 Vendor/facebook-ios-sdk/LICENSE create mode 100644 Vendor/facebook-ios-sdk/README create mode 100644 Vendor/facebook-ios-sdk/README.mdown create mode 100644 Vendor/facebook-ios-sdk/samples/FriendPickerSample/FriendPickerSample.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/samples/FriendPickerSample/FriendPickerSample/Default-568h@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/FriendPickerSample/FriendPickerSample/Default.png create mode 100644 Vendor/facebook-ios-sdk/samples/FriendPickerSample/FriendPickerSample/Default@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/FriendPickerSample/FriendPickerSample/FPAppDelegate.h create mode 100644 Vendor/facebook-ios-sdk/samples/FriendPickerSample/FriendPickerSample/FPAppDelegate.m create mode 100644 Vendor/facebook-ios-sdk/samples/FriendPickerSample/FriendPickerSample/FPViewController.h create mode 100644 Vendor/facebook-ios-sdk/samples/FriendPickerSample/FriendPickerSample/FPViewController.m create mode 100644 Vendor/facebook-ios-sdk/samples/FriendPickerSample/FriendPickerSample/FriendPickerSample-Info.plist create mode 100644 Vendor/facebook-ios-sdk/samples/FriendPickerSample/FriendPickerSample/FriendPickerSample-Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/samples/FriendPickerSample/FriendPickerSample/Icon-72.png create mode 100644 Vendor/facebook-ios-sdk/samples/FriendPickerSample/FriendPickerSample/Icon-72@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/FriendPickerSample/FriendPickerSample/Icon.png create mode 100644 Vendor/facebook-ios-sdk/samples/FriendPickerSample/FriendPickerSample/Icon@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/FriendPickerSample/FriendPickerSample/en.lproj/FPViewController_iPad.xib create mode 100644 Vendor/facebook-ios-sdk/samples/FriendPickerSample/FriendPickerSample/en.lproj/FPViewController_iPhone.xib create mode 100644 Vendor/facebook-ios-sdk/samples/FriendPickerSample/FriendPickerSample/en.lproj/InfoPlist.strings create mode 100644 Vendor/facebook-ios-sdk/samples/FriendPickerSample/FriendPickerSample/main.m create mode 100644 Vendor/facebook-ios-sdk/samples/FriendPickerSample/ReadMe.txt create mode 100644 Vendor/facebook-ios-sdk/samples/GraphApiSample/GraphApiSample.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/samples/GraphApiSample/GraphApiSample/Default-568h@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/GraphApiSample/GraphApiSample/Default.png create mode 100644 Vendor/facebook-ios-sdk/samples/GraphApiSample/GraphApiSample/Default@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/GraphApiSample/GraphApiSample/GraphApiAppDelegate.h create mode 100644 Vendor/facebook-ios-sdk/samples/GraphApiSample/GraphApiSample/GraphApiAppDelegate.m create mode 100644 Vendor/facebook-ios-sdk/samples/GraphApiSample/GraphApiSample/GraphApiSample-Info.plist create mode 100644 Vendor/facebook-ios-sdk/samples/GraphApiSample/GraphApiSample/GraphApiSample-Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/samples/GraphApiSample/GraphApiSample/GraphApiViewController.h create mode 100644 Vendor/facebook-ios-sdk/samples/GraphApiSample/GraphApiSample/GraphApiViewController.m create mode 100644 Vendor/facebook-ios-sdk/samples/GraphApiSample/GraphApiSample/Icon-72.png create mode 100644 Vendor/facebook-ios-sdk/samples/GraphApiSample/GraphApiSample/Icon-72@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/GraphApiSample/GraphApiSample/Icon.png create mode 100644 Vendor/facebook-ios-sdk/samples/GraphApiSample/GraphApiSample/Icon@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/GraphApiSample/GraphApiSample/en.lproj/GraphApiViewController_iPad.xib create mode 100644 Vendor/facebook-ios-sdk/samples/GraphApiSample/GraphApiSample/en.lproj/GraphApiViewController_iPhone.xib create mode 100644 Vendor/facebook-ios-sdk/samples/GraphApiSample/GraphApiSample/en.lproj/InfoPlist.strings create mode 100644 Vendor/facebook-ios-sdk/samples/GraphApiSample/GraphApiSample/main.m create mode 100644 Vendor/facebook-ios-sdk/samples/GraphApiSample/ReadMe.txt create mode 100644 Vendor/facebook-ios-sdk/samples/HelloFacebookSample/HelloFacebookSample.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/samples/HelloFacebookSample/HelloFacebookSample/Default-568h@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/HelloFacebookSample/HelloFacebookSample/Default.png create mode 100644 Vendor/facebook-ios-sdk/samples/HelloFacebookSample/HelloFacebookSample/Default@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/HelloFacebookSample/HelloFacebookSample/HFAppDelegate.h create mode 100644 Vendor/facebook-ios-sdk/samples/HelloFacebookSample/HelloFacebookSample/HFAppDelegate.m create mode 100644 Vendor/facebook-ios-sdk/samples/HelloFacebookSample/HelloFacebookSample/HFViewController.h create mode 100644 Vendor/facebook-ios-sdk/samples/HelloFacebookSample/HelloFacebookSample/HFViewController.m create mode 100644 Vendor/facebook-ios-sdk/samples/HelloFacebookSample/HelloFacebookSample/HelloFacebookSample-Info.plist create mode 100644 Vendor/facebook-ios-sdk/samples/HelloFacebookSample/HelloFacebookSample/HelloFacebookSample-Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/samples/HelloFacebookSample/HelloFacebookSample/Icon-72.png create mode 100644 Vendor/facebook-ios-sdk/samples/HelloFacebookSample/HelloFacebookSample/Icon-72@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/HelloFacebookSample/HelloFacebookSample/Icon.png create mode 100644 Vendor/facebook-ios-sdk/samples/HelloFacebookSample/HelloFacebookSample/Icon@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/HelloFacebookSample/HelloFacebookSample/en.lproj/HFViewController_iPad.xib create mode 100644 Vendor/facebook-ios-sdk/samples/HelloFacebookSample/HelloFacebookSample/en.lproj/HFViewController_iPhone.xib create mode 100644 Vendor/facebook-ios-sdk/samples/HelloFacebookSample/HelloFacebookSample/en.lproj/InfoPlist.strings create mode 100644 Vendor/facebook-ios-sdk/samples/HelloFacebookSample/HelloFacebookSample/main.m create mode 100644 Vendor/facebook-ios-sdk/samples/HelloFacebookSample/ReadMe.txt create mode 100644 Vendor/facebook-ios-sdk/samples/PlacePickerSample/PlacePickerSample.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/samples/PlacePickerSample/PlacePickerSample/Default-568h@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/PlacePickerSample/PlacePickerSample/Default.png create mode 100644 Vendor/facebook-ios-sdk/samples/PlacePickerSample/PlacePickerSample/Default@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/PlacePickerSample/PlacePickerSample/Icon-72.png create mode 100644 Vendor/facebook-ios-sdk/samples/PlacePickerSample/PlacePickerSample/Icon-72@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/PlacePickerSample/PlacePickerSample/Icon.png create mode 100644 Vendor/facebook-ios-sdk/samples/PlacePickerSample/PlacePickerSample/Icon@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/PlacePickerSample/PlacePickerSample/PPAppDelegate.h create mode 100644 Vendor/facebook-ios-sdk/samples/PlacePickerSample/PlacePickerSample/PPAppDelegate.m create mode 100644 Vendor/facebook-ios-sdk/samples/PlacePickerSample/PlacePickerSample/PPViewController.h create mode 100644 Vendor/facebook-ios-sdk/samples/PlacePickerSample/PlacePickerSample/PPViewController.m create mode 100644 Vendor/facebook-ios-sdk/samples/PlacePickerSample/PlacePickerSample/PlacePickerSample-Info.plist create mode 100644 Vendor/facebook-ios-sdk/samples/PlacePickerSample/PlacePickerSample/PlacePickerSample-Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/samples/PlacePickerSample/PlacePickerSample/en.lproj/InfoPlist.strings create mode 100644 Vendor/facebook-ios-sdk/samples/PlacePickerSample/PlacePickerSample/en.lproj/PPViewController_iPad.xib create mode 100644 Vendor/facebook-ios-sdk/samples/PlacePickerSample/PlacePickerSample/en.lproj/PPViewController_iPhone.xib create mode 100644 Vendor/facebook-ios-sdk/samples/PlacePickerSample/PlacePickerSample/main.m create mode 100644 Vendor/facebook-ios-sdk/samples/PlacePickerSample/ReadMe.txt create mode 100644 Vendor/facebook-ios-sdk/samples/ProfilePictureSample/ProfilePictureSample.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/samples/ProfilePictureSample/ProfilePictureSample/Default-568h@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/ProfilePictureSample/ProfilePictureSample/Default.png create mode 100644 Vendor/facebook-ios-sdk/samples/ProfilePictureSample/ProfilePictureSample/Default@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/ProfilePictureSample/ProfilePictureSample/Icon-72.png create mode 100644 Vendor/facebook-ios-sdk/samples/ProfilePictureSample/ProfilePictureSample/Icon-72@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/ProfilePictureSample/ProfilePictureSample/Icon.png create mode 100644 Vendor/facebook-ios-sdk/samples/ProfilePictureSample/ProfilePictureSample/Icon@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/ProfilePictureSample/ProfilePictureSample/PPAppDelegate.h create mode 100644 Vendor/facebook-ios-sdk/samples/ProfilePictureSample/ProfilePictureSample/PPAppDelegate.m create mode 100644 Vendor/facebook-ios-sdk/samples/ProfilePictureSample/ProfilePictureSample/PPViewController.h create mode 100644 Vendor/facebook-ios-sdk/samples/ProfilePictureSample/ProfilePictureSample/PPViewController.m create mode 100644 Vendor/facebook-ios-sdk/samples/ProfilePictureSample/ProfilePictureSample/ProfilePictureSample-Info.plist create mode 100644 Vendor/facebook-ios-sdk/samples/ProfilePictureSample/ProfilePictureSample/ProfilePictureSample-Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/samples/ProfilePictureSample/ProfilePictureSample/en.lproj/InfoPlist.strings create mode 100644 Vendor/facebook-ios-sdk/samples/ProfilePictureSample/ProfilePictureSample/en.lproj/PPViewController_iPad.xib create mode 100644 Vendor/facebook-ios-sdk/samples/ProfilePictureSample/ProfilePictureSample/en.lproj/PPViewController_iPhone.xib create mode 100644 Vendor/facebook-ios-sdk/samples/ProfilePictureSample/ProfilePictureSample/main.m create mode 100644 Vendor/facebook-ios-sdk/samples/ProfilePictureSample/ReadMe.txt create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/Default-568h@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/Default.png create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/Default@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/Icon-72.png create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/Icon-72@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/Icon.png create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/Icon@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/IconFacebook-72@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/OGProtocols.h create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/RPSAppDelegate.h create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/RPSAppDelegate.m create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/RPSCommonObjects.m create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/RPSFriendsViewController.h create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/RPSFriendsViewController.m create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/RPSGameViewController.h create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/RPSGameViewController.m create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/RPSSample-Info.plist create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/RPSSample-Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/en.lproj/InfoPlist.strings create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/en.lproj/RPSFriendsViewController_iPad.xib create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/en.lproj/RPSFriendsViewController_iPhone.xib create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/en.lproj/RPSGameViewController_iPad.xib create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/en.lproj/RPSGameViewController_iPhone.xib create mode 100755 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/left-paper-128.png create mode 100755 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/left-paper-88.png create mode 100755 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/left-rock-128.png create mode 100755 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/left-rock-88.png create mode 100755 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/left-scissors-128.png create mode 100755 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/left-scissors-88.png create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/main.m create mode 100755 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/right-paper-128.png create mode 100755 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/right-paper-88.png create mode 100755 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/right-rock-128.png create mode 100755 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/right-rock-88.png create mode 100755 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/right-scissors-128.png create mode 100755 Vendor/facebook-ios-sdk/samples/RPSSample/RPSSample/right-scissors-88.png create mode 100644 Vendor/facebook-ios-sdk/samples/RPSSample/ReadMe.txt create mode 100755 Vendor/facebook-ios-sdk/samples/RPSSample/post_app_objects.sh create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/Icon-72.png create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/Icon.png create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/Icon@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/ReadMe.txt create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/Scrumptious.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/Default-568h@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/Default.png create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/Default@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/FacebookSDKOverrides.bundle/FacebookSDKImages/FBLoginViewButton.png create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/FacebookSDKOverrides.bundle/FacebookSDKImages/FBLoginViewButton@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/FacebookSDKOverrides.bundle/en.lproj/Localizable.strings create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/FacebookSDKOverrides.bundle/he.lproj/Localizable.strings create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/SCAppDelegate.h create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/SCAppDelegate.m create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/SCLoginViewController.h create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/SCLoginViewController.m create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/SCLoginViewController.xib create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/SCPhotoViewController.h create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/SCPhotoViewController.m create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/SCPhotoViewController.xib create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/SCProtocols.h create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/SCViewController.h create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/SCViewController.m create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/en.lproj/InfoPlist.strings create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/en.lproj/SCViewController.xib create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/en.lproj/Scrumptious.strings create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/he.lproj/Scrumptious.strings create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/images/action-eating.png create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/images/action-location.png create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/images/action-people.png create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/images/action-photo.png create mode 100755 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/images/facebook.png create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/main.m create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/scrumptious-Info.plist create mode 100644 Vendor/facebook-ios-sdk/samples/Scrumptious/scrumptious/scrumptious-Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/samples/SessionLoginSample/ReadMe.txt create mode 100644 Vendor/facebook-ios-sdk/samples/SessionLoginSample/SessionLoginSample.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/samples/SessionLoginSample/SessionLoginSample/Default-568h@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/SessionLoginSample/SessionLoginSample/Default.png create mode 100644 Vendor/facebook-ios-sdk/samples/SessionLoginSample/SessionLoginSample/Default@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/SessionLoginSample/SessionLoginSample/Icon-72.png create mode 100644 Vendor/facebook-ios-sdk/samples/SessionLoginSample/SessionLoginSample/Icon-72@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/SessionLoginSample/SessionLoginSample/Icon.png create mode 100644 Vendor/facebook-ios-sdk/samples/SessionLoginSample/SessionLoginSample/Icon@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/SessionLoginSample/SessionLoginSample/SLAppDelegate.h create mode 100644 Vendor/facebook-ios-sdk/samples/SessionLoginSample/SessionLoginSample/SLAppDelegate.m create mode 100644 Vendor/facebook-ios-sdk/samples/SessionLoginSample/SessionLoginSample/SLViewController.h create mode 100644 Vendor/facebook-ios-sdk/samples/SessionLoginSample/SessionLoginSample/SLViewController.m create mode 100644 Vendor/facebook-ios-sdk/samples/SessionLoginSample/SessionLoginSample/SessionLoginSample-Info.plist create mode 100644 Vendor/facebook-ios-sdk/samples/SessionLoginSample/SessionLoginSample/SessionLoginSample-Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/samples/SessionLoginSample/SessionLoginSample/en.lproj/InfoPlist.strings create mode 100644 Vendor/facebook-ios-sdk/samples/SessionLoginSample/SessionLoginSample/en.lproj/SLViewController_iPad.xib create mode 100644 Vendor/facebook-ios-sdk/samples/SessionLoginSample/SessionLoginSample/en.lproj/SLViewController_iPhone.xib create mode 100644 Vendor/facebook-ios-sdk/samples/SessionLoginSample/SessionLoginSample/main.m create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/ReadMe.txt create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/Default-568h@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/Default.png create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/Default@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/Icon-72.png create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/Icon-72@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/Icon.png create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/Icon@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/SUAppDelegate.h create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/SUAppDelegate.m create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/SUProfileTableViewCell.h create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/SUProfileTableViewCell.m create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/SUSettingsViewController.h create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/SUSettingsViewController.m create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/SUSettingsViewController_iPad.xib create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/SUSettingsViewController_iPhone.xib create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/SUUserManager.h create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/SUUserManager.m create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/SUUsingViewController.h create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/SUUsingViewController.m create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/SUUsingViewController_iPad.xib create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/SUUsingViewController_iPhone.xib create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/SwitchUserSample-Info.plist create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/SwitchUserSample-Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/en.lproj/InfoPlist.strings create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/first.png create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/first@2x.png create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/main.m create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/second.png create mode 100644 Vendor/facebook-ios-sdk/samples/SwitchUserSample/SwitchUserSample/second@2x.png create mode 100755 Vendor/facebook-ios-sdk/scripts/build_all.sh create mode 100755 Vendor/facebook-ios-sdk/scripts/build_distribution.sh create mode 100755 Vendor/facebook-ios-sdk/scripts/build_documentation.sh create mode 100755 Vendor/facebook-ios-sdk/scripts/build_framework.sh create mode 100755 Vendor/facebook-ios-sdk/scripts/build_samples.sh create mode 100755 Vendor/facebook-ios-sdk/scripts/common.sh create mode 100755 Vendor/facebook-ios-sdk/scripts/configure_simulator_for_unit_tests.sh create mode 100755 Vendor/facebook-ios-sdk/scripts/image_to_code.py create mode 100755 Vendor/facebook-ios-sdk/scripts/label_version_number.sh create mode 100755 Vendor/facebook-ios-sdk/scripts/rm_test_users.py create mode 100755 Vendor/facebook-ios-sdk/scripts/run_tests.sh create mode 100644 Vendor/facebook-ios-sdk/src/Base64/FBBase64.h create mode 100644 Vendor/facebook-ios-sdk/src/Base64/FBBase64.m create mode 100644 Vendor/facebook-ios-sdk/src/Cryptography/FBCrypto.h create mode 100644 Vendor/facebook-ios-sdk/src/Cryptography/FBCrypto.m create mode 100644 Vendor/facebook-ios-sdk/src/FBAccessTokenData+Internal.h create mode 100644 Vendor/facebook-ios-sdk/src/FBAccessTokenData.h create mode 100644 Vendor/facebook-ios-sdk/src/FBAccessTokenData.m create mode 100644 Vendor/facebook-ios-sdk/src/FBAppBridge.h create mode 100644 Vendor/facebook-ios-sdk/src/FBAppBridge.m create mode 100644 Vendor/facebook-ios-sdk/src/FBAppBridgeTypeToJSONConverter.h create mode 100644 Vendor/facebook-ios-sdk/src/FBAppBridgeTypeToJSONConverter.m create mode 100644 Vendor/facebook-ios-sdk/src/FBAppCall+Internal.h create mode 100644 Vendor/facebook-ios-sdk/src/FBAppCall.h create mode 100644 Vendor/facebook-ios-sdk/src/FBAppCall.m create mode 100644 Vendor/facebook-ios-sdk/src/FBAppEvents+Internal.h create mode 100644 Vendor/facebook-ios-sdk/src/FBAppEvents.h create mode 100644 Vendor/facebook-ios-sdk/src/FBAppEvents.m create mode 100644 Vendor/facebook-ios-sdk/src/FBAppLinkData+Internal.h create mode 100644 Vendor/facebook-ios-sdk/src/FBAppLinkData.h create mode 100644 Vendor/facebook-ios-sdk/src/FBAppLinkData.m create mode 100644 Vendor/facebook-ios-sdk/src/FBCacheDescriptor.h create mode 100644 Vendor/facebook-ios-sdk/src/FBCacheDescriptor.m create mode 100644 Vendor/facebook-ios-sdk/src/FBCacheIndex.h create mode 100644 Vendor/facebook-ios-sdk/src/FBCacheIndex.m create mode 100644 Vendor/facebook-ios-sdk/src/FBConnect.h create mode 100644 Vendor/facebook-ios-sdk/src/FBDataDiskCache.h create mode 100644 Vendor/facebook-ios-sdk/src/FBDataDiskCache.m create mode 100644 Vendor/facebook-ios-sdk/src/FBDialog.h create mode 100644 Vendor/facebook-ios-sdk/src/FBDialog.m create mode 100644 Vendor/facebook-ios-sdk/src/FBDialogs+Internal.h create mode 100644 Vendor/facebook-ios-sdk/src/FBDialogs.h create mode 100644 Vendor/facebook-ios-sdk/src/FBDialogs.m create mode 100644 Vendor/facebook-ios-sdk/src/FBDialogsData+Internal.h create mode 100644 Vendor/facebook-ios-sdk/src/FBDialogsData.h create mode 100644 Vendor/facebook-ios-sdk/src/FBDialogsData.m create mode 100644 Vendor/facebook-ios-sdk/src/FBDialogsParams+Internal.h create mode 100644 Vendor/facebook-ios-sdk/src/FBDialogsParams.h create mode 100644 Vendor/facebook-ios-sdk/src/FBDialogsParams.m create mode 100644 Vendor/facebook-ios-sdk/src/FBDynamicFrameworkLoader.h create mode 100644 Vendor/facebook-ios-sdk/src/FBDynamicFrameworkLoader.m create mode 100644 Vendor/facebook-ios-sdk/src/FBError.h create mode 100644 Vendor/facebook-ios-sdk/src/FBError.m create mode 100644 Vendor/facebook-ios-sdk/src/FBErrorUtility+Internal.h create mode 100644 Vendor/facebook-ios-sdk/src/FBErrorUtility.h create mode 100644 Vendor/facebook-ios-sdk/src/FBErrorUtility.m create mode 100644 Vendor/facebook-ios-sdk/src/FBFetchedAppSettings.h create mode 100644 Vendor/facebook-ios-sdk/src/FBFetchedAppSettings.m create mode 100644 Vendor/facebook-ios-sdk/src/FBFrictionlessDialogSupportDelegate.h create mode 100644 Vendor/facebook-ios-sdk/src/FBFrictionlessRecipientCache.h create mode 100644 Vendor/facebook-ios-sdk/src/FBFrictionlessRecipientCache.m create mode 100644 Vendor/facebook-ios-sdk/src/FBFrictionlessRequestSettings.h create mode 100644 Vendor/facebook-ios-sdk/src/FBFrictionlessRequestSettings.m create mode 100644 Vendor/facebook-ios-sdk/src/FBFriendPickerCacheDescriptor.h create mode 100644 Vendor/facebook-ios-sdk/src/FBFriendPickerCacheDescriptor.m create mode 100644 Vendor/facebook-ios-sdk/src/FBFriendPickerViewController+Internal.h create mode 100644 Vendor/facebook-ios-sdk/src/FBFriendPickerViewController.h create mode 100644 Vendor/facebook-ios-sdk/src/FBFriendPickerViewController.m create mode 100644 Vendor/facebook-ios-sdk/src/FBGraphLocation.h create mode 100644 Vendor/facebook-ios-sdk/src/FBGraphObject.h create mode 100644 Vendor/facebook-ios-sdk/src/FBGraphObject.m create mode 100644 Vendor/facebook-ios-sdk/src/FBGraphObjectPagingLoader.h create mode 100644 Vendor/facebook-ios-sdk/src/FBGraphObjectPagingLoader.m create mode 100644 Vendor/facebook-ios-sdk/src/FBGraphObjectTableCell.h create mode 100644 Vendor/facebook-ios-sdk/src/FBGraphObjectTableCell.m create mode 100644 Vendor/facebook-ios-sdk/src/FBGraphObjectTableDataSource.h create mode 100644 Vendor/facebook-ios-sdk/src/FBGraphObjectTableDataSource.m create mode 100644 Vendor/facebook-ios-sdk/src/FBGraphObjectTableSelection.h create mode 100644 Vendor/facebook-ios-sdk/src/FBGraphObjectTableSelection.m create mode 100644 Vendor/facebook-ios-sdk/src/FBGraphPlace.h create mode 100644 Vendor/facebook-ios-sdk/src/FBGraphUser.h create mode 100644 Vendor/facebook-ios-sdk/src/FBImageResourceLoader.h create mode 100644 Vendor/facebook-ios-sdk/src/FBImageResourceLoader.m create mode 100644 Vendor/facebook-ios-sdk/src/FBInsights.h create mode 100644 Vendor/facebook-ios-sdk/src/FBInsights.m create mode 100644 Vendor/facebook-ios-sdk/src/FBLogger.h create mode 100644 Vendor/facebook-ios-sdk/src/FBLogger.m create mode 100644 Vendor/facebook-ios-sdk/src/FBLoginDialog.h create mode 100644 Vendor/facebook-ios-sdk/src/FBLoginDialog.m create mode 100644 Vendor/facebook-ios-sdk/src/FBLoginDialogParams.h create mode 100644 Vendor/facebook-ios-sdk/src/FBLoginDialogParams.m create mode 100644 Vendor/facebook-ios-sdk/src/FBLoginView.h create mode 100644 Vendor/facebook-ios-sdk/src/FBLoginView.m create mode 100644 Vendor/facebook-ios-sdk/src/FBNativeDialogs.h create mode 100644 Vendor/facebook-ios-sdk/src/FBNativeDialogs.m create mode 100644 Vendor/facebook-ios-sdk/src/FBOpenGraphAction.h create mode 100644 Vendor/facebook-ios-sdk/src/FBOpenGraphActionShareDialogParams.h create mode 100644 Vendor/facebook-ios-sdk/src/FBOpenGraphActionShareDialogParams.m create mode 100644 Vendor/facebook-ios-sdk/src/FBOpenGraphObject.h create mode 100644 Vendor/facebook-ios-sdk/src/FBPlacePickerCacheDescriptor.h create mode 100644 Vendor/facebook-ios-sdk/src/FBPlacePickerCacheDescriptor.m create mode 100644 Vendor/facebook-ios-sdk/src/FBPlacePickerViewController+Internal.h create mode 100644 Vendor/facebook-ios-sdk/src/FBPlacePickerViewController.h create mode 100644 Vendor/facebook-ios-sdk/src/FBPlacePickerViewController.m create mode 100644 Vendor/facebook-ios-sdk/src/FBProfilePictureView.h create mode 100644 Vendor/facebook-ios-sdk/src/FBProfilePictureView.m create mode 100644 Vendor/facebook-ios-sdk/src/FBRequest+Internal.h create mode 100644 Vendor/facebook-ios-sdk/src/FBRequest.h create mode 100644 Vendor/facebook-ios-sdk/src/FBRequest.m create mode 100644 Vendor/facebook-ios-sdk/src/FBRequestBody.h create mode 100644 Vendor/facebook-ios-sdk/src/FBRequestBody.m create mode 100644 Vendor/facebook-ios-sdk/src/FBRequestConnection+Internal.h create mode 100644 Vendor/facebook-ios-sdk/src/FBRequestConnection.h create mode 100644 Vendor/facebook-ios-sdk/src/FBRequestConnection.m create mode 100644 Vendor/facebook-ios-sdk/src/FBRequestConnectionRetryManager.h create mode 100644 Vendor/facebook-ios-sdk/src/FBRequestConnectionRetryManager.m create mode 100644 Vendor/facebook-ios-sdk/src/FBRequestHandlerFactory.h create mode 100644 Vendor/facebook-ios-sdk/src/FBRequestHandlerFactory.m create mode 100644 Vendor/facebook-ios-sdk/src/FBRequestMetadata.h create mode 100644 Vendor/facebook-ios-sdk/src/FBRequestMetadata.m create mode 100644 Vendor/facebook-ios-sdk/src/FBSDKVersion.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSession+FBSessionLoginStrategy.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSession+Internal.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSession+Protected.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSession.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSession.m create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionAppEventsState.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionAppEventsState.m create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionAppSwitchingLoginStategy.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionAppSwitchingLoginStategy.m create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionAuthLogger.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionAuthLogger.m create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionFacebookAppNativeLoginStategy.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionFacebookAppNativeLoginStategy.m create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionFacebookAppWebLoginStategy.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionFacebookAppWebLoginStategy.m create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionInlineWebViewLoginStategy.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionInlineWebViewLoginStategy.m create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionLoginStrategy.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionLoginStrategyParams.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionLoginStrategyParams.m create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionManualTokenCachingStrategy.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionManualTokenCachingStrategy.m create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionSafariLoginStategy.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionSafariLoginStategy.m create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionSystemLoginStategy.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionSystemLoginStategy.m create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionTokenCachingStrategy.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionTokenCachingStrategy.m create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionUtility.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSessionUtility.m create mode 100644 Vendor/facebook-ios-sdk/src/FBSettings+Internal.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSettings.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSettings.m create mode 100644 Vendor/facebook-ios-sdk/src/FBShareDialogParams.h create mode 100644 Vendor/facebook-ios-sdk/src/FBShareDialogParams.m create mode 100644 Vendor/facebook-ios-sdk/src/FBSystemAccountStoreAdapter.h create mode 100644 Vendor/facebook-ios-sdk/src/FBSystemAccountStoreAdapter.m create mode 100644 Vendor/facebook-ios-sdk/src/FBTask+Private.h create mode 100644 Vendor/facebook-ios-sdk/src/FBTask.h create mode 100644 Vendor/facebook-ios-sdk/src/FBTask.m create mode 100644 Vendor/facebook-ios-sdk/src/FBTaskCompletionSource.h create mode 100644 Vendor/facebook-ios-sdk/src/FBTaskCompletionSource.m create mode 100644 Vendor/facebook-ios-sdk/src/FBTestSession+Internal.h create mode 100644 Vendor/facebook-ios-sdk/src/FBTestSession.h create mode 100644 Vendor/facebook-ios-sdk/src/FBTestSession.m create mode 100644 Vendor/facebook-ios-sdk/src/FBURLConnection.h create mode 100644 Vendor/facebook-ios-sdk/src/FBURLConnection.m create mode 100644 Vendor/facebook-ios-sdk/src/FBUserSettingsViewController.h create mode 100644 Vendor/facebook-ios-sdk/src/FBUserSettingsViewController.m create mode 100644 Vendor/facebook-ios-sdk/src/FBUserSettingsViewResources.bundle/Contents/Resources/en.lproj/Localizable.strings create mode 100644 Vendor/facebook-ios-sdk/src/FBUserSettingsViewResources.bundle/Contents/Resources/he.lproj/Localizable.strings create mode 100644 Vendor/facebook-ios-sdk/src/FBUserSettingsViewResources.bundle/images/facebook-logo.png create mode 100644 Vendor/facebook-ios-sdk/src/FBUserSettingsViewResources.bundle/images/facebook-logo@2x.png create mode 100644 Vendor/facebook-ios-sdk/src/FBUserSettingsViewResources.bundle/images/loginBackgroundIPadLandscape.jpg create mode 100644 Vendor/facebook-ios-sdk/src/FBUserSettingsViewResources.bundle/images/loginBackgroundIPadLandscape@2x.jpg create mode 100644 Vendor/facebook-ios-sdk/src/FBUserSettingsViewResources.bundle/images/loginBackgroundIPadPortrait.jpg create mode 100644 Vendor/facebook-ios-sdk/src/FBUserSettingsViewResources.bundle/images/loginBackgroundIPadPortrait@2x.jpg create mode 100644 Vendor/facebook-ios-sdk/src/FBUserSettingsViewResources.bundle/images/loginBackgroundIPhonePortrait.jpg create mode 100644 Vendor/facebook-ios-sdk/src/FBUserSettingsViewResources.bundle/images/loginBackgroundIPhonePortrait@2x.jpg create mode 100644 Vendor/facebook-ios-sdk/src/FBUserSettingsViewResources.bundle/images/silver-button-normal.png create mode 100644 Vendor/facebook-ios-sdk/src/FBUserSettingsViewResources.bundle/images/silver-button-normal@2x.png create mode 100644 Vendor/facebook-ios-sdk/src/FBUserSettingsViewResources.bundle/images/silver-button-pressed.png create mode 100644 Vendor/facebook-ios-sdk/src/FBUserSettingsViewResources.bundle/images/silver-button-pressed@2x.png create mode 100644 Vendor/facebook-ios-sdk/src/FBUtility.h create mode 100644 Vendor/facebook-ios-sdk/src/FBUtility.m create mode 100644 Vendor/facebook-ios-sdk/src/FBViewController+Internal.h create mode 100644 Vendor/facebook-ios-sdk/src/FBViewController.h create mode 100644 Vendor/facebook-ios-sdk/src/FBViewController.m create mode 100644 Vendor/facebook-ios-sdk/src/FBWebDialogs.h create mode 100644 Vendor/facebook-ios-sdk/src/FBWebDialogs.m create mode 100644 Vendor/facebook-ios-sdk/src/Facebook.h create mode 100644 Vendor/facebook-ios-sdk/src/Facebook.m create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDK.h create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBAccessTokenDataTests.h create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBAccessTokenDataTests.m create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBAppEventsIntegrationTests.h create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBAppEventsIntegrationTests.m create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBBatchRequestIntegrationTests.h create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBBatchRequestIntegrationTests.m create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBCacheIntegrationTests.h create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBCacheIntegrationTests.m create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBIntegrationTests.h create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBIntegrationTests.m create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBOpenGraphActionTests.h create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBOpenGraphActionTests.m create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBRequestConnectionIntegrationTests.h create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBRequestConnectionIntegrationTests.m create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBRequestIntegrationTests.h create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBRequestIntegrationTests.m create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBSessionIntegrationTests.h create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBSessionIntegrationTests.m create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBTestSessionTests.h create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FBTestSessionTests.m create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FacebookSDKIntegrationTests-Info.plist create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/FacebookSDKIntegrationTests-Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKIntegrationTests/en.lproj/InfoPlist.strings create mode 100644 Vendor/facebook-ios-sdk/src/FacebookSDKResources.bundle.README create mode 100644 Vendor/facebook-ios-sdk/src/Framework/Resources/Info.plist create mode 100644 Vendor/facebook-ios-sdk/src/ImageResources/FBDialog/FBDialogClose.png create mode 100644 Vendor/facebook-ios-sdk/src/ImageResources/FBDialog/FBDialogClose@2x.png create mode 100755 Vendor/facebook-ios-sdk/src/ImageResources/FBFriendPickerView/FBFriendPickerViewDefault.png create mode 100644 Vendor/facebook-ios-sdk/src/ImageResources/FBLoginView/FBLoginViewButton.png create mode 100644 Vendor/facebook-ios-sdk/src/ImageResources/FBLoginView/FBLoginViewButton@2x.png create mode 100644 Vendor/facebook-ios-sdk/src/ImageResources/FBLoginView/FBLoginViewButtonPressed.png create mode 100644 Vendor/facebook-ios-sdk/src/ImageResources/FBLoginView/FBLoginViewButtonPressed@2x.png create mode 100755 Vendor/facebook-ios-sdk/src/ImageResources/FBPlacePickerView/FBPlacePickerViewGenericPlace.png create mode 100644 Vendor/facebook-ios-sdk/src/ImageResources/FBProfilePictureView/FBProfilePictureViewBlankProfilePortrait.png create mode 100644 Vendor/facebook-ios-sdk/src/ImageResources/FBProfilePictureView/FBProfilePictureViewBlankProfileSquare.png create mode 100644 Vendor/facebook-ios-sdk/src/NSError+FBError.h create mode 100644 Vendor/facebook-ios-sdk/src/NSError+FBError.m create mode 100644 Vendor/facebook-ios-sdk/src/Package/FacebookSDK.pmdoc/01package-contents.xml create mode 100644 Vendor/facebook-ios-sdk/src/Package/FacebookSDK.pmdoc/01package.xml create mode 100644 Vendor/facebook-ios-sdk/src/Package/FacebookSDK.pmdoc/index.xml create mode 100644 Vendor/facebook-ios-sdk/src/facebook-ios-sdk.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/src/facebook-ios-sdk.xcodeproj/xcshareddata/xcschemes/FacebookSDKIntegrationTests.xcscheme create mode 100644 Vendor/facebook-ios-sdk/src/facebook-ios-sdk.xcodeproj/xcshareddata/xcschemes/facebook-ios-sdk-tests.xcscheme create mode 100644 Vendor/facebook-ios-sdk/src/facebook-ios-sdk.xcodeproj/xcshareddata/xcschemes/facebook-ios-sdk.xcscheme create mode 100644 Vendor/facebook-ios-sdk/src/facebook_ios_sdk_Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBAppBridgeTests.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBAppBridgeTests.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBAppCallTests.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBAppCallTests.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBAppLinkDataTests.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBAppLinkDataTests.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBAuthenticationTests.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBAuthenticationTests.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBBatchRequestTests.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBBatchRequestTests.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBCacheTests.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBCacheTests.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBFacebookAppAuthenticationTests.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBFacebookAppAuthenticationTests.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBGraphObjectTests.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBGraphObjectTests.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBIsStringRepresentingJSONDictionary.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBIsStringRepresentingJSONDictionary.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBIsURLHavingQueryParams.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBIsURLHavingQueryParams.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBLoginDialogAuthenticationTests.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBLoginDialogAuthenticationTests.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBRequestConnectionTests.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBRequestConnectionTests.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBRequestTests.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBRequestTests.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBSafariAuthenticationTests.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBSafariAuthenticationTests.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBSessionTests.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBSessionTests.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBSettingsTests.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBSettingsTests.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBSystemAccountAuthenticationTests.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBSystemAccountAuthenticationTests.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBTestBlocker.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBTestBlocker.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBTests.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBTests.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBURLConnectionTests.h create mode 100644 Vendor/facebook-ios-sdk/src/tests/FBURLConnectionTests.m create mode 100644 Vendor/facebook-ios-sdk/src/tests/FacebookSDKTests-Info.plist create mode 100644 Vendor/facebook-ios-sdk/src/tests/FacebookSDKTests-Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/src/tests/FacebookSDKTests.xcconfig create mode 100644 Vendor/facebook-ios-sdk/src/tests/en.lproj/InfoPlist.strings create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/.gitignore create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/.gitmodules create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/CHANGES.txt create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Documentation/Doxyfile create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Documentation/Makefile create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Documentation/README.txt create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Examples/CustomDateMatcher/CustomDateMatcher.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Examples/CustomDateMatcher/Example-Info.plist create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Examples/CustomDateMatcher/IsGivenDayOfWeek.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Examples/CustomDateMatcher/IsGivenDayOfWeek.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Examples/CustomDateMatcher/SampleTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Examples/MacExample/Example-Info.plist create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Examples/MacExample/Example.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Examples/MacExample/MacExample.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Examples/iOSExample/Example-Info.plist create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Examples/iOSExample/Example.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Examples/iOSExample/iOSExample.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/LICENSE.txt create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/README.md create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/HCAssertThat.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/HCAssertThat.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/HCBaseDescription.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/HCBaseDescription.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/HCBaseMatcher.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/HCBaseMatcher.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/HCDescription.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/HCMatcher.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/HCSelfDescribing.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/HCStringDescription.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/HCStringDescription.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/Helpers/HCCollectMatchers.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/Helpers/HCCollectMatchers.m create mode 100755 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/Helpers/HCInvocationMatcher.h create mode 100755 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/Helpers/HCInvocationMatcher.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/Helpers/HCRequireNonNilObject.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/Helpers/HCRequireNonNilObject.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/Helpers/HCRequireNonNilString.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/Helpers/HCRequireNonNilString.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/Helpers/HCWrapInMatcher.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Core/Helpers/HCWrapInMatcher.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/CustomMatchers.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCHasCount.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCHasCount.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContaining.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContaining.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInAnyOrder.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInAnyOrder.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInOrder.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInOrder.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionOnlyContaining.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionOnlyContaining.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContaining.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContaining.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingEntries.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingEntries.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingKey.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingKey.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingValue.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingValue.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsEmptyCollection.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsEmptyCollection.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsIn.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Collection/HCIsIn.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Decorator/HCDescribedAs.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Decorator/HCDescribedAs.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Decorator/HCIs.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Decorator/HCIs.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Logical/HCAllOf.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Logical/HCAllOf.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Logical/HCAnyOf.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Logical/HCAnyOf.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Logical/HCIsAnything.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Logical/HCIsAnything.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Logical/HCIsNot.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Logical/HCIsNot.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Number/HCBoxNumber.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Number/HCIsCloseTo.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Number/HCIsCloseTo.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Number/HCIsEqualToNumber.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Number/HCIsEqualToNumber.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Number/HCNumberAssert.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Number/HCNumberAssert.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Number/HCOrderingComparison.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Number/HCOrderingComparison.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Object/HCConformsToProtocol.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Object/HCConformsToProtocol.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Object/HCHasDescription.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Object/HCHasDescription.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Object/HCHasProperty.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Object/HCHasProperty.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Object/HCIsEqual.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Object/HCIsEqual.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Object/HCIsInstanceOf.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Object/HCIsInstanceOf.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Object/HCIsNil.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Object/HCIsNil.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Object/HCIsSame.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Object/HCIsSame.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Text/HCIsEqualIgnoringCase.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Text/HCIsEqualIgnoringCase.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Text/HCIsEqualIgnoringWhiteSpace.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Text/HCIsEqualIgnoringWhiteSpace.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Text/HCStringContains.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Text/HCStringContains.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Text/HCStringContainsInOrder.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Text/HCStringContainsInOrder.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Text/HCStringEndsWith.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Text/HCStringEndsWith.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Text/HCStringStartsWith.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Text/HCStringStartsWith.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Text/HCSubstringMatcher.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Library/Text/HCSubstringMatcher.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/MainPage.h create mode 100755 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/MakeDistribution.sh create mode 100755 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/MakeDocumentation.sh create mode 100755 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/MakeIOSFramework.sh create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/OCHamcrest-Info.plist create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/OCHamcrest-Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/OCHamcrest.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/OCHamcrest.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/OCHamcrest.xcodeproj/xcshareddata/xcschemes/OCHamcrest.xcscheme create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/OCHamcrest.xcodeproj/xcshareddata/xcschemes/libochamcrest.xcscheme create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests-Info.plist create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Collection/FakeWithCount.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Collection/FakeWithCount.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Collection/FakeWithoutCount.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Collection/FakeWithoutCount.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Collection/HasCountTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Collection/IsCollectionContainingInAnyOrderTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Collection/IsCollectionContainingInOrderTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Collection/IsCollectionContainingTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Collection/IsCollectionOnlyContainingTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Collection/IsDictionaryContainingEntriesTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Collection/IsDictionaryContainingKeyTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Collection/IsDictionaryContainingTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Collection/IsDictionaryContainingValueTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Collection/IsEmptyCollectionTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Collection/IsInTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Core/AbstractMatcherTest.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Core/AbstractMatcherTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Core/AssertThatTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Core/BaseMatcherTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Core/InvocationMatcherTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Core/StringDescriptionTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Decorator/DescribedAsTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Decorator/IsTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Decorator/NeverMatch.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Decorator/NeverMatch.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Logical/AllOfTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Logical/AnyOfTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Logical/IsAnythingTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Logical/IsNotTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Number/IsCloseToTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Number/IsEqualToNumberTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Number/NumberAssertTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Number/OrderingComparisonTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Object/ConformsToProtocolTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Object/HasDescriptionTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Object/HasPropertyTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Object/IsEqualTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Object/IsInstanceOfTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Object/IsNilTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Object/IsSameTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Text/IsEqualIgnoringCaseTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Text/IsEqualIgnoringWhiteSpaceTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Text/StringContainsInOrderTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Text/StringContainsTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Text/StringEndsWithTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Tests/Text/StringStartsWithTest.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/Warnings.xcconfig create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/XcodeCoverage/.gitignore create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/XcodeCoverage/LICENSE.txt create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/XcodeCoverage/README.md create mode 100755 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/XcodeCoverage/cleancov create mode 100644 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/XcodeCoverage/envcov.sh create mode 100755 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/XcodeCoverage/exportenv.sh create mode 100755 Vendor/facebook-ios-sdk/vendor/OCHamcrest/Source/XcodeCoverage/getcov create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/.gitignore create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/ArcExample/ArcExample.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/ArcExample/ArcExample/ArcExample-Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/ArcExample/ArcExample/ArcExample.1 create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/ArcExample/ArcExample/main.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/iOS5Example.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/iOS5Example/AppDelegate.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/iOS5Example/AppDelegate.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/iOS5Example/DetailViewController.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/iOS5Example/DetailViewController.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/iOS5Example/MasterViewController.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/iOS5Example/MasterViewController.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/iOS5Example/en.lproj/InfoPlist.strings create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/iOS5Example/en.lproj/MainStoryboard_iPad.storyboard create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/iOS5Example/en.lproj/MainStoryboard_iPhone.storyboard create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/iOS5Example/iOS5Example-Info.plist create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/iOS5Example/iOS5Example-Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/iOS5Example/main.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/ProtocolTests.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/ProtocolTests.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/en.lproj/InfoPlist.strings create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/iOS5ExampleTests-Info.plist create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/iOS5ExampleTests.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/iOS5ExampleTests.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/NSNotificationCenter+OCMAdditions.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMArg.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMConstraint.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMock.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMockObject.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMockRecorder.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/Classes/RootViewController.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/Classes/RootViewController.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/Classes/iPhoneExampleAppDelegate.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/Classes/iPhoneExampleAppDelegate.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/NSNotificationCenter+OCMAdditions.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMArg.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMConstraint.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMock.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMockObject.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMockRecorder.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/Libraries/libOCMock.a create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/MainWindow.xib create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/OCMockLogo.png create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/RootViewController.xib create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/Tests/RootViewControllerTests.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/Tests/RootViewControllerTests.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/iPhoneExample-Info.plist create mode 100755 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/iPhoneExample.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/iPhoneExampleTests-Info.plist create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/iPhoneExample_Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Examples/iPhoneExample/main.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/README.md create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/Changes.txt create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/Frameworks/OCHamcrest.tar.bz2 create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/License.txt create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/NSInvocation+OCMAdditions.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/NSInvocation+OCMAdditions.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/NSMethodSignature+OCMAdditions.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/NSMethodSignature+OCMAdditions.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/NSNotificationCenter+OCMAdditions.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/NSNotificationCenter+OCMAdditions.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCClassMockObject.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCClassMockObject.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMArg.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMArg.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMBlockCaller.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMBlockCaller.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMBoxedReturnValueProvider.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMBoxedReturnValueProvider.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMConstraint.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMConstraint.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMExceptionReturnValueProvider.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMExceptionReturnValueProvider.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMIndirectReturnValueProvider.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMIndirectReturnValueProvider.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMNotificationPoster.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMNotificationPoster.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMObserverRecorder.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMObserverRecorder.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMPassByRefSetter.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMPassByRefSetter.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMRealObjectForwarder.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMRealObjectForwarder.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMReturnValueProvider.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMReturnValueProvider.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMock-Info.plist create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMock-Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMock.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMockObject.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMockObject.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMockRecorder.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCMockRecorder.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCObserverMockObject.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCObserverMockObject.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCPartialMockObject.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCPartialMockObject.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCPartialMockRecorder.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCPartialMockRecorder.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCProtocolMockObject.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/OCProtocolMockObject.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMock/en.lproj/InfoPlist.strings create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockLib/OCMockLib-Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/NSInvocationOCMAdditionsTests.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/NSInvocationOCMAdditionsTests.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/OCMConstraintTests.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/OCMConstraintTests.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/OCMockObjectClassMethodMockingTests.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/OCMockObjectClassMethodMockingTests.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/OCMockObjectForwardingTargetTests.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/OCMockObjectForwardingTargetTests.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/OCMockObjectHamcrestTests.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/OCMockObjectHamcrestTests.mm create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/OCMockObjectPartialMocksTests.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/OCMockObjectPartialMocksTests.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/OCMockObjectTests.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/OCMockObjectTests.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/OCMockRecorderTests.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/OCMockRecorderTests.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/OCMockTests-Info.plist create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/OCObserverMockObjectTests.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/OCObserverMockObjectTests.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OCMock/Source/OCMockTests/en.lproj/InfoPlist.strings create mode 100755 Vendor/facebook-ios-sdk/vendor/OCMock/Tools/build.rb create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/.gitignore create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/.gitmodules create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/LICENSE create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/OHHTTPStubs.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/OHHTTPStubs.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/OHHTTPStubs.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/OHHTTPStubs.xcodeproj/xcshareddata/xcschemes/OHHTTPStubs.xcscheme create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/OHHTTPStubsResponse.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/OHHTTPStubsResponse.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/PortableLibrary.xcconfig create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/.gitignore create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking.podspec create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking/AFHTTPClient.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking/AFHTTPClient.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking/AFHTTPRequestOperation.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking/AFHTTPRequestOperation.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking/AFImageRequestOperation.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking/AFImageRequestOperation.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking/AFJSONRequestOperation.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking/AFJSONRequestOperation.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking/AFNetworkActivityIndicatorManager.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking/AFNetworkActivityIndicatorManager.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking/AFNetworking.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking/AFPropertyListRequestOperation.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking/AFPropertyListRequestOperation.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking/AFURLConnectionOperation.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking/AFURLConnectionOperation.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking/AFXMLRequestOperation.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking/AFXMLRequestOperation.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking/UIImageView+AFNetworking.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/AFNetworking/UIImageView+AFNetworking.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/CHANGES create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/AFNetworking Example.entitlements create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/AFNetworking Mac Example.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/AFNetworking iOS Example.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/AppDelegate.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/AppDelegate.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/Classes/AFAppDotNetAPIClient.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/Classes/AFAppDotNetAPIClient.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/Classes/Controllers/GlobalTimelineViewController.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/Classes/Controllers/GlobalTimelineViewController.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/Classes/Models/Post.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/Classes/Models/Post.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/Classes/Models/User.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/Classes/Models/User.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/Classes/Views/PostTableViewCell.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/Classes/Views/PostTableViewCell.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/Default-568h@2x.png create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/Default.png create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/Default@2x.png create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/Icon.png create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/Icon@2x.png create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/Images/profile-image-placeholder.png create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/Images/profile-image-placeholder@2x.png create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/Mac-Info.plist create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/MainMenu.xib create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/en.lproj/MainMenu.xib create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/iOS-Info.plist create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/Example/main.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/LICENSE create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AFNetworking/README.md create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AsyncSenTestCase.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/AsyncSenTestCase.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/Test Suites/AFNetworkingTests.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/Test Suites/NSURLConnectionDelegateTests.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/Test Suites/NSURLConnectionTests.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/Test Suites/WithContentsOfURLTests.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/UnitTests-Info.plist create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubs/UnitTests/UnitTests-Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubsDemo.xcworkspace/contents.xcworkspacedata create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubsDemo.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubsDemo/Default-568h@2x.png create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubsDemo/MainViewController.h create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubsDemo/MainViewController.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubsDemo/MainViewController.xib create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubsDemo/OHHTTPStubsDemo-Info.plist create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubsDemo/OHHTTPStubsDemo-Prefix.pch create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubsDemo/OHHTTPStubsDemo.xcodeproj/project.pbxproj create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubsDemo/OHHTTPStubsDemo.xcodeproj/xcshareddata/xcschemes/OHHTTPStubsDemo.xcscheme create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubsDemo/main.m create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubsDemo/stub.jpg create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/OHHTTPStubsDemo/stub.txt create mode 100644 Vendor/facebook-ios-sdk/vendor/OHHTTPStubs/README.md create mode 100755 Vendor/libidn/build-libidn.sh create mode 100644 Vendor/libidn/idn-int.h create mode 100644 Vendor/libidn/libidn.a create mode 100644 Vendor/libidn/stringprep.h create mode 100644 XMPPFramework.h create mode 100644 module/module.modulemap diff --git a/Authentication/Anonymous/XMPPAnonymousAuthentication.h b/Authentication/Anonymous/XMPPAnonymousAuthentication.h new file mode 100644 index 0000000..94b5515 --- /dev/null +++ b/Authentication/Anonymous/XMPPAnonymousAuthentication.h @@ -0,0 +1,45 @@ +#import +#import "XMPPSASLAuthentication.h" +#import "XMPP.h" + + +@interface XMPPAnonymousAuthentication : NSObject + +- (id)initWithStream:(XMPPStream *)stream; + +// This class implements the XMPPSASLAuthentication protocol. +// +// See XMPPSASLAuthentication.h for more information. + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPStream (XMPPAnonymousAuthentication) + +/** + * Returns whether or not the server support anonymous authentication. + * + * This information is available after the stream is connected. + * In other words, after the delegate has received xmppStreamDidConnect: notification. +**/ +- (BOOL)supportsAnonymousAuthentication; + +/** + * This method attempts to start the anonymous authentication process. + * + * This method is asynchronous. + * + * If there is something immediately wrong, + * such as the stream is not connected or doesn't support anonymous authentication, + * the method will return NO and set the error. + * Otherwise the delegate callbacks are used to communicate auth success or failure. + * + * @see xmppStreamDidAuthenticate: + * @see xmppStream:didNotAuthenticate: +**/ +- (BOOL)authenticateAnonymously:(NSError **)errPtr; + +@end diff --git a/Authentication/Anonymous/XMPPAnonymousAuthentication.m b/Authentication/Anonymous/XMPPAnonymousAuthentication.m new file mode 100644 index 0000000..aadc1a7 --- /dev/null +++ b/Authentication/Anonymous/XMPPAnonymousAuthentication.m @@ -0,0 +1,131 @@ +#import "XMPPAnonymousAuthentication.h" +#import "XMPP.h" +#import "XMPPLogging.h" +#import "XMPPInternal.h" +#import "NSXMLElement+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +/** + * Seeing a return statements within an inner block + * can sometimes be mistaken for a return point of the enclosing method. + * This makes inline blocks a bit easier to read. +**/ +#define return_from_block return + + +@implementation XMPPAnonymousAuthentication +{ + #if __has_feature(objc_arc_weak) + __weak XMPPStream *xmppStream; + #else + __unsafe_unretained XMPPStream *xmppStream; + #endif +} + ++ (NSString *)mechanismName +{ + return @"ANONYMOUS"; +} + +- (id)initWithStream:(XMPPStream *)stream +{ + if ((self = [super init])) + { + xmppStream = stream; + } + return self; +} + +- (id)initWithStream:(XMPPStream *)stream password:(NSString *)password +{ + return [self initWithStream:stream]; +} + +- (BOOL)start:(NSError **)errPtr +{ + // + + NSXMLElement *auth = [NSXMLElement elementWithName:@"auth" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + [auth addAttributeWithName:@"mechanism" stringValue:@"ANONYMOUS"]; + + [xmppStream sendAuthElement:auth]; + + return YES; +} + +- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)authResponse +{ + // We're expecting a success response. + // If we get anything else we can safely assume it's the equivalent of a failure response. + + if ([[authResponse name] isEqualToString:@"success"]) + { + return XMPP_AUTH_SUCCESS; + } + else + { + return XMPP_AUTH_FAIL; + } +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPStream (XMPPAnonymousAuthentication) + +- (BOOL)supportsAnonymousAuthentication +{ + return [self supportsAuthenticationMechanism:[XMPPAnonymousAuthentication mechanismName]]; +} + +- (BOOL)authenticateAnonymously:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = YES; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if ([self supportsAnonymousAuthentication]) + { + XMPPAnonymousAuthentication *anonymousAuth = [[XMPPAnonymousAuthentication alloc] initWithStream:self]; + + result = [self authenticate:anonymousAuth error:&err]; + } + else + { + NSString *errMsg = @"The server does not support anonymous authentication."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamUnsupportedAction userInfo:info]; + + result = NO; + } + }}; + + if (dispatch_get_specific(self.xmppQueueTag)) + block(); + else + dispatch_sync(self.xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +@end diff --git a/Authentication/Deprecated-Digest/XMPPDeprecatedDigestAuthentication.h b/Authentication/Deprecated-Digest/XMPPDeprecatedDigestAuthentication.h new file mode 100644 index 0000000..4993f03 --- /dev/null +++ b/Authentication/Deprecated-Digest/XMPPDeprecatedDigestAuthentication.h @@ -0,0 +1,22 @@ +#import +#import "XMPPSASLAuthentication.h" +#import "XMPPStream.h" + + +@interface XMPPDeprecatedDigestAuthentication : NSObject + +// This class implements the XMPPSASLAuthentication protocol. +// +// See XMPPSASLAuthentication.h for more information. + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPStream (XMPPDeprecatedDigestAuthentication) + +- (BOOL)supportsDeprecatedDigestAuthentication; + +@end diff --git a/Authentication/Deprecated-Digest/XMPPDeprecatedDigestAuthentication.m b/Authentication/Deprecated-Digest/XMPPDeprecatedDigestAuthentication.m new file mode 100644 index 0000000..c39b521 --- /dev/null +++ b/Authentication/Deprecated-Digest/XMPPDeprecatedDigestAuthentication.m @@ -0,0 +1,162 @@ +#import "XMPPDeprecatedDigestAuthentication.h" +#import "XMPP.h" +#import "XMPPInternal.h" +#import "XMPPLogging.h" +#import "NSData+XMPP.h" +#import "NSXMLElement+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + + +@implementation XMPPDeprecatedDigestAuthentication +{ + #if __has_feature(objc_arc_weak) + __weak XMPPStream *xmppStream; + #else + __unsafe_unretained XMPPStream *xmppStream; + #endif + + NSString *password; +} + ++ (NSString *)mechanismName +{ + // This deprecated method isn't listed in the normal mechanisms list + return nil; +} + +- (id)initWithStream:(XMPPStream *)stream password:(NSString *)inPassword +{ + if ((self = [super init])) + { + xmppStream = stream; + password = inPassword; + } + return self; +} + +- (BOOL)start:(NSError **)errPtr +{ + XMPPLogTrace(); + + // The server does not appear to support SASL authentication (at least any type we can use) + // So we'll revert back to the old fashioned jabber:iq:auth mechanism + + XMPPJID *myJID = xmppStream.myJID; + + NSString *username = [myJID user]; + NSString *resource = [myJID resource]; + + if ([resource length] == 0) + { + // If resource is nil or empty, we need to auto-create one + + resource = [XMPPStream generateUUID]; + } + + NSString *rootID = [[[xmppStream rootElement] attributeForName:@"id"] stringValue]; + NSString *digestStr = [NSString stringWithFormat:@"%@%@", rootID, password]; + + NSString *digest = [[[digestStr dataUsingEncoding:NSUTF8StringEncoding] xmpp_sha1Digest] xmpp_hexStringValue]; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:auth"]; + [query addChild:[NSXMLElement elementWithName:@"username" stringValue:username]]; + [query addChild:[NSXMLElement elementWithName:@"resource" stringValue:resource]]; + [query addChild:[NSXMLElement elementWithName:@"digest" stringValue:digest]]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set"]; + [iq addChild:query]; + + [xmppStream sendAuthElement:iq]; + + return YES; +} + +- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + // We used the old fashioned jabber:iq:auth mechanism + + if ([[authResponse attributeStringValueForName:@"type"] isEqualToString:@"error"]) + { + return XMPP_AUTH_FAIL; + } + else + { + return XMPP_AUTH_SUCCESS; + } +} + +- (BOOL)shouldResendOpeningNegotiationAfterSuccessfulAuthentication +{ + return NO; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPStream (XMPPDeprecatedDigestAuthentication) + +/** + * This method only applies to servers that don't support XMPP version 1.0, as defined in RFC 3920. + * With these servers, we attempt to discover supported authentication modes via the jabber:iq:auth namespace. +**/ +- (BOOL)supportsDeprecatedDigestAuthentication +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool { + + // The root element can be properly queried for authentication mechanisms anytime after the + // stream:features are received, and TLS has been setup (if required) + if (self.state >= STATE_XMPP_POST_NEGOTIATION) + { + // Search for an iq element within the rootElement. + // Recall that some servers might stupidly add a "jabber:client" namespace which might cause problems + // if we simply used the elementForName method. + + NSXMLElement *iq = nil; + + NSUInteger i, count = [self.rootElement childCount]; + for (i = 0; i < count; i++) + { + NSXMLNode *childNode = [self.rootElement childAtIndex:i]; + + if ([childNode kind] == NSXMLElementKind) + { + if ([[childNode name] isEqualToString:@"iq"]) + { + iq = (NSXMLElement *)childNode; + } + } + } + + NSXMLElement *query = [iq elementForName:@"query" xmlns:@"jabber:iq:auth"]; + NSXMLElement *digest = [query elementForName:@"digest"]; + + result = (digest != nil); + } + }}; + + if (dispatch_get_specific(self.xmppQueueTag)) + block(); + else + dispatch_sync(self.xmppQueue, block); + + return result; +} + +@end diff --git a/Authentication/Deprecated-Plain/XMPPDeprecatedPlainAuthentication.h b/Authentication/Deprecated-Plain/XMPPDeprecatedPlainAuthentication.h new file mode 100644 index 0000000..b86efa3 --- /dev/null +++ b/Authentication/Deprecated-Plain/XMPPDeprecatedPlainAuthentication.h @@ -0,0 +1,22 @@ +#import +#import "XMPPSASLAuthentication.h" +#import "XMPPStream.h" + + +@interface XMPPDeprecatedPlainAuthentication : NSObject + +// This class implements the XMPPSASLAuthentication protocol. +// +// See XMPPSASLAuthentication.h for more information. + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPStream (XMPPDeprecatedPlainAuthentication) + +- (BOOL)supportsDeprecatedPlainAuthentication; + +@end diff --git a/Authentication/Deprecated-Plain/XMPPDeprecatedPlainAuthentication.m b/Authentication/Deprecated-Plain/XMPPDeprecatedPlainAuthentication.m new file mode 100644 index 0000000..f6c36b4 --- /dev/null +++ b/Authentication/Deprecated-Plain/XMPPDeprecatedPlainAuthentication.m @@ -0,0 +1,156 @@ +#import "XMPPDeprecatedPlainAuthentication.h" +#import "XMPP.h" +#import "XMPPInternal.h" +#import "XMPPLogging.h" +#import "NSXMLElement+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + + +@implementation XMPPDeprecatedPlainAuthentication +{ + #if __has_feature(objc_arc_weak) + __weak XMPPStream *xmppStream; + #else + __unsafe_unretained XMPPStream *xmppStream; + #endif + + NSString *password; +} + ++ (NSString *)mechanismName +{ + // This deprecated method isn't listed in the normal mechanisms list + return nil; +} + +- (id)initWithStream:(XMPPStream *)stream password:(NSString *)inPassword +{ + if ((self = [super init])) + { + xmppStream = stream; + password = inPassword; + } + return self; +} + +- (BOOL)start:(NSError **)errPtr +{ + XMPPLogTrace(); + + // The server does not appear to support SASL authentication (at least any type we can use) + // So we'll revert back to the old fashioned jabber:iq:auth mechanism + + XMPPJID *myJID = xmppStream.myJID; + + NSString *username = [myJID user]; + NSString *resource = [myJID resource]; + + if ([resource length] == 0) + { + // If resource is nil or empty, we need to auto-create one + + resource = [XMPPStream generateUUID]; + } + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:auth"]; + [query addChild:[NSXMLElement elementWithName:@"username" stringValue:username]]; + [query addChild:[NSXMLElement elementWithName:@"resource" stringValue:resource]]; + [query addChild:[NSXMLElement elementWithName:@"password" stringValue:password]]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set"]; + [iq addChild:query]; + + [xmppStream sendAuthElement:iq]; + + return YES; +} + +- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + // We used the old fashioned jabber:iq:auth mechanism + + if ([[authResponse attributeStringValueForName:@"type"] isEqualToString:@"error"]) + { + return XMPP_AUTH_FAIL; + } + else + { + return XMPP_AUTH_SUCCESS; + } +} + +- (BOOL)shouldResendOpeningNegotiationAfterSuccessfulAuthentication +{ + return NO; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPStream (XMPPDeprecatedPlainAuthentication) + +/** + * This method only applies to servers that don't support XMPP version 1.0, as defined in RFC 3920. + * With these servers, we attempt to discover supported authentication modes via the jabber:iq:auth namespace. +**/ +- (BOOL)supportsDeprecatedPlainAuthentication +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool { + + // The root element can be properly queried for authentication mechanisms anytime after the + // stream:features are received, and TLS has been setup (if required) + if (self.state >= STATE_XMPP_POST_NEGOTIATION) + { + // Search for an iq element within the rootElement. + // Recall that some servers might stupidly add a "jabber:client" namespace which might cause problems + // if we simply used the elementForName method. + + NSXMLElement *iq = nil; + + NSUInteger i, count = [self.rootElement childCount]; + for (i = 0; i < count; i++) + { + NSXMLNode *childNode = [self.rootElement childAtIndex:i]; + + if ([childNode kind] == NSXMLElementKind) + { + if ([[childNode name] isEqualToString:@"iq"]) + { + iq = (NSXMLElement *)childNode; + } + } + } + + NSXMLElement *query = [iq elementForName:@"query" xmlns:@"jabber:iq:auth"]; + NSXMLElement *plain = [query elementForName:@"password"]; + + result = (plain != nil); + } + }}; + + if (dispatch_get_specific(self.xmppQueueTag)) + block(); + else + dispatch_sync(self.xmppQueue, block); + + return result; +} + +@end diff --git a/Authentication/Digest-MD5/XMPPDigestMD5Authentication.h b/Authentication/Digest-MD5/XMPPDigestMD5Authentication.h new file mode 100644 index 0000000..b228781 --- /dev/null +++ b/Authentication/Digest-MD5/XMPPDigestMD5Authentication.h @@ -0,0 +1,22 @@ +#import +#import "XMPPSASLAuthentication.h" +#import "XMPPStream.h" + + +@interface XMPPDigestMD5Authentication : NSObject + +// This class implements the XMPPSASLAuthentication protocol. +// +// See XMPPSASLAuthentication.h for more information. + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPStream (XMPPDigestMD5Authentication) + +- (BOOL)supportsDigestMD5Authentication; + +@end diff --git a/Authentication/Digest-MD5/XMPPDigestMD5Authentication.m b/Authentication/Digest-MD5/XMPPDigestMD5Authentication.m new file mode 100644 index 0000000..28d4697 --- /dev/null +++ b/Authentication/Digest-MD5/XMPPDigestMD5Authentication.m @@ -0,0 +1,336 @@ +#import "XMPPDigestMD5Authentication.h" +#import "XMPP.h" +#import "XMPPLogging.h" +#import "XMPPInternal.h" +#import "NSData+XMPP.h" +#import "NSXMLElement+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +@interface XMPPDigestMD5Authentication () +{ + #if __has_feature(objc_arc_weak) + __weak XMPPStream *xmppStream; + #else + __unsafe_unretained XMPPStream *xmppStream; + #endif + + BOOL awaitingChallenge; + + NSString *realm; + NSString *nonce; + NSString *qop; + NSString *cnonce; + NSString *digestURI; + NSString *username; + NSString *password; +} + +// The properties are hooks (primarily for testing) + +@property (nonatomic, strong) NSString *realm; +@property (nonatomic, strong) NSString *nonce; +@property (nonatomic, strong) NSString *qop; +@property (nonatomic, strong) NSString *cnonce; +@property (nonatomic, strong) NSString *digestURI; +@property (nonatomic, strong) NSString *username; +@property (nonatomic, strong) NSString *password; + +- (NSDictionary *)dictionaryFromChallenge:(NSXMLElement *)challenge; +- (NSString *)base64EncodedFullResponse; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPDigestMD5Authentication + ++ (NSString *)mechanismName +{ + return @"DIGEST-MD5"; +} + +@synthesize realm; +@synthesize nonce; +@synthesize qop; +@synthesize cnonce; +@synthesize digestURI; +@synthesize username; +@synthesize password; + +- (id)initWithStream:(XMPPStream *)stream password:(NSString *)inPassword +{ + return [self initWithStream:stream username:nil password:inPassword]; +} + +- (id)initWithStream:(XMPPStream *)stream username:(NSString *)inUsername password:(NSString *)inPassword +{ + if ((self = [super init])) + { + xmppStream = stream; + username = inUsername; + password = inPassword; + } + return self; +} + +- (BOOL)start:(NSError **)errPtr +{ + XMPPLogTrace(); + + // + + NSXMLElement *auth = [NSXMLElement elementWithName:@"auth" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + [auth addAttributeWithName:@"mechanism" stringValue:@"DIGEST-MD5"]; + + [xmppStream sendAuthElement:auth]; + awaitingChallenge = YES; + + return YES; +} + +- (XMPPHandleAuthResponse)handleAuth1:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + // We're expecting a challenge response. + // If we get anything else we're going to assume it's some kind of failure response. + + if (![[authResponse name] isEqualToString:@"challenge"]) + { + return XMPP_AUTH_FAIL; + } + + // Extract components from incoming challenge + + NSDictionary *auth = [self dictionaryFromChallenge:authResponse]; + + realm = auth[@"realm"]; + nonce = auth[@"nonce"]; + qop = auth[@"qop"]; + + // Fill out all the other variables + // + // Sometimes the realm isn't specified. + // In this case I believe the realm is implied as the virtual host name. + + XMPPJID *myJID = xmppStream.myJID; + + NSString *virtualHostName = [myJID domain]; + NSString *serverHostName = xmppStream.hostName; + + if (realm == nil) + { + if ([virtualHostName length] > 0) + realm = virtualHostName; + else + realm = serverHostName; + } + + if ([virtualHostName length] > 0) + digestURI = [NSString stringWithFormat:@"xmpp/%@", virtualHostName]; + else + digestURI = [NSString stringWithFormat:@"xmpp/%@", serverHostName]; + + if (cnonce == nil) + cnonce = [XMPPStream generateUUID]; + + if (username == nil) + { + username = [myJID user]; + } + + // Create and send challenge response element + + NSXMLElement *response = [NSXMLElement elementWithName:@"response" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + [response setStringValue:[self base64EncodedFullResponse]]; + + [xmppStream sendAuthElement:response]; + awaitingChallenge = NO; + + return XMPP_AUTH_CONTINUE; +} + +- (XMPPHandleAuthResponse)handleAuth2:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + if ([[authResponse name] isEqualToString:@"challenge"]) + { + NSDictionary *auth = [self dictionaryFromChallenge:authResponse]; + NSString *rspauth = auth[@"rspauth"]; + + if (rspauth == nil) + { + // We're getting another challenge? + // Not sure what this could possibly be, so for now we'll assume it's a failure. + + return XMPP_AUTH_FAIL; + } + else + { + // We received another challenge, but it's really just an rspauth + // This is supposed to be included in the success element (according to the updated RFC) + // but many implementations incorrectly send it inside a second challenge request. + // + // Create and send empty challenge response element. + + NSXMLElement *response = + [NSXMLElement elementWithName:@"response" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + + [xmppStream sendAuthElement:response]; + + return XMPP_AUTH_CONTINUE; + } + } + else if ([[authResponse name] isEqualToString:@"success"]) + { + return XMPP_AUTH_SUCCESS; + } + else + { + return XMPP_AUTH_FAIL; + } +} + +- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)auth +{ + XMPPLogTrace(); + + if (awaitingChallenge) + { + return [self handleAuth1:auth]; + } + else + { + return [self handleAuth2:auth]; + } +} + +- (NSDictionary *)dictionaryFromChallenge:(NSXMLElement *)challenge +{ + // The value of the challenge stanza is base 64 encoded. + // Once "decoded", it's just a string of key=value pairs separated by commas. + + NSData *base64Data = [[challenge stringValue] dataUsingEncoding:NSASCIIStringEncoding]; + NSData *decodedData = [base64Data xmpp_base64Decoded]; + + NSString *authStr = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; + + XMPPLogVerbose(@"%@: Decoded challenge: %@", THIS_FILE, authStr); + + NSArray *components = [authStr componentsSeparatedByString:@","]; + NSMutableDictionary *auth = [NSMutableDictionary dictionaryWithCapacity:5]; + + for (NSString *component in components) + { + NSRange separator = [component rangeOfString:@"="]; + if (separator.location != NSNotFound) + { + NSMutableString *key = [[component substringToIndex:separator.location] mutableCopy]; + NSMutableString *value = [[component substringFromIndex:separator.location+1] mutableCopy]; + + if(key) CFStringTrimWhitespace((__bridge CFMutableStringRef)key); + if(value) CFStringTrimWhitespace((__bridge CFMutableStringRef)value); + + if ([value hasPrefix:@"\""] && [value hasSuffix:@"\""] && [value length] > 2) + { + // Strip quotes from value + [value deleteCharactersInRange:NSMakeRange(0, 1)]; + [value deleteCharactersInRange:NSMakeRange([value length]-1, 1)]; + } + + if(key && value) + { + auth[key] = value; + } + } + } + + return auth; +} + +- (NSString *)response +{ + NSString *HA1str = [NSString stringWithFormat:@"%@:%@:%@", username, realm, password]; + NSString *HA2str = [NSString stringWithFormat:@"AUTHENTICATE:%@", digestURI]; + + XMPPLogVerbose(@"HA1str: %@", HA1str); + XMPPLogVerbose(@"HA2str: %@", HA2str); + + NSData *HA1dataA = [[HA1str dataUsingEncoding:NSUTF8StringEncoding] xmpp_md5Digest]; + NSData *HA1dataB = [[NSString stringWithFormat:@":%@:%@", nonce, cnonce] dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogVerbose(@"HA1dataA: %@", HA1dataA); + XMPPLogVerbose(@"HA1dataB: %@", HA1dataB); + + NSMutableData *HA1data = [NSMutableData dataWithCapacity:([HA1dataA length] + [HA1dataB length])]; + [HA1data appendData:HA1dataA]; + [HA1data appendData:HA1dataB]; + + XMPPLogVerbose(@"HA1data: %@", HA1data); + + NSString *HA1 = [[HA1data xmpp_md5Digest] xmpp_hexStringValue]; + + NSString *HA2 = [[[HA2str dataUsingEncoding:NSUTF8StringEncoding] xmpp_md5Digest] xmpp_hexStringValue]; + + XMPPLogVerbose(@"HA1: %@", HA1); + XMPPLogVerbose(@"HA2: %@", HA2); + + NSString *responseStr = [NSString stringWithFormat:@"%@:%@:00000001:%@:auth:%@", + HA1, nonce, cnonce, HA2]; + + XMPPLogVerbose(@"responseStr: %@", responseStr); + + NSString *response = [[[responseStr dataUsingEncoding:NSUTF8StringEncoding] xmpp_md5Digest] xmpp_hexStringValue]; + + XMPPLogVerbose(@"response: %@", response); + + return response; +} + +- (NSString *)base64EncodedFullResponse +{ + NSMutableString *buffer = [NSMutableString stringWithCapacity:100]; + [buffer appendFormat:@"username=\"%@\",", username]; + [buffer appendFormat:@"realm=\"%@\",", realm]; + [buffer appendFormat:@"nonce=\"%@\",", nonce]; + [buffer appendFormat:@"cnonce=\"%@\",", cnonce]; + [buffer appendFormat:@"nc=00000001,"]; + [buffer appendFormat:@"qop=auth,"]; + [buffer appendFormat:@"digest-uri=\"%@\",", digestURI]; + [buffer appendFormat:@"response=%@,", [self response]]; + [buffer appendFormat:@"charset=utf-8"]; + + XMPPLogVerbose(@"%@: Decoded response: %@", THIS_FILE, buffer); + + NSData *utf8data = [buffer dataUsingEncoding:NSUTF8StringEncoding]; + + return [utf8data xmpp_base64Encoded]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPStream (XMPPDigestMD5Authentication) + +- (BOOL)supportsDigestMD5Authentication +{ + return [self supportsAuthenticationMechanism:[XMPPDigestMD5Authentication mechanismName]]; +} + +@end diff --git a/Authentication/Plain/XMPPPlainAuthentication.h b/Authentication/Plain/XMPPPlainAuthentication.h new file mode 100644 index 0000000..a1f79f1 --- /dev/null +++ b/Authentication/Plain/XMPPPlainAuthentication.h @@ -0,0 +1,22 @@ +#import +#import "XMPPSASLAuthentication.h" +#import "XMPPStream.h" + + +@interface XMPPPlainAuthentication : NSObject + +// This class implements the XMPPSASLAuthentication protocol. +// +// See XMPPSASLAuthentication.h for more information. + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPStream (XMPPPlainAuthentication) + +- (BOOL)supportsPlainAuthentication; + +@end diff --git a/Authentication/Plain/XMPPPlainAuthentication.m b/Authentication/Plain/XMPPPlainAuthentication.m new file mode 100644 index 0000000..7b74551 --- /dev/null +++ b/Authentication/Plain/XMPPPlainAuthentication.m @@ -0,0 +1,114 @@ +#import "XMPPPlainAuthentication.h" +#import "XMPP.h" +#import "XMPPLogging.h" +#import "XMPPInternal.h" +#import "NSData+XMPP.h" +#import "NSXMLElement+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + + +@implementation XMPPPlainAuthentication +{ + #if __has_feature(objc_arc_weak) + __weak XMPPStream *xmppStream; + #else + __unsafe_unretained XMPPStream *xmppStream; + #endif + + NSString *username; + NSString *password; +} + ++ (NSString *)mechanismName +{ + return @"PLAIN"; +} + +- (id)initWithStream:(XMPPStream *)stream password:(NSString *)inPassword +{ + return [self initWithStream:stream username:nil password:inPassword]; +} + +- (id)initWithStream:(XMPPStream *)stream username:(NSString *)inUsername password:(NSString *)inPassword +{ + if ((self = [super init])) + { + xmppStream = stream; + username = inUsername; + password = inPassword; + } + return self; +} + +- (BOOL)start:(NSError **)errPtr +{ + XMPPLogTrace(); + + // From RFC 4616 - PLAIN SASL Mechanism: + // [authzid] UTF8NUL authcid UTF8NUL passwd + // + // authzid: authorization identity + // authcid: authentication identity (username) + // passwd : password for authcid + + NSString *authUsername = username; + if (!authUsername) + { + authUsername = [xmppStream.myJID user]; + } + + NSString *payload = [NSString stringWithFormat:@"\0%@\0%@", authUsername, password]; + NSString *base64 = [[payload dataUsingEncoding:NSUTF8StringEncoding] xmpp_base64Encoded]; + + // Base-64-Info + + NSXMLElement *auth = [NSXMLElement elementWithName:@"auth" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + [auth addAttributeWithName:@"mechanism" stringValue:@"PLAIN"]; + [auth setStringValue:base64]; + + [xmppStream sendAuthElement:auth]; + + return YES; +} + +- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + // We're expecting a success response. + // If we get anything else we can safely assume it's the equivalent of a failure response. + + if ([[authResponse name] isEqualToString:@"success"]) + { + return XMPP_AUTH_SUCCESS; + } + else + { + return XMPP_AUTH_FAIL; + } +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPStream (XMPPPlainAuthentication) + +- (BOOL)supportsPlainAuthentication +{ + return [self supportsAuthenticationMechanism:[XMPPPlainAuthentication mechanismName]]; +} + +@end diff --git a/Authentication/SCRAM-SHA-1/XMPPSCRAMSHA1Authentication.h b/Authentication/SCRAM-SHA-1/XMPPSCRAMSHA1Authentication.h new file mode 100644 index 0000000..cf741e7 --- /dev/null +++ b/Authentication/SCRAM-SHA-1/XMPPSCRAMSHA1Authentication.h @@ -0,0 +1,21 @@ +// +// XMPPSCRAMSHA1Authentication.h +// iPhoneXMPP +// +// Created by David Chiles on 3/21/14. +// +// + +#import +#import "XMPPSASLAuthentication.h" +#import "XMPPStream.h" + +@interface XMPPSCRAMSHA1Authentication : NSObject + +@end + +@interface XMPPStream (XMPPSCRAMSHA1Authentication) + +- (BOOL)supportsSCRAMSHA1Authentication; + +@end diff --git a/Authentication/SCRAM-SHA-1/XMPPSCRAMSHA1Authentication.m b/Authentication/SCRAM-SHA-1/XMPPSCRAMSHA1Authentication.m new file mode 100644 index 0000000..d0d795b --- /dev/null +++ b/Authentication/SCRAM-SHA-1/XMPPSCRAMSHA1Authentication.m @@ -0,0 +1,342 @@ +// +// XMPPSCRAMSHA1Authentication.m +// iPhoneXMPP +// +// Created by David Chiles on 3/21/14. +// +// + +#import "XMPPSCRAMSHA1Authentication.h" +#import "XMPP.h" +#import "XMPPLogging.h" +#import "XMPPStream.h" +#import "XMPPInternal.h" +#import "NSData+XMPP.h" +#import "XMPPStringPrep.h" + +#import + + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG +static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO; // | XMPP_LOG_FLAG_TRACE; +#else +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +@interface XMPPSCRAMSHA1Authentication () +{ + #if __has_feature(objc_arc_weak) + __weak XMPPStream *xmppStream; + #else + __unsafe_unretained XMPPStream *xmppStream; + #endif +} + +@property (nonatomic) BOOL awaitingChallenge; +@property (nonatomic, strong) NSString *username; +@property (nonatomic, strong) NSString *password; +@property (nonatomic, strong) NSString *clientNonce; +@property (nonatomic, strong) NSString *combinedNonce; +@property (nonatomic, strong) NSString *salt; +@property (nonatomic, strong) NSNumber *count; +@property (nonatomic, strong) NSString *serverMessage1; +@property (nonatomic, strong) NSString *clientFirstMessageBare; +@property (nonatomic, strong) NSData *serverSignatureData; +@property (nonatomic, strong) NSData *clientProofData; +@property (nonatomic) CCHmacAlgorithm hashAlgorithm; + +@end + +///////////RFC5802 http://tools.ietf.org/html/rfc5802 ////////////// + +//Channel binding not yet supported + +@implementation XMPPSCRAMSHA1Authentication + ++ (NSString *)mechanismName +{ + return @"SCRAM-SHA-1"; +} + +- (id)initWithStream:(XMPPStream *)stream password:(NSString *)password +{ + return [self initWithStream:stream username:nil password:password]; +} + +- (id)initWithStream:(XMPPStream *)stream username:(NSString *)username password:(NSString *)password +{ + if ((self = [super init])) { + xmppStream = stream; + if (username) + { + _username = username; + } + else + { + _username = [XMPPStringPrep prepNode:[xmppStream.myJID user]]; + } + _password = [XMPPStringPrep prepPassword:password]; + _hashAlgorithm = kCCHmacAlgSHA1; + } + return self; +} + +- (BOOL)start:(NSError **)errPtr +{ + XMPPLogTrace(); + + if(self.username.length || self.password.length) { + + NSXMLElement *auth = [NSXMLElement elementWithName:@"auth" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + [auth addAttributeWithName:@"mechanism" stringValue:@"SCRAM-SHA-1"]; + [auth setStringValue:[self clientMessage1]]; + + [xmppStream sendAuthElement:auth]; + self.awaitingChallenge = YES; + + return YES; + } + else { + return NO; + } +} + +- (XMPPHandleAuthResponse)handleAuth1:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + // We're expecting a challenge response. + // If we get anything else we're going to assume it's some kind of failure response. + + if (![[authResponse name] isEqualToString:@"challenge"]) + { + return XMPP_AUTH_FAIL; + } + + NSDictionary *auth = [self dictionaryFromChallenge:authResponse]; + + NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; + [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle]; + + self.combinedNonce = auth[@"r"]; + self.salt = auth[@"s"]; + self.count = [numberFormatter numberFromString:auth[@"i"]]; + + //We have all the necessary information to calculate client proof and server signature + if ([self calculateProofs]) { + NSXMLElement *response = [NSXMLElement elementWithName:@"response" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + [response setStringValue:[self clientMessage2]]; + + [xmppStream sendAuthElement:response]; + self.awaitingChallenge = NO; + + return XMPP_AUTH_CONTINUE; + } + else { + return XMPP_AUTH_FAIL; + } +} + +- (XMPPHandleAuthResponse)handleAuth2:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + NSDictionary *auth = [self dictionaryFromChallenge:authResponse]; + + if ([[authResponse name] isEqual:@"success"]) { + NSString *receivedServerSignature = auth[@"v"]; + + if([self.serverSignatureData isEqualToData:[[receivedServerSignature dataUsingEncoding:NSUTF8StringEncoding] xmpp_base64Decoded]]){ + return XMPP_AUTH_SUCCESS; + } + else { + return XMPP_AUTH_FAIL; + } + } + else { + return XMPP_AUTH_FAIL; + } +} + +- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)auth +{ + XMPPLogTrace(); + + if (self.awaitingChallenge) { + return [self handleAuth1:auth]; + } + else { + return [self handleAuth2:auth]; + } +} + +- (NSString *)clientMessage1 +{ + self.clientNonce = [XMPPStream generateUUID]; + + self.clientFirstMessageBare = [NSString stringWithFormat:@"n=%@,r=%@",self.username,self.clientNonce]; + + NSData *message1Data = [[NSString stringWithFormat:@"n,,%@",self.clientFirstMessageBare] dataUsingEncoding:NSUTF8StringEncoding]; + + return [message1Data xmpp_base64Encoded]; +} + +- (NSString *)clientMessage2 +{ + NSString *clientProofString = [self.clientProofData xmpp_base64Encoded]; + NSData *message2Data = [[NSString stringWithFormat:@"c=biws,r=%@,p=%@",self.combinedNonce,clientProofString] dataUsingEncoding:NSUTF8StringEncoding]; + + return [message2Data xmpp_base64Encoded]; +} + +- (BOOL)calculateProofs +{ + //Check to see that we have a password, salt and iteration count above 4096 (from RFC5802) + if (!self.password.length || !self.salt.length || self.count.unsignedIntegerValue < 4096) { + return NO; + } + + NSData *passwordData = [self.password dataUsingEncoding:NSUTF8StringEncoding]; + NSData *saltData = [[self.salt dataUsingEncoding:NSUTF8StringEncoding] xmpp_base64Decoded]; + + NSData *saltedPasswordData = [self HashWithAlgorithm:self.hashAlgorithm password:passwordData salt:saltData iterations:[self.count unsignedIntValue]]; + + NSData *clientKeyData = [self HashWithAlgorithm:self.hashAlgorithm data:[@"Client Key" dataUsingEncoding:NSUTF8StringEncoding] key:saltedPasswordData]; + NSData *serverKeyData = [self HashWithAlgorithm:self.hashAlgorithm data:[@"Server Key" dataUsingEncoding:NSUTF8StringEncoding] key:saltedPasswordData]; + + NSData *storedKeyData = [clientKeyData xmpp_sha1Digest]; + + NSData *authMessageData = [[NSString stringWithFormat:@"%@,%@,c=biws,r=%@",self.clientFirstMessageBare,self.serverMessage1,self.combinedNonce] dataUsingEncoding:NSUTF8StringEncoding]; + + NSData *clientSignatureData = [self HashWithAlgorithm:self.hashAlgorithm data:authMessageData key:storedKeyData]; + + self.serverSignatureData = [self HashWithAlgorithm:self.hashAlgorithm data:authMessageData key:serverKeyData]; + self.clientProofData = [self xorData:clientKeyData withData:clientSignatureData]; + + //check to see that we caclulated some client proof and server signature + if (self.clientProofData && self.serverSignatureData) { + return YES; + } + else { + return NO; + } +} + +- (NSData *)HashWithAlgorithm:(CCHmacAlgorithm) algorithm password:(NSData *)passwordData salt:(NSData *)saltData iterations:(NSUInteger)rounds +{ + NSMutableData *mutableSaltData = [saltData mutableCopy]; + UInt8 zeroHex= 0x00; + UInt8 oneHex= 0x01; + NSData *zeroData = [[NSData alloc] initWithBytes:&zeroHex length:sizeof(zeroHex)]; + NSData *oneData = [[NSData alloc] initWithBytes:&oneHex length:sizeof(oneHex)]; + + [mutableSaltData appendData:zeroData]; + [mutableSaltData appendData:zeroData]; + [mutableSaltData appendData:zeroData]; + [mutableSaltData appendData:oneData]; + + NSData *result = [self HashWithAlgorithm:algorithm data:mutableSaltData key:passwordData]; + NSData *previous = [result copy]; + + for (int i = 1; i < rounds; i++) { + previous = [self HashWithAlgorithm:algorithm data:previous key:passwordData]; + result = [self xorData:result withData:previous]; + } + + return result; +} + +- (NSData *)HashWithAlgorithm:(CCHmacAlgorithm) algorithm data:(NSData *)data key:(NSData *)key +{ + unsigned char cHMAC[CC_SHA1_DIGEST_LENGTH]; + + CCHmac(algorithm, [key bytes], [key length], [data bytes], [data length], cHMAC); + + return [[NSData alloc] initWithBytes:cHMAC length:sizeof(cHMAC)]; +} + +- (NSData *)xorData:(NSData *)data1 withData:(NSData *)data2 +{ + NSMutableData *result = data1.mutableCopy; + + char *dataPtr = (char *)result.mutableBytes; + + char *keyData = (char *)data2.bytes; + + char *keyPtr = keyData; + int keyIndex = 0; + + for (int x = 0; x < data1.length; x++) { + *dataPtr = *dataPtr ^ *keyPtr; + dataPtr++; + keyPtr++; + + if (++keyIndex == data2.length) { + keyIndex = 0; + keyPtr = keyData; + } + } + return result; +} + +- (NSDictionary *)dictionaryFromChallenge:(NSXMLElement *)challenge +{ + // The value of the challenge stanza is base 64 encoded. + // Once "decoded", it's just a string of key=value pairs separated by commas. + + NSData *base64Data = [[challenge stringValue] dataUsingEncoding:NSASCIIStringEncoding]; + NSData *decodedData = [base64Data xmpp_base64Decoded]; + + self.serverMessage1 = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; + + XMPPLogVerbose(@"%@: Decoded challenge: %@", THIS_FILE, self.serverMessage1); + + NSArray *components = [self.serverMessage1 componentsSeparatedByString:@","]; + NSMutableDictionary *auth = [NSMutableDictionary dictionaryWithCapacity:5]; + + for (NSString *component in components) + { + NSRange separator = [component rangeOfString:@"="]; + if (separator.location != NSNotFound) + { + NSMutableString *key = [[component substringToIndex:separator.location] mutableCopy]; + NSMutableString *value = [[component substringFromIndex:separator.location+1] mutableCopy]; + + if(key) CFStringTrimWhitespace((__bridge CFMutableStringRef)key); + if(value) CFStringTrimWhitespace((__bridge CFMutableStringRef)value); + + if ([value hasPrefix:@"\""] && [value hasSuffix:@"\""] && [value length] > 2) + { + // Strip quotes from value + [value deleteCharactersInRange:NSMakeRange(0, 1)]; + [value deleteCharactersInRange:NSMakeRange([value length]-1, 1)]; + } + + if(key && value) + { + auth[key] = value; + } + } + } + + return auth; +} +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPStream (XMPPSCRAMSHA1Authentication) + +- (BOOL)supportsSCRAMSHA1Authentication +{ + return [self supportsAuthenticationMechanism:[XMPPSCRAMSHA1Authentication mechanismName]]; +} + +@end diff --git a/Authentication/X-Facebook-Platform/XMPPXFacebookPlatformAuthentication.h b/Authentication/X-Facebook-Platform/XMPPXFacebookPlatformAuthentication.h new file mode 100644 index 0000000..ef0a97a --- /dev/null +++ b/Authentication/X-Facebook-Platform/XMPPXFacebookPlatformAuthentication.h @@ -0,0 +1,56 @@ +#import +#import "XMPPSASLAuthentication.h" +#import "XMPPStream.h" + + +@interface XMPPXFacebookPlatformAuthentication : NSObject + +/** + * You should use this init method (as opposed the one defined in the XMPPSASLAuthentication protocol). +**/ +- (id)initWithStream:(XMPPStream *)stream appId:(NSString *)appId accessToken:(NSString *)accessToken; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPStream (XMPPXFacebookPlatformAuthentication) + +/** + * Facebook Chat X-FACEBOOK-PLATFORM SASL authentication initialization. + * This is a convienence init method to help configure Facebook Chat. +**/ +- (id)initWithFacebookAppId:(NSString *)fbAppId; + +/** + * The appId can be passed to custom authentication classes. + * For example, the appId is used for Facebook Chat X-FACEBOOK-PLATFORM SASL authentication. +**/ +@property (readwrite, copy) NSString *facebookAppId; + +/** + * Returns whether or not the server supports X-FACEBOOK-PLATFORM authentication. + * + * This information is available after the stream is connected. + * In other words, after the delegate has received xmppStreamDidConnect: notification. +**/ +- (BOOL)supportsXFacebookPlatformAuthentication; + +/** + * This method attempts to start the facebook oauth authentication process. + * + * This method is asynchronous. + * + * If there is something immediately wrong, + * such as the stream is not connected or doesn't have a set appId or accessToken, + * the method will return NO and set the error. + * Otherwise the delegate callbacks are used to communicate auth success or failure. + * + * @see xmppStreamDidAuthenticate: + * @see xmppStream:didNotAuthenticate: + **/ +- (BOOL)authenticateWithFacebookAccessToken:(NSString *)accessToken error:(NSError **)errPtr; + +@end diff --git a/Authentication/X-Facebook-Platform/XMPPXFacebookPlatformAuthentication.m b/Authentication/X-Facebook-Platform/XMPPXFacebookPlatformAuthentication.m new file mode 100644 index 0000000..dccc80d --- /dev/null +++ b/Authentication/X-Facebook-Platform/XMPPXFacebookPlatformAuthentication.m @@ -0,0 +1,327 @@ +#import "XMPPXFacebookPlatformAuthentication.h" +#import "XMPP.h" +#import "XMPPLogging.h" +#import "XMPPInternal.h" +#import "NSData+XMPP.h" + +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +static NSString *const XMPPFacebookChatHostName = @"chat.facebook.com"; + +static char facebookAppIdKey; + +@interface XMPPXFacebookPlatformAuthentication () +{ + #if __has_feature(objc_arc_weak) + __weak XMPPStream *xmppStream; + #else + __unsafe_unretained XMPPStream *xmppStream; + #endif + + BOOL awaitingChallenge; + + NSString *appId; + NSString *accessToken; + NSString *nonce; + NSString *method; +} + +- (NSDictionary *)dictionaryFromChallenge:(NSXMLElement *)challenge; +- (NSString *)base64EncodedFullResponse; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPXFacebookPlatformAuthentication + ++ (NSString *)mechanismName +{ + return @"X-FACEBOOK-PLATFORM"; +} + +- (id)initWithStream:(XMPPStream *)stream password:(NSString *)password +{ + if ((self = [super init])) + { + xmppStream = stream; + } + return self; +} + +- (id)initWithStream:(XMPPStream *)stream appId:(NSString *)inAppId accessToken:(NSString *)inAccessToken +{ + if ((self = [super init])) + { + xmppStream = stream; + appId = inAppId; + accessToken = inAccessToken; + } + return self; +} + +- (BOOL)start:(NSError **)errPtr +{ + if (!appId || !accessToken) + { + NSString *errMsg = @"Missing facebook appId and/or accessToken."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + NSError *err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + if (errPtr) *errPtr = err; + return NO; + } + + // + + NSXMLElement *auth = [NSXMLElement elementWithName:@"auth" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + [auth addAttributeWithName:@"mechanism" stringValue:@"X-FACEBOOK-PLATFORM"]; + + [xmppStream sendAuthElement:auth]; + awaitingChallenge = YES; + + return YES; +} + +- (XMPPHandleAuthResponse)handleAuth1:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + // We're expecting a challenge response. + // If we get anything else we're going to assume it's some kind of failure response. + + if (![[authResponse name] isEqualToString:@"challenge"]) + { + return XMPP_AUTH_FAIL; + } + + // Extract components from incoming challenge + + NSDictionary *auth = [self dictionaryFromChallenge:authResponse]; + + nonce = auth[@"nonce"]; + method = auth[@"method"]; + + // Create and send challenge response element + + NSXMLElement *response = [NSXMLElement elementWithName:@"response" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + [response setStringValue:[self base64EncodedFullResponse]]; + + [xmppStream sendAuthElement:response]; + awaitingChallenge = NO; + + return XMPP_AUTH_CONTINUE; +} + +- (XMPPHandleAuthResponse)handleAuth2:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + // We're expecting a success response. + // If we get anything else we can safely assume it's the equivalent of a failure response. + + if ([[authResponse name] isEqualToString:@"success"]) + { + return XMPP_AUTH_SUCCESS; + } + else + { + return XMPP_AUTH_FAIL; + } +} + +- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)authResponse +{ + if (awaitingChallenge) + { + return [self handleAuth1:authResponse]; + } + else + { + return [self handleAuth2:authResponse]; + } +} + +- (NSDictionary *)dictionaryFromChallenge:(NSXMLElement *)challenge +{ + // The value of the challenge stanza is base 64 encoded. + // Once "decoded", it's just a string of key=value pairs separated by ampersands. + + NSData *base64Data = [[challenge stringValue] dataUsingEncoding:NSASCIIStringEncoding]; + NSData *decodedData = [base64Data xmpp_base64Decoded]; + + NSString *authStr = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; + + XMPPLogVerbose(@"%@: decoded challenge: %@", THIS_FILE, authStr); + + NSArray *components = [authStr componentsSeparatedByString:@"&"]; + NSMutableDictionary *auth = [NSMutableDictionary dictionaryWithCapacity:3]; + + for (NSString *component in components) + { + NSRange separator = [component rangeOfString:@"="]; + if (separator.location != NSNotFound) + { + NSString *key = [[component substringToIndex:separator.location] + stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + + NSString *value = [[component substringFromIndex:separator.location+1] + stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + + if ([value hasPrefix:@"\""] && [value hasSuffix:@"\""] && [value length] > 2) + { + // Strip quotes from value + value = [value substringWithRange:NSMakeRange(1,([value length]-2))]; + } + + auth[key] = value; + } + } + + return auth; +} + +- (NSString *)base64EncodedFullResponse +{ + if (!appId || !accessToken || !method || !nonce) + { + return nil; + } + + srand([[NSDate date] timeIntervalSince1970]); + + NSMutableString *buffer = [NSMutableString stringWithCapacity:250]; + [buffer appendFormat:@"method=%@&", method]; + [buffer appendFormat:@"nonce=%@&", nonce]; + [buffer appendFormat:@"access_token=%@&", accessToken]; + [buffer appendFormat:@"api_key=%@&", appId]; + [buffer appendFormat:@"call_id=%d&", rand()]; + [buffer appendFormat:@"v=%@",@"1.0"]; + + XMPPLogVerbose(@"XMPPXFacebookPlatformAuthentication: response for facebook: %@", buffer); + + NSData *utf8data = [buffer dataUsingEncoding:NSUTF8StringEncoding]; + + return [utf8data xmpp_base64Encoded]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPStream (XMPPXFacebookPlatformAuthentication) + +- (id)initWithFacebookAppId:(NSString *)fbAppId +{ + if ((self = [self init])) // Note: Using [self init], NOT [super init] + { + self.facebookAppId = fbAppId; + self.myJID = [XMPPJID jidWithString:XMPPFacebookChatHostName]; + + // As of October 8, 2011, Facebook doesn't have their XMPP SRV records set. + // And, as per the XMPP specification, we MUST check the XMPP SRV records for an IP address, + // before falling back to a traditional A record lookup. + // + // So we're setting the hostname as a minor optimization to avoid the SRV timeout delay. + + self.hostName = XMPPFacebookChatHostName; + } + return self; +} + +- (NSString *)facebookAppId +{ + __block NSString *result = nil; + + dispatch_block_t block = ^{ + result = objc_getAssociatedObject(self, &facebookAppIdKey); + }; + + if (dispatch_get_specific(self.xmppQueueTag)) + block(); + else + dispatch_sync(self.xmppQueue, block); + + return result; +} + +- (void)setFacebookAppId:(NSString *)inFacebookAppId +{ + NSString *newFacebookAppId = [inFacebookAppId copy]; + + dispatch_block_t block = ^{ + objc_setAssociatedObject(self, &facebookAppIdKey, newFacebookAppId, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + }; + + if (dispatch_get_specific(self.xmppQueueTag)) + block(); + else + dispatch_async(self.xmppQueue, block); +} + +- (BOOL)supportsXFacebookPlatformAuthentication +{ + return [self supportsAuthenticationMechanism:[XMPPXFacebookPlatformAuthentication mechanismName]]; +} + +/** + * This method attempts to connect to the Facebook Chat servers + * using the Facebook OAuth token returned by the Facebook OAuth 2.0 authentication process. +**/ +- (BOOL)authenticateWithFacebookAccessToken:(NSString *)accessToken error:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = YES; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if ([self supportsXFacebookPlatformAuthentication]) + { + XMPPXFacebookPlatformAuthentication *facebookAuth = + [[XMPPXFacebookPlatformAuthentication alloc] initWithStream:self + appId:self.facebookAppId + accessToken:accessToken]; + + result = [self authenticate:facebookAuth error:&err]; + } + else + { + NSString *errMsg = @"The server does not support X-FACEBOOK-PLATFORM authentication."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamUnsupportedAction userInfo:info]; + + result = NO; + } + }}; + + if (dispatch_get_specific(self.xmppQueueTag)) + block(); + else + dispatch_sync(self.xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +@end diff --git a/Authentication/X-OAuth2-Google/XMPPXOAuth2Google.h b/Authentication/X-OAuth2-Google/XMPPXOAuth2Google.h new file mode 100644 index 0000000..4720206 --- /dev/null +++ b/Authentication/X-OAuth2-Google/XMPPXOAuth2Google.h @@ -0,0 +1,28 @@ +// +// XMPPXOAuth2Google.h +// Off the Record +// +// Created by David Chiles on 9/13/13. +// Copyright (c) 2013 Chris Ballinger. All rights reserved. +// + +#import +#import "XMPPSASLAuthentication.h" +#import "XMPPStream.h" + +@interface XMPPXOAuth2Google : NSObject + +-(id)initWithStream:(XMPPStream *)stream accessToken:(NSString *)accessToken; + +@end + + + +@interface XMPPStream (XMPPXOAuth2Google) + + +- (BOOL)supportsXOAuth2GoogleAuthentication; + +- (BOOL)authenticateWithGoogleAccessToken:(NSString *)accessToken error:(NSError **)errPtr; + +@end diff --git a/Authentication/X-OAuth2-Google/XMPPXOAuth2Google.m b/Authentication/X-OAuth2-Google/XMPPXOAuth2Google.m new file mode 100644 index 0000000..e07c361 --- /dev/null +++ b/Authentication/X-OAuth2-Google/XMPPXOAuth2Google.m @@ -0,0 +1,180 @@ +// +// XMPPXOAuth2Google.m +// Off the Record +// +// Created by David Chiles on 9/13/13. +// Copyright (c) 2013 Chris Ballinger. All rights reserved. +// + +#import "XMPPXOAuth2Google.h" +#import "XMPP.h" +#import "XMPPLogging.h" +#import "XMPPInternal.h" +#import "NSData+XMPP.h" + +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG +static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO; // | XMPP_LOG_FLAG_TRACE; +#else +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +static NSString *const XMPPGoogleTalkHostName = @"talk.google.com"; + +@interface XMPPXOAuth2Google () +{ +#if __has_feature(objc_arc_weak) + __weak XMPPStream *xmppStream; +#else + __unsafe_unretained XMPPStream *xmppStream; +#endif + + //BOOL awaitingChallenge; + + //NSString *appId; + NSString *accessToken; + //NSString *nonce; + //NSString *method; +} + + + +@end + +@implementation XMPPXOAuth2Google + ++ (NSString *)mechanismName +{ + return @"X-OAUTH2"; +} + +- (id)initWithStream:(XMPPStream *)stream password:(NSString *)password +{ + if ((self = [super init])) + { + xmppStream = stream; + xmppStream.hostName = XMPPGoogleTalkHostName; + } + return self; +} + +-(id)initWithStream:(XMPPStream *)stream accessToken:(NSString *)inAccessToken +{ + if (self = [super init]) { + xmppStream = stream; + accessToken = inAccessToken; + } + return self; +} + +- (BOOL)start:(NSError **)errPtr +{ + if (!accessToken) + { + NSString *errMsg = @"Missing facebook accessToken."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + NSError *err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + if (errPtr) *errPtr = err; + return NO; + } + XMPPLogTrace(); + + // From RFC 4616 - PLAIN SASL Mechanism: + // [authzid] UTF8NUL authcid UTF8NUL passwd + // + // authzid: authorization identity + // authcid: authentication identity (username) + // passwd : password for authcid + + NSString *username = [xmppStream.myJID user]; + + NSString *payload = [NSString stringWithFormat:@"\0%@\0%@", username, accessToken]; + NSString *base64 = [[payload dataUsingEncoding:NSUTF8StringEncoding] xmpp_base64Encoded]; + + // Base-64-Info + + NSXMLElement *auth = [NSXMLElement elementWithName:@"auth" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + [auth addAttributeWithName:@"mechanism" stringValue:@"X-OAUTH2"]; + [auth addAttributeWithName:@"auth:service" stringValue:@"oauth2"]; + [auth addAttributeWithName:@"xmlns:auth" stringValue:@"http://www.google.com/talk/protocol/auth"]; + [auth setStringValue:base64]; + + [xmppStream sendAuthElement:auth]; + + return YES; +} + +- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)authResponse +{ + XMPPLogTrace(); + + // We're expecting a success response. + // If we get anything else we can safely assume it's the equivalent of a failure response. + + if ([[authResponse name] isEqualToString:@"success"]) + { + return XMPP_AUTH_SUCCESS; + } + else + { + return XMPP_AUTH_FAIL; + } +} +@end + +@implementation XMPPStream (XMPPXOAuth2Google) + + + +- (BOOL)supportsXOAuth2GoogleAuthentication +{ + return [self supportsAuthenticationMechanism:[XMPPXOAuth2Google mechanismName]]; +} + +- (BOOL)authenticateWithGoogleAccessToken:(NSString *)accessToken error:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = YES; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if ([self supportsXOAuth2GoogleAuthentication]) + { + XMPPXOAuth2Google * googleAuth = [[XMPPXOAuth2Google alloc] initWithStream:self + accessToken:accessToken]; + + result = [self authenticate:googleAuth error:&err]; + } + else + { + NSString *errMsg = @"The server does not support X-OATH2-GOOGLE authentication."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamUnsupportedAction userInfo:info]; + + result = NO; + } + }}; + + if (dispatch_get_specific(self.xmppQueueTag)) + block(); + else + dispatch_sync(self.xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +@end diff --git a/Authentication/XMPPCustomBinding.h b/Authentication/XMPPCustomBinding.h new file mode 100644 index 0000000..fa83485 --- /dev/null +++ b/Authentication/XMPPCustomBinding.h @@ -0,0 +1,93 @@ +#import +#if TARGET_OS_IPHONE + #import "DDXML.h" +#endif + +typedef NS_ENUM(NSInteger, XMPPBindResult) { + + XMPP_BIND_CONTINUE, // The custom binding process is still ongoing. + + XMPP_BIND_SUCCESS, // Custom binding succeeded. + // The stream should continue normal post-binding operation. + + XMPP_BIND_FAIL_FALLBACK, // Custom binding failed. + // The stream should fallback to the standard binding protocol. + + XMPP_BIND_FAIL_ABORT // Custom binding failed. + // The stream must abort the binding process. + // Further, because the stream is in a bad state (authenticated, but + // unable to complete the full handshake) it must immediately disconnect. + // The given NSError will be reported via xmppStreamDidDisconnect:withError: +}; + +/** + * Binding a JID resource is a standard part of the authentication process, + * and occurs after SASL authentication completes (which generally authenticates the JID username). + * + * This protocol may be used if there is a need to customize the binding process. + * For example: + * + * - Custom SASL authentication scheme required both username & resource + * - Custom SASL authentication scheme provided required resource in server response + * - Stream Management (XEP-0198) replaces binding with resumption from previously bound session + * + * A custom binding procedure may be plugged into an XMPPStream instance via the delegate method: + * - (id )xmppStreamWillBind; +**/ +@protocol XMPPCustomBinding +@required + +/** + * Attempts to start the custom binding process. + * + * If it isn't possible to start the process (perhaps due to missing information), + * this method should return XMPP_BIND_FAIL_FALLBACK or XMPP_BIND_FAIL_ABORT. + * + * (The error message is only used by xmppStream if this method returns XMPP_BIND_FAIL_ABORT.) + * + * If binding isn't needed (for example, because custom SASL authentication already handled it), + * this method should return XMPP_BIND_SUCCESS. + * In this case, xmppStream will immediately move to its post-binding operations. + * + * Otherwise this method should send whatever stanzas are needed to begin the binding process. + * And then return XMPP_BIND_CONTINUE. + * + * This method is called by automatically XMPPStream. + * You MUST NOT invoke this method manually. +**/ +- (XMPPBindResult)start:(NSError **)errPtr; + +/** + * After the custom binding process has started, all incoming xmpp stanzas are routed to this method. + * The method should process the stanza as appropriate, and return the coresponding result. + * If the process is not yet complete, it should return XMPP_BIND_CONTINUE, + * meaning the xmpp stream will continue to forward all incoming xmpp stanzas to this method. + * + * This method is called automatically by XMPPStream. + * You MUST NOT invoke this method manually. +**/ +- (XMPPBindResult)handleBind:(NSXMLElement *)auth withError:(NSError **)errPtr; + +@optional + +/** + * Optionally implement this method to override the default behavior. + * By default behavior, we mean the behavior normally taken by xmppStream, which is: + * + * - IF the server includes in its stream:features + * - AND xmppStream.skipStartSession property is NOT set + * - THEN xmppStream will send the session start request, and await the response before transitioning to authenticated + * + * Thus if you implement this method and return YES, then xmppStream will skip starting a session, + * regardless of the stream:features and the current xmppStream.skipStartSession property value. + * + * If you implement this method and return NO, then xmppStream will follow the default behavior detailed above. + * This means that, even if this method returns NO, the xmppStream may still skip starting a session if + * the server doesn't require it via its stream:features, + * or if the user has explicitly forbidden it via the xmppStream.skipStartSession property. + * + * The default value is NO. +**/ +- (BOOL)shouldSkipStartSessionAfterSuccessfulBinding; + +@end diff --git a/Authentication/XMPPSASLAuthentication.h b/Authentication/XMPPSASLAuthentication.h new file mode 100644 index 0000000..fb78872 --- /dev/null +++ b/Authentication/XMPPSASLAuthentication.h @@ -0,0 +1,102 @@ +#import +#if TARGET_OS_IPHONE + #import "DDXML.h" +#endif + +@class XMPPStream; + + +typedef NS_ENUM(NSInteger, XMPPHandleAuthResponse) { + + XMPP_AUTH_FAIL, // Authentication failed. + // The delegate will be informed via xmppStream:didNotAuthenticate: + + XMPP_AUTH_SUCCESS, // Authentication succeeded. + // The delegate will be informed via xmppStreamDidAuthenticate: + + XMPP_AUTH_CONTINUE, // The authentication process is still ongoing. +}; + + +@protocol XMPPSASLAuthentication +@required + +/** + * Returns the associated mechanism name. + * + * An xmpp server sends a list of supported authentication mechanisms during the xmpp handshake. + * The list looks something like this: + * + * + * + * DIGEST-MD5 + * X-FACEBOOK-PLATFORM + * X-YOUR-CUSTOM-AUTH-SCHEME + * + * + * + * The mechanismName returned should match the value inside the HERE. +**/ ++ (NSString *)mechanismName; + +/** + * Standard init method. + * + * The XMPPStream class natively supports the standard authentication scheme (auth with password). + * If that method is used, then xmppStream will automatically create an authentication instance via this method. + * Which authentication class it chooses is based on the configured authentication priorities, + * and the auth mechanisms supported by the server. + * + * Not all authentication mechanisms will use this init method. + * For example: + * - they require an appId and authToken + * - they require a userName (not related to JID), privilegeLevel, and password + * - they require an eyeScan and voiceFingerprint + * + * In this case, the authentication mechanism class should provide it's own custom init method. + * However it should still implement this method, and then use the start method to notify of errors. +**/ +- (id)initWithStream:(XMPPStream *)stream password:(NSString *)password; + + +/** + * Attempts to start the authentication process. + * The auth mechanism should send whatever stanzas are needed to begin the authentication process. + * + * If it isn't possible to start the authentication process (perhaps due to missing information), + * this method should return NO and set an appropriate error message. + * For example: "X-Custom-Platform authentication requires authToken" + * Otherwise this method should return YES. + * + * This method is called by automatically XMPPStream (via the authenticate: method). + * You should NOT invoke this method manually. +**/ +- (BOOL)start:(NSError **)errPtr; + +/** + * After the authentication process has started, all incoming xmpp stanzas are routed to this method. + * The authentication mechanism should process the stanza as appropriate, and return the coresponding result. + * If the authentication is not yet complete, it should return XMPP_AUTH_CONTINUE, + * meaning the xmpp stream will continue to forward all incoming xmpp stanzas to this method. + * + * This method is called automatically by XMPPStream (via the authenticate: method). + * You should NOT invoke this method manually. +**/ +- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)auth; + +@optional + +/** + * Use this init method if the username used for authentication does not match the user part of the JID. + * If username is nil, the user part of the JID will be used. + * The standard init method uses this init method, passing nil for the username. + **/ +- (id)initWithStream:(XMPPStream *)stream username:(NSString *)username password:(NSString *)password; + +/** + * Optionally implement this method to override the default behavior. + * The default value is YES. +**/ +- (BOOL)shouldResendOpeningNegotiationAfterSuccessfulAuthentication; + +@end diff --git a/Categories/NSData+XMPP.h b/Categories/NSData+XMPP.h new file mode 100644 index 0000000..593abdc --- /dev/null +++ b/Categories/NSData+XMPP.h @@ -0,0 +1,34 @@ +#import + +@interface NSData (XMPP) + +- (NSData *)xmpp_md5Digest; + +- (NSData *)xmpp_sha1Digest; + +- (NSString *)xmpp_hexStringValue; + +- (NSString *)xmpp_base64Encoded; +- (NSData *)xmpp_base64Decoded; + +- (BOOL)xmpp_isJPEG; +- (BOOL)xmpp_isPNG; +- (NSString *)xmpp_imageType; + +@end + +#ifndef XMPP_EXCLUDE_DEPRECATED + +#define XMPP_DEPRECATED($message) __attribute__((deprecated($message))) + +@interface NSData (XMPPDeprecated) +- (NSData *)md5Digest XMPP_DEPRECATED("Use -xmpp_md5Digest"); +- (NSData *)sha1Digest XMPP_DEPRECATED("Use -xmpp_sha1Digest"); +- (NSString *)hexStringValue XMPP_DEPRECATED("Use -xmpp_hexStringValue"); +- (NSString *)base64Encoded XMPP_DEPRECATED("Use -xmpp_base64Encoded"); +- (NSData *)base64Decoded XMPP_DEPRECATED("Use -xmpp_base64Decoded"); +@end + +#undef XMPP_DEPRECATED + +#endif diff --git a/Categories/NSData+XMPP.m b/Categories/NSData+XMPP.m new file mode 100644 index 0000000..18a3eb9 --- /dev/null +++ b/Categories/NSData+XMPP.m @@ -0,0 +1,240 @@ +#import "NSData+XMPP.h" +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation NSData (XMPP) + +static char encodingTable[64] = { + 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', + 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', + 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', + 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' }; + + +- (NSData *)xmpp_md5Digest +{ + unsigned char result[CC_MD5_DIGEST_LENGTH]; + + CC_MD5([self bytes], (CC_LONG)[self length], result); + return [NSData dataWithBytes:result length:CC_MD5_DIGEST_LENGTH]; +} + +- (NSData *)xmpp_sha1Digest +{ + unsigned char result[CC_SHA1_DIGEST_LENGTH]; + + CC_SHA1([self bytes], (CC_LONG)[self length], result); + return [NSData dataWithBytes:result length:CC_SHA1_DIGEST_LENGTH]; +} + +- (NSString *)xmpp_hexStringValue +{ + NSMutableString *stringBuffer = [NSMutableString stringWithCapacity:([self length] * 2)]; + + const unsigned char *dataBuffer = [self bytes]; + int i; + + for (i = 0; i < [self length]; ++i) + { + [stringBuffer appendFormat:@"%02x", (unsigned int)dataBuffer[i]]; + } + + return [stringBuffer copy]; +} + +- (NSString *)xmpp_base64Encoded +{ + const unsigned char *bytes = [self bytes]; + NSMutableString *result = [NSMutableString stringWithCapacity:[self length]]; + unsigned long ixtext = 0; + unsigned long lentext = [self length]; + long ctremaining = 0; + unsigned char inbuf[3], outbuf[4]; + unsigned short i = 0; + unsigned short charsonline = 0, ctcopy = 0; + unsigned long ix = 0; + + while( YES ) + { + ctremaining = lentext - ixtext; + if( ctremaining <= 0 ) break; + + for( i = 0; i < 3; i++ ) { + ix = ixtext + i; + if( ix < lentext ) inbuf[i] = bytes[ix]; + else inbuf [i] = 0; + } + + outbuf [0] = (inbuf [0] & 0xFC) >> 2; + outbuf [1] = ((inbuf [0] & 0x03) << 4) | ((inbuf [1] & 0xF0) >> 4); + outbuf [2] = ((inbuf [1] & 0x0F) << 2) | ((inbuf [2] & 0xC0) >> 6); + outbuf [3] = inbuf [2] & 0x3F; + ctcopy = 4; + + switch( ctremaining ) + { + case 1: + ctcopy = 2; + break; + case 2: + ctcopy = 3; + break; + } + + for( i = 0; i < ctcopy; i++ ) + [result appendFormat:@"%c", encodingTable[outbuf[i]]]; + + for( i = ctcopy; i < 4; i++ ) + [result appendString:@"="]; + + ixtext += 3; + charsonline += 4; + } + + return [NSString stringWithString:result]; +} + +- (NSData *)xmpp_base64Decoded +{ + const unsigned char *bytes = [self bytes]; + NSMutableData *result = [NSMutableData dataWithCapacity:[self length]]; + + unsigned long ixtext = 0; + unsigned long lentext = [self length]; + unsigned char ch = 0; + unsigned char inbuf[4] = {0, 0, 0, 0}; + unsigned char outbuf[3] = {0, 0, 0}; + short i = 0, ixinbuf = 0; + BOOL flignore = NO; + BOOL flendtext = NO; + + while( YES ) + { + if( ixtext >= lentext ) break; + ch = bytes[ixtext++]; + flignore = NO; + + if( ( ch >= 'A' ) && ( ch <= 'Z' ) ) ch = ch - 'A'; + else if( ( ch >= 'a' ) && ( ch <= 'z' ) ) ch = ch - 'a' + 26; + else if( ( ch >= '0' ) && ( ch <= '9' ) ) ch = ch - '0' + 52; + else if( ch == '+' ) ch = 62; + else if( ch == '=' ) flendtext = YES; + else if( ch == '/' ) ch = 63; + else flignore = YES; + + if( ! flignore ) + { + short ctcharsinbuf = 3; + BOOL flbreak = NO; + + if( flendtext ) + { + if( ! ixinbuf ) break; + if( ( ixinbuf == 1 ) || ( ixinbuf == 2 ) ) ctcharsinbuf = 1; + else ctcharsinbuf = 2; + ixinbuf = 3; + flbreak = YES; + } + + inbuf [ixinbuf++] = ch; + + if( ixinbuf == 4 ) + { + ixinbuf = 0; + outbuf [0] = ( inbuf[0] << 2 ) | ( ( inbuf[1] & 0x30) >> 4 ); + outbuf [1] = ( ( inbuf[1] & 0x0F ) << 4 ) | ( ( inbuf[2] & 0x3C ) >> 2 ); + outbuf [2] = ( ( inbuf[2] & 0x03 ) << 6 ) | ( inbuf[3] & 0x3F ); + + for( i = 0; i < ctcharsinbuf; i++ ) + [result appendBytes:&outbuf[i] length:1]; + } + + if( flbreak ) break; + } + } + + return [NSData dataWithData:result]; +} + + +- (BOOL)xmpp_isJPEG +{ + if (self.length > 4) + { + unsigned char buffer[4]; + [self getBytes:&buffer length:4]; + + return buffer[0]==0xff && + buffer[1]==0xd8 && + buffer[2]==0xff && + buffer[3]==0xe0; + } + + return NO; +} + +- (BOOL)xmpp_isPNG +{ + if (self.length > 4) + { + unsigned char buffer[4]; + [self getBytes:&buffer length:4]; + + return buffer[0]==0x89 && + buffer[1]==0x50 && + buffer[2]==0x4e && + buffer[3]==0x47; + } + + return NO; +} + +- (NSString *)xmpp_imageType +{ + NSString *result = nil; + + if([self xmpp_isPNG]) + { + result = @"image/png"; + } + else if([self xmpp_isJPEG]) + { + result = @"image/jpeg"; + } + + return result; +} + +@end + +#ifndef XMPP_EXCLUDE_DEPRECATED + +@implementation NSData (XMPPDeprecated) + +- (NSData *)md5Digest { + return [self xmpp_md5Digest]; +} + +- (NSData *)sha1Digest { + return [self xmpp_sha1Digest]; +} + +- (NSString *)hexStringValue { + return [self xmpp_hexStringValue]; +} + +- (NSString *)base64Encoded { + return [self xmpp_base64Encoded]; +} + +- (NSData *)base64Decoded { + return [self xmpp_base64Decoded]; +} + +@end + +#endif diff --git a/Categories/NSNumber+XMPP.h b/Categories/NSNumber+XMPP.h new file mode 100644 index 0000000..2c4cff6 --- /dev/null +++ b/Categories/NSNumber+XMPP.h @@ -0,0 +1,46 @@ +#import + + +@interface NSNumber (XMPP) + ++ (NSNumber *)xmpp_numberWithPtr:(const void *)ptr; +- (id)xmpp_initWithPtr:(const void *)ptr __attribute__((objc_method_family(init))); + ++ (BOOL)xmpp_parseString:(NSString *)str intoInt32:(int32_t *)pNum; ++ (BOOL)xmpp_parseString:(NSString *)str intoUInt32:(uint32_t *)pNum; + ++ (BOOL)xmpp_parseString:(NSString *)str intoInt64:(int64_t *)pNum; ++ (BOOL)xmpp_parseString:(NSString *)str intoUInt64:(uint64_t *)pNum; + ++ (BOOL)xmpp_parseString:(NSString *)str intoNSInteger:(NSInteger *)pNum; ++ (BOOL)xmpp_parseString:(NSString *)str intoNSUInteger:(NSUInteger *)pNum; + ++ (UInt8)xmpp_extractUInt8FromData:(NSData *)data atOffset:(unsigned int)offset; + ++ (UInt16)xmpp_extractUInt16FromData:(NSData *)data atOffset:(unsigned int)offset andConvertFromNetworkOrder:(BOOL)flag; + ++ (UInt32)xmpp_extractUInt32FromData:(NSData *)data atOffset:(unsigned int)offset andConvertFromNetworkOrder:(BOOL)flag; + +@end + +#ifndef XMPP_EXCLUDE_DEPRECATED + +#define XMPP_DEPRECATED($message) __attribute__((deprecated($message))) + +@interface NSNumber (XMPPDeprecated) ++ (NSNumber *)numberWithPtr:(const void *)ptr XMPP_DEPRECATED("Use +xmpp_numberWithPtr:"); +- (id)initWithPtr:(const void *)ptr XMPP_DEPRECATED("Use -xmpp_initWithPtr:"); ++ (BOOL)parseString:(NSString *)str intoInt32:(int32_t *)pNum XMPP_DEPRECATED("Use +xmpp_parseString:intoInt32:"); ++ (BOOL)parseString:(NSString *)str intoUInt32:(uint32_t *)pNum XMPP_DEPRECATED("Use +xmpp_parseString:intoUInt32:"); ++ (BOOL)parseString:(NSString *)str intoInt64:(int64_t *)pNum XMPP_DEPRECATED("Use +xmpp_parseString:intoInt64:"); ++ (BOOL)parseString:(NSString *)str intoUInt64:(uint64_t *)pNum XMPP_DEPRECATED("Use +xmpp_parseString:intoUInt64:"); ++ (BOOL)parseString:(NSString *)str intoNSInteger:(NSInteger *)pNum XMPP_DEPRECATED("Use +xmpp_parseString:intoNSInteger:"); ++ (BOOL)parseString:(NSString *)str intoNSUInteger:(NSUInteger *)pNum XMPP_DEPRECATED("Use +xmpp_parseString:intoNSUInteger:"); ++ (UInt8)extractUInt8FromData:(NSData *)data atOffset:(unsigned int)offset XMPP_DEPRECATED("Use +xmpp_extractUInt8FromData:atOffset:"); ++ (UInt16)extractUInt16FromData:(NSData *)data atOffset:(unsigned int)offset andConvertFromNetworkOrder:(BOOL)flag XMPP_DEPRECATED("Use +xmpp_extractUInt16FromData:atOffset:andConvertFromNetworkOrder:"); ++ (UInt32)extractUInt32FromData:(NSData *)data atOffset:(unsigned int)offset andConvertFromNetworkOrder:(BOOL)flag XMPP_DEPRECATED("Use +xmpp_extractUInt32FromData:atOffset:andConvertFromNetworkOrder:"); +@end + +#undef XMPP_DEPRECATED + +#endif diff --git a/Categories/NSNumber+XMPP.m b/Categories/NSNumber+XMPP.m new file mode 100644 index 0000000..01bdb91 --- /dev/null +++ b/Categories/NSNumber+XMPP.m @@ -0,0 +1,265 @@ +#import "NSNumber+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation NSNumber (XMPP) + ++ (NSNumber *)xmpp_numberWithPtr:(const void *)ptr +{ + return [[NSNumber alloc] xmpp_initWithPtr:ptr]; +} + +- (id)xmpp_initWithPtr:(const void *)ptr +{ + return [self initWithLong:(long)ptr]; +} + ++ (BOOL)xmpp_parseString:(NSString *)str intoInt32:(int32_t *)pNum +{ + if (str == nil) + { + *pNum = (int32_t)0; + return NO; + } + + errno = 0; + + long result = strtol([str UTF8String], NULL, 10); + + if (LONG_BIT != 32) + { + if (result > INT32_MAX) + { + *pNum = INT32_MAX; + return NO; + } + if (result < INT32_MIN) + { + *pNum = INT32_MIN; + return NO; + } + } + + // From the manpage: + // + // If no conversion could be performed, 0 is returned and the global variable errno is set to EINVAL. + // If an overflow or underflow occurs, errno is set to ERANGE and the function return value is clamped. + // + // Clamped means it will be TYPE_MAX or TYPE_MIN. + // If overflow/underflow occurs, returning a clamped value is more accurate then returning zero. + + *pNum = (int32_t)result; + + if (errno != 0) + return NO; + else + return YES; +} + ++ (BOOL)xmpp_parseString:(NSString *)str intoUInt32:(uint32_t *)pNum +{ + if (str == nil) + { + *pNum = (uint32_t)0; + return NO; + } + + errno = 0; + + unsigned long result = strtoul([str UTF8String], NULL, 10); + + if (LONG_BIT != 32) + { + if (result > UINT32_MAX) + { + *pNum = UINT32_MAX; + return NO; + } + } + + // From the manpage: + // + // If no conversion could be performed, 0 is returned and the global variable errno is set to EINVAL. + // If an overflow or underflow occurs, errno is set to ERANGE and the function return value is clamped. + // + // Clamped means it will be TYPE_MAX or TYPE_MIN. + // If overflow/underflow occurs, returning a clamped value is more accurate then returning zero. + + *pNum = (uint32_t)result; + + if (errno != 0) + return NO; + else + return YES; +} + ++ (BOOL)xmpp_parseString:(NSString *)str intoInt64:(int64_t *)pNum +{ + if (str == nil) + { + *pNum = (int64_t)0; + return NO; + } + + errno = 0; + + // On both 32-bit and 64-bit machines, long long = 64 bit + + *pNum = strtoll([str UTF8String], NULL, 10); + + // From the manpage: + // + // If no conversion could be performed, 0 is returned and the global variable errno is set to EINVAL. + // If an overflow or underflow occurs, errno is set to ERANGE and the function return value is clamped. + // + // Clamped means it will be TYPE_MAX or TYPE_MIN. + // If overflow/underflow occurs, returning a clamped value is more accurate then returning zero. + + if (errno != 0) + return NO; + else + return YES; +} + ++ (BOOL)xmpp_parseString:(NSString *)str intoUInt64:(uint64_t *)pNum +{ + if (str == nil) + { + *pNum = (uint64_t)0; + return NO; + } + + errno = 0; + + // On both 32-bit and 64-bit machines, unsigned long long = 64 bit + + *pNum = strtoull([str UTF8String], NULL, 10); + + // From the manpage: + // + // If no conversion could be performed, 0 is returned and the global variable errno is set to EINVAL. + // If an overflow or underflow occurs, errno is set to ERANGE and the function return value is clamped. + // + // Clamped means it will be TYPE_MAX or TYPE_MIN. + // If overflow/underflow occurs, returning a clamped value is more accurate then returning zero. + + if (errno != 0) + return NO; + else + return YES; +} + ++ (BOOL)xmpp_parseString:(NSString *)str intoNSInteger:(NSInteger *)pNum +{ + if (NSIntegerMax == INT32_MAX) + return [self xmpp_parseString:str intoInt32:(int32_t *)pNum]; + else + return [self xmpp_parseString:str intoInt64:(int64_t *)pNum]; +} + ++ (BOOL)xmpp_parseString:(NSString *)str intoNSUInteger:(NSUInteger *)pNum +{ + if (NSUIntegerMax == UINT32_MAX) + return [self xmpp_parseString:str intoUInt32:(uint32_t *)pNum]; + else + return [self xmpp_parseString:str intoUInt64:(uint64_t *)pNum]; +} + ++ (UInt8)xmpp_extractUInt8FromData:(NSData *)data atOffset:(unsigned int)offset +{ + // 8 bits = 1 byte + + if([data length] < offset + 1) return 0; + + UInt8 *pResult = (UInt8 *)([data bytes] + offset); + UInt8 result = *pResult; + + return result; +} + ++ (UInt16)xmpp_extractUInt16FromData:(NSData *)data atOffset:(unsigned int)offset andConvertFromNetworkOrder:(BOOL)flag +{ + // 16 bits = 2 bytes + + if([data length] < offset + 2) return 0; + + UInt16 *pResult = (UInt16 *)([data bytes] + offset); + UInt16 result = *pResult; + + if(flag) + return ntohs(result); + else + return result; +} + ++ (UInt32)xmpp_extractUInt32FromData:(NSData *)data atOffset:(unsigned int)offset andConvertFromNetworkOrder:(BOOL)flag +{ + // 32 bits = 4 bytes + + if([data length] < offset + 4) return 0; + + UInt32 *pResult = (UInt32 *)([data bytes] + offset); + UInt32 result = *pResult; + + if(flag) + return ntohl(result); + else + return result; +} + +@end + +#ifndef XMPP_EXCLUDE_DEPRECATED + +@implementation NSNumber (XMPPDeprecated) + ++ (NSNumber *)numberWithPtr:(const void *)ptr { + return [self xmpp_numberWithPtr:ptr]; +} + +- (id)initWithPtr:(const void *)ptr { + return [self xmpp_initWithPtr:ptr]; +} + ++ (BOOL)parseString:(NSString *)str intoInt32:(int32_t *)pNum { + return [self xmpp_parseString:str intoInt32:pNum]; +} + ++ (BOOL)parseString:(NSString *)str intoUInt32:(uint32_t *)pNum { + return [self xmpp_parseString:str intoUInt32:pNum]; +} + ++ (BOOL)parseString:(NSString *)str intoInt64:(int64_t *)pNum { + return [self xmpp_parseString:str intoInt64:pNum]; +} + ++ (BOOL)parseString:(NSString *)str intoUInt64:(uint64_t *)pNum { + return [self xmpp_parseString:str intoUInt64:pNum]; +} + ++ (BOOL)parseString:(NSString *)str intoNSInteger:(NSInteger *)pNum { + return [self xmpp_parseString:str intoNSInteger:pNum]; +} + ++ (BOOL)parseString:(NSString *)str intoNSUInteger:(NSUInteger *)pNum { + return [self xmpp_parseString:str intoNSUInteger:pNum]; +} + ++ (UInt8)extractUInt8FromData:(NSData *)data atOffset:(unsigned int)offset { + return [self xmpp_extractUInt8FromData:data atOffset:offset]; +} + ++ (UInt16)extractUInt16FromData:(NSData *)data atOffset:(unsigned int)offset andConvertFromNetworkOrder:(BOOL)flag { + return [self xmpp_extractUInt16FromData:data atOffset:offset andConvertFromNetworkOrder:flag]; +} + ++ (UInt32)extractUInt32FromData:(NSData *)data atOffset:(unsigned int)offset andConvertFromNetworkOrder:(BOOL)flag { + return [self xmpp_extractUInt32FromData:data atOffset:offset andConvertFromNetworkOrder:flag]; +} + +@end + +#endif diff --git a/Categories/NSXMLElement+XMPP.h b/Categories/NSXMLElement+XMPP.h new file mode 100644 index 0000000..addf89a --- /dev/null +++ b/Categories/NSXMLElement+XMPP.h @@ -0,0 +1,157 @@ +#import + +#if TARGET_OS_IPHONE + #import "DDXML.h" +#endif + + +@interface NSXMLElement (XMPP) + +/** + * Convenience methods for Creating elements. +**/ + ++ (NSXMLElement *)elementWithName:(NSString *)name numberValue:(NSNumber *)number; +- (id)initWithName:(NSString *)name numberValue:(NSNumber *)number; + ++ (NSXMLElement *)elementWithName:(NSString *)name objectValue:(id)objectValue; +- (id)initWithName:(NSString *)name objectValue:(id)objectValue; + +/** + * Creating elements with explicit xmlns values. + * + * Use these instead of [NSXMLElement initWithName:URI:]. + * The category methods below are more readable, and they actually work. +**/ + ++ (NSXMLElement *)elementWithName:(NSString *)name xmlns:(NSString *)ns; +- (id)initWithName:(NSString *)name xmlns:(NSString *)ns; + +/** + * Extracting multiple elements. +**/ + +- (NSArray *)elementsForXmlns:(NSString *)ns; +- (NSArray *)elementsForXmlnsPrefix:(NSString *)nsPrefix; + +/** + * Extracting a single element. +**/ + +- (NSXMLElement *)elementForName:(NSString *)name; +- (NSXMLElement *)elementForName:(NSString *)name xmlns:(NSString *)xmlns; +- (NSXMLElement *)elementForName:(NSString *)name xmlnsPrefix:(NSString *)xmlnsPrefix; + +/** + * Convenience methods for removing child elements. + * + * If the element doesn't exist, these methods do nothing. +**/ + +- (void)removeElementForName:(NSString *)name; +- (void)removeElementsForName:(NSString *)name; +- (void)removeElementForName:(NSString *)name xmlns:(NSString *)xmlns; +- (void)removeElementForName:(NSString *)name xmlnsPrefix:(NSString *)xmlnsPrefix; + +/** + * Working with the common xmpp xmlns value. + * + * Use these instead of getting/setting the URI. + * The category methods below are more readable, and they actually work. +**/ + +- (NSString *)xmlns; +- (void)setXmlns:(NSString *)ns; + +/** + * Convenience methods for printing xml elements with different styles. +**/ + +- (NSString *)prettyXMLString; +- (NSString *)compactXMLString; + +/** + * Convenience methods for adding attributes. +**/ + +- (void)addAttributeWithName:(NSString *)name intValue:(int)intValue; +- (void)addAttributeWithName:(NSString *)name boolValue:(BOOL)boolValue; +- (void)addAttributeWithName:(NSString *)name floatValue:(float)floatValue; +- (void)addAttributeWithName:(NSString *)name doubleValue:(double)doubleValue; +- (void)addAttributeWithName:(NSString *)name integerValue:(NSInteger)integerValue; +- (void)addAttributeWithName:(NSString *)name unsignedIntegerValue:(NSUInteger)unsignedIntegerValue; +- (void)addAttributeWithName:(NSString *)name stringValue:(NSString *)string; +- (void)addAttributeWithName:(NSString *)name numberValue:(NSNumber *)number; +- (void)addAttributeWithName:(NSString *)name objectValue:(id)objectValue; + +/** + * Convenience methods for extracting attribute values in different formats. + * + * E.g. // float price = [beer attributeFloatValueForName:@"price"]; +**/ + +- (int)attributeIntValueForName:(NSString *)name; +- (BOOL)attributeBoolValueForName:(NSString *)name; +- (float)attributeFloatValueForName:(NSString *)name; +- (double)attributeDoubleValueForName:(NSString *)name; +- (int32_t)attributeInt32ValueForName:(NSString *)name; +- (uint32_t)attributeUInt32ValueForName:(NSString *)name; +- (int64_t)attributeInt64ValueForName:(NSString *)name; +- (uint64_t)attributeUInt64ValueForName:(NSString *)name; +- (NSInteger)attributeIntegerValueForName:(NSString *)name; +- (NSUInteger)attributeUnsignedIntegerValueForName:(NSString *)name; +- (NSString *)attributeStringValueForName:(NSString *)name; +- (NSNumber *)attributeNumberIntValueForName:(NSString *)name; +- (NSNumber *)attributeNumberBoolValueForName:(NSString *)name; +- (NSNumber *)attributeNumberFloatValueForName:(NSString *)name; +- (NSNumber *)attributeNumberDoubleValueForName:(NSString *)name; +- (NSNumber *)attributeNumberInt32ValueForName:(NSString *)name; +- (NSNumber *)attributeNumberUInt32ValueForName:(NSString *)name; +- (NSNumber *)attributeNumberInt64ValueForName:(NSString *)name; +- (NSNumber *)attributeNumberUInt64ValueForName:(NSString *)name; +- (NSNumber *)attributeNumberIntegerValueForName:(NSString *)name; +- (NSNumber *)attributeNumberUnsignedIntegerValueForName:(NSString *)name; + +- (int)attributeIntValueForName:(NSString *)name withDefaultValue:(int)defaultValue; +- (BOOL)attributeBoolValueForName:(NSString *)name withDefaultValue:(BOOL)defaultValue; +- (float)attributeFloatValueForName:(NSString *)name withDefaultValue:(float)defaultValue; +- (double)attributeDoubleValueForName:(NSString *)name withDefaultValue:(double)defaultValue; +- (int32_t)attributeInt32ValueForName:(NSString *)name withDefaultValue:(int32_t)defaultValue; +- (uint32_t)attributeUInt32ValueForName:(NSString *)name withDefaultValue:(uint32_t)defaultValue; +- (int64_t)attributeInt64ValueForName:(NSString *)name withDefaultValue:(int64_t)defaultValue; +- (uint64_t)attributeUInt64ValueForName:(NSString *)name withDefaultValue:(uint64_t)defaultValue; +- (NSInteger)attributeIntegerValueForName:(NSString *)name withDefaultValue:(NSInteger)defaultValue; +- (NSUInteger)attributeUnsignedIntegerValueForName:(NSString *)name withDefaultValue:(NSUInteger)defaultValue; +- (NSString *)attributeStringValueForName:(NSString *)name withDefaultValue:(NSString *)defaultValue; +- (NSNumber *)attributeNumberIntValueForName:(NSString *)name withDefaultValue:(int)defaultValue; +- (NSNumber *)attributeNumberBoolValueForName:(NSString *)name withDefaultValue:(BOOL)defaultValue; + +- (NSMutableDictionary *)attributesAsDictionary; + +/** + * Convenience methods for extracting element values in different formats. + * + * E.g. 9.99 // float price = [priceElement stringValueAsFloat]; +**/ + +- (int)stringValueAsInt; +- (BOOL)stringValueAsBool; +- (float)stringValueAsFloat; +- (double)stringValueAsDouble; +- (int32_t)stringValueAsInt32; +- (uint32_t)stringValueAsUInt32; +- (int64_t)stringValueAsInt64; +- (uint64_t)stringValueAsUInt64; +- (NSInteger)stringValueAsNSInteger; +- (NSUInteger)stringValueAsNSUInteger; + +/** + * Working with namespaces. +**/ + +- (void)addNamespaceWithPrefix:(NSString *)prefix stringValue:(NSString *)string; + +- (NSString *)namespaceStringValueForPrefix:(NSString *)prefix; +- (NSString *)namespaceStringValueForPrefix:(NSString *)prefix withDefaultValue:(NSString *)defaultValue; + +@end diff --git a/Categories/NSXMLElement+XMPP.m b/Categories/NSXMLElement+XMPP.m new file mode 100644 index 0000000..1daaf2e --- /dev/null +++ b/Categories/NSXMLElement+XMPP.m @@ -0,0 +1,663 @@ +#import "NSXMLElement+XMPP.h" +#import "NSNumber+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +@implementation NSXMLElement (XMPP) + +/** + * Convenience methods for Creating elements. +**/ + ++ (NSXMLElement *)elementWithName:(NSString *)name numberValue:(NSNumber *)number +{ + return [self elementWithName:name stringValue:[number stringValue]]; +} + +- (id)initWithName:(NSString *)name numberValue:(NSNumber *)number +{ + return [self initWithName:name stringValue:[number stringValue]]; +} + ++ (NSXMLElement *)elementWithName:(NSString *)name objectValue:(id)objectValue +{ + if([objectValue isKindOfClass:[NSString class]]) + { + return [self elementWithName:name stringValue:objectValue]; + } + else if([objectValue isKindOfClass:[NSNumber class]]) + { + return [self elementWithName:name numberValue:objectValue]; + } + else if([objectValue respondsToSelector:@selector(stringValue)]) + { + return [self elementWithName:name stringValue:[objectValue stringValue]]; + } + else + { + return [self elementWithName:name]; + } +} + +- (id)initWithName:(NSString *)name objectValue:(id)objectValue +{ + if([objectValue isKindOfClass:[NSString class]]) + { + return [self initWithName:name stringValue:objectValue]; + } + else if([objectValue isKindOfClass:[NSNumber class]]) + { + return [self initWithName:name numberValue:objectValue]; + } + else if([objectValue respondsToSelector:@selector(stringValue)]) + { + return [self initWithName:name stringValue:[objectValue stringValue]]; + } + else + { + return [self initWithName:name]; + } +} + +/** + * Quick method to create an element +**/ ++ (NSXMLElement *)elementWithName:(NSString *)name xmlns:(NSString *)ns +{ + NSXMLElement *element = [NSXMLElement elementWithName:name]; + [element setXmlns:ns]; + return element; +} + +- (id)initWithName:(NSString *)name xmlns:(NSString *)ns +{ + if ((self = [self initWithName:name])) + { + [self setXmlns:ns]; + } + return self; +} + +- (NSArray *)elementsForXmlns:(NSString *)ns +{ + NSMutableArray *elements = [NSMutableArray array]; + + for (NSXMLNode *node in [self children]) + { + if ([node isKindOfClass:[NSXMLElement class]]) + { + NSXMLElement *element = (NSXMLElement *)node; + + if ([[element xmlns] isEqual:ns]) + { + [elements addObject:element]; + } + } + } + + return elements; +} + +- (NSArray *)elementsForXmlnsPrefix:(NSString *)nsPrefix +{ + NSMutableArray *elements = [NSMutableArray array]; + + for (NSXMLNode *node in [self children]) + { + if ([node isKindOfClass:[NSXMLElement class]]) + { + NSXMLElement *element = (NSXMLElement *)node; + + if ([[element xmlns] hasPrefix:nsPrefix]) + { + [elements addObject:element]; + } + } + } + + return elements; +} + +/** + * This method returns the first child element for the given name (as an NSXMLElement). + * If no child elements exist for the given name, nil is returned. +**/ +- (NSXMLElement *)elementForName:(NSString *)name +{ + NSArray *elements = [self elementsForName:name]; + if ([elements count] > 0) + { + return elements[0]; + } + else + { + // There is a bug in the NSXMLElement elementsForName: method. + // Consider the following XML fragment: + // + // + // + // + // + // Calling [query elementsForName:@"x"] results in an empty array! + // + // However, it will work properly if you use the following: + // [query elementsForLocalName:@"x" URI:@"some:other:namespace"] + // + // The trouble with this is that we may not always know the xmlns in advance, + // so in this particular case there is no way to access the element without looping through the children. + // + // This bug was submitted to apple on June 1st, 2007 and was classified as "serious". + // + // --!!-- This bug does NOT exist in DDXML --!!-- + + return nil; + } +} + +/** + * This method returns the first child element for the given name and given xmlns (as an NSXMLElement). + * If no child elements exist for the given name and given xmlns, nil is returned. +**/ +- (NSXMLElement *)elementForName:(NSString *)name xmlns:(NSString *)xmlns +{ + NSArray *elements = [self elementsForLocalName:name URI:xmlns]; + if ([elements count] > 0) + { + return elements[0]; + } + else + { + return nil; + } +} + +- (NSXMLElement *)elementForName:(NSString *)name xmlnsPrefix:(NSString *)xmlnsPrefix{ + + NSXMLElement *result = nil; + + for (NSXMLNode *node in [self children]) + { + if ([node isKindOfClass:[NSXMLElement class]]) + { + NSXMLElement *element = (NSXMLElement *)node; + + if ([[element name] isEqualToString:name] && [[element xmlns] hasPrefix:xmlnsPrefix]) + { + result = element; + break; + } + } + } + + return result; +} + +/** + * This method removes the first child element for the given name. + * If no child elements exist for the given name, this method does nothing. +**/ +- (void)removeElementForName:(NSString *)name +{ + NSXMLElement *element = [self elementForName:name]; + + if(element) + { + [self removeChildAtIndex:[[self children] indexOfObject:element]]; + } +} + +/** + * This method removes the all child elements for the given name. + * If no child elements exist for the given name, this method does nothing. +**/ +- (void)removeElementsForName:(NSString *)name +{ + NSArray *elements = [self elementsForName:name]; + + for(NSXMLElement *element in elements) + { + [self removeChildAtIndex:[[self children] indexOfObject:element]]; + } +} + +/** + * This method removes the first child element for the given name and given xmlns. + * If no child elements exist for the given name and given xmlns, this method does nothing. +**/ +- (void)removeElementForName:(NSString *)name xmlns:(NSString *)xmlns +{ + NSXMLElement *element = [self elementForName:name xmlns:xmlns]; + + if(element) + { + [self removeChildAtIndex:[[self children] indexOfObject:element]]; + } +} + +/** + * This method removes the first child element for the given name and given xmlns prefix. + * If no child elements exist for the given name and given xmlns prefix, this method does nothing. +**/ +- (void)removeElementForName:(NSString *)name xmlnsPrefix:(NSString *)xmlnsPrefix +{ + NSXMLElement *element = [self elementForName:name xmlnsPrefix:xmlnsPrefix]; + + if(element) + { + [self removeChildAtIndex:[[self children] indexOfObject:element]]; + } +} + +/** + * Returns the common xmlns "attribute", which is only accessible via the namespace methods. + * The xmlns value is often used in jabber elements. +**/ +- (NSString *)xmlns +{ + return [[self namespaceForPrefix:@""] stringValue]; +} + +- (void)setXmlns:(NSString *)ns +{ + // If we use setURI: then the xmlns won't be displayed in the XMLString. + // Adding the namespace this way works properly. + + [self addNamespace:[NSXMLNode namespaceWithName:@"" stringValue:ns]]; +} + +/** + * Shortcut to get a pretty (formatted) string representation of the element. +**/ +- (NSString *)prettyXMLString +{ + return [self XMLStringWithOptions:(NSXMLNodePrettyPrint | NSXMLNodeCompactEmptyElement)]; +} + +/** + * Shortcut to get a compact string representation of the element. +**/ +- (NSString *)compactXMLString +{ + return [self XMLStringWithOptions:NSXMLNodeCompactEmptyElement]; +} + +/** + * Shortcut to avoid having to use NSXMLNode everytime +**/ + +- (void)addAttributeWithName:(NSString *)name intValue:(int)intValue +{ + [self addAttributeWithName:name numberValue:@(intValue)]; +} + +- (void)addAttributeWithName:(NSString *)name boolValue:(BOOL)boolValue +{ + [self addAttributeWithName:name numberValue:@(boolValue)]; +} + +- (void)addAttributeWithName:(NSString *)name floatValue:(float)floatValue +{ + [self addAttributeWithName:name numberValue:@(floatValue)]; +} + +- (void)addAttributeWithName:(NSString *)name doubleValue:(double)doubleValue +{ + [self addAttributeWithName:name numberValue:@(doubleValue)]; +} + +- (void)addAttributeWithName:(NSString *)name integerValue:(NSInteger)integerValue +{ + [self addAttributeWithName:name numberValue:@(integerValue)]; +} + +- (void)addAttributeWithName:(NSString *)name unsignedIntegerValue:(NSUInteger)unsignedIntegerValue +{ + [self addAttributeWithName:name numberValue:@(unsignedIntegerValue)]; +} + +- (void)addAttributeWithName:(NSString *)name stringValue:(NSString *)string +{ + [self addAttribute:[NSXMLNode attributeWithName:name stringValue:string]]; +} + +- (void)addAttributeWithName:(NSString *)name numberValue:(NSNumber *)number +{ + [self addAttributeWithName:name stringValue:[number stringValue]]; +} + +- (void)addAttributeWithName:(NSString *)name objectValue:(id)objectValue +{ + if([objectValue isKindOfClass:[NSString class]]) + { + [self addAttributeWithName:name stringValue:objectValue]; + } + else if([objectValue isKindOfClass:[NSNumber class]]) + { + [self addAttributeWithName:name numberValue:objectValue]; + } + else if([objectValue respondsToSelector:@selector(stringValue)]) + { + [self addAttributeWithName:name stringValue:[objectValue stringValue]]; + } +} + +/** + * The following methods return the corresponding value of the attribute with the given name. +**/ + +- (int)attributeIntValueForName:(NSString *)name +{ + return [[self attributeStringValueForName:name] intValue]; +} +- (BOOL)attributeBoolValueForName:(NSString *)name +{ + NSString *attributeStringValueForName = [self attributeStringValueForName:name]; + + BOOL result = NO; + + // An XML boolean datatype can have the following legal literals: true, false, 1, 0 + + if ([attributeStringValueForName isEqualToString:@"true"] || [attributeStringValueForName isEqualToString:@"1"]) + { + result = YES; + } + else if([attributeStringValueForName isEqualToString:@"false"] || [attributeStringValueForName isEqualToString:@"0"]) + { + result = NO; + } + else + { + result = [attributeStringValueForName boolValue]; + } + + return result; +} +- (float)attributeFloatValueForName:(NSString *)name +{ + return [[self attributeStringValueForName:name] floatValue]; +} +- (double)attributeDoubleValueForName:(NSString *)name +{ + return [[self attributeStringValueForName:name] doubleValue]; +} +- (int32_t)attributeInt32ValueForName:(NSString *)name +{ + int32_t result = 0; + [NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoInt32:&result]; + return result; +} +- (uint32_t)attributeUInt32ValueForName:(NSString *)name +{ + uint32_t result = 0; + [NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoUInt32:&result]; + return result; +} +- (int64_t)attributeInt64ValueForName:(NSString *)name +{ + int64_t result; + [NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoInt64:&result]; + return result; +} +- (uint64_t)attributeUInt64ValueForName:(NSString *)name +{ + uint64_t result; + [NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoUInt64:&result]; + return result; +} +- (NSInteger)attributeIntegerValueForName:(NSString *)name +{ + NSInteger result; + [NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoNSInteger:&result]; + return result; +} +- (NSUInteger)attributeUnsignedIntegerValueForName:(NSString *)name +{ + NSUInteger result = 0; + [NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoNSUInteger:&result]; + return result; +} +- (NSString *)attributeStringValueForName:(NSString *)name +{ + return [[self attributeForName:name] stringValue]; +} +- (NSNumber *)attributeNumberIntValueForName:(NSString *)name +{ + return @([self attributeIntValueForName:name]); +} +- (NSNumber *)attributeNumberBoolValueForName:(NSString *)name +{ + return @([self attributeBoolValueForName:name]); +} +- (NSNumber *)attributeNumberFloatValueForName:(NSString *)name +{ + return @([self attributeFloatValueForName:name]); +} +- (NSNumber *)attributeNumberDoubleValueForName:(NSString *)name +{ + return @([self attributeDoubleValueForName:name]); +} +- (NSNumber *)attributeNumberInt32ValueForName:(NSString *)name +{ + return @([self attributeInt32ValueForName:name]); +} +- (NSNumber *)attributeNumberUInt32ValueForName:(NSString *)name +{ + return @([self attributeUInt32ValueForName:name]); +} +- (NSNumber *)attributeNumberInt64ValueForName:(NSString *)name +{ + return @([self attributeInt64ValueForName:name]); +} +- (NSNumber *)attributeNumberUInt64ValueForName:(NSString *)name +{ + return @([self attributeUInt64ValueForName:name]); +} +- (NSNumber *)attributeNumberIntegerValueForName:(NSString *)name +{ + return @([self attributeIntegerValueForName:name]); +} +- (NSNumber *)attributeNumberUnsignedIntegerValueForName:(NSString *)name +{ + return @([self attributeUnsignedIntegerValueForName:name]); +} + +/** + * The following methods return the corresponding value of the attribute with the given name. + * If the attribute does not exist, the given defaultValue is returned. +**/ + +- (int)attributeIntValueForName:(NSString *)name withDefaultValue:(int)defaultValue +{ + NSXMLNode *attr = [self attributeForName:name]; + return (attr) ? [[attr stringValue] intValue] : defaultValue; +} +- (BOOL)attributeBoolValueForName:(NSString *)name withDefaultValue:(BOOL)defaultValue +{ + NSXMLNode *attr = [self attributeForName:name]; + return (attr) ? [[attr stringValue] boolValue] : defaultValue; +} +- (float)attributeFloatValueForName:(NSString *)name withDefaultValue:(float)defaultValue +{ + NSXMLNode *attr = [self attributeForName:name]; + return (attr) ? [[attr stringValue] floatValue] : defaultValue; +} +- (double)attributeDoubleValueForName:(NSString *)name withDefaultValue:(double)defaultValue +{ + NSXMLNode *attr = [self attributeForName:name]; + return (attr) ? [[attr stringValue] doubleValue] : defaultValue; +} +- (int32_t)attributeInt32ValueForName:(NSString *)name withDefaultValue:(int32_t)defaultValue +{ + int32_t result = 0; + if ([NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoInt32:&result]) + return result; + else + return defaultValue; +} +- (uint32_t)attributeUInt32ValueForName:(NSString *)name withDefaultValue:(uint32_t)defaultValue +{ + uint32_t result = 0; + if ([NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoUInt32:&result]) + return result; + else + return defaultValue; +} +- (int64_t)attributeInt64ValueForName:(NSString *)name withDefaultValue:(int64_t)defaultValue +{ + int64_t result = 0; + if ([NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoInt64:&result]) + return result; + else + return defaultValue; +} +- (uint64_t)attributeUInt64ValueForName:(NSString *)name withDefaultValue:(uint64_t)defaultValue +{ + uint64_t result = 0; + if ([NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoUInt64:&result]) + return result; + else + return defaultValue; +} +- (NSInteger)attributeIntegerValueForName:(NSString *)name withDefaultValue:(NSInteger)defaultValue +{ + NSInteger result = 0; + if ([NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoNSInteger:&result]) + return result; + else + return defaultValue; +} +- (NSUInteger)attributeUnsignedIntegerValueForName:(NSString *)name withDefaultValue:(NSUInteger)defaultValue +{ + NSUInteger result = 0; + if ([NSNumber xmpp_parseString:[self attributeStringValueForName:name] intoNSUInteger:&result]) + return result; + else + return defaultValue; +} +- (NSString *)attributeStringValueForName:(NSString *)name withDefaultValue:(NSString *)defaultValue +{ + NSXMLNode *attr = [self attributeForName:name]; + return (attr) ? [attr stringValue] : defaultValue; +} +- (NSNumber *)attributeNumberIntValueForName:(NSString *)name withDefaultValue:(int)defaultValue +{ + return @([self attributeIntValueForName:name withDefaultValue:defaultValue]); +} +- (NSNumber *)attributeNumberBoolValueForName:(NSString *)name withDefaultValue:(BOOL)defaultValue +{ + return @([self attributeBoolValueForName:name withDefaultValue:defaultValue]); +} + +/** + * Returns all the attributes in a dictionary. +**/ +- (NSMutableDictionary *)attributesAsDictionary +{ + NSArray *attributes = [self attributes]; + NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:[attributes count]]; + + NSUInteger i; + for(i = 0; i < [attributes count]; i++) + { + NSXMLNode *node = attributes[i]; + + result[[node name]] = [node stringValue]; + } + return result; +} + +/** + * The following methods return the corresponding value of the node. +**/ + +- (int)stringValueAsInt +{ + return [[self stringValue] intValue]; +} +- (BOOL)stringValueAsBool +{ + return [[self stringValue] boolValue]; +} +- (float)stringValueAsFloat +{ + return [[self stringValue] floatValue]; +} +- (double)stringValueAsDouble +{ + return [[self stringValue] doubleValue]; +} +- (int32_t)stringValueAsInt32 +{ + int32_t result; + if ([NSNumber xmpp_parseString:[self stringValue] intoInt32:&result]) + return result; + else + return 0; +} +- (uint32_t)stringValueAsUInt32 +{ + uint32_t result; + if ([NSNumber xmpp_parseString:[self stringValue] intoUInt32:&result]) + return result; + else + return 0; +} +- (int64_t)stringValueAsInt64 +{ + int64_t result = 0; + if ([NSNumber xmpp_parseString:[self stringValue] intoInt64:&result]) + return result; + else + return 0; +} +- (uint64_t)stringValueAsUInt64 +{ + uint64_t result = 0; + if ([NSNumber xmpp_parseString:[self stringValue] intoUInt64:&result]) + return result; + else + return 0; +} +- (NSInteger)stringValueAsNSInteger +{ + NSInteger result = 0; + if ([NSNumber xmpp_parseString:[self stringValue] intoNSInteger:&result]) + return result; + else + return 0; +} +- (NSUInteger)stringValueAsNSUInteger +{ + NSUInteger result = 0; + if ([NSNumber xmpp_parseString:[self stringValue] intoNSUInteger:&result]) + return result; + else + return 0; +} + +/** + * Shortcut to avoid having to use NSXMLNode everytime +**/ +- (void)addNamespaceWithPrefix:(NSString *)prefix stringValue:(NSString *)string +{ + [self addNamespace:[NSXMLNode namespaceWithName:prefix stringValue:string]]; +} + +/** + * Just to make your code look a little bit cleaner. +**/ + +- (NSString *)namespaceStringValueForPrefix:(NSString *)prefix +{ + return [[self namespaceForPrefix:prefix] stringValue]; +} + +- (NSString *)namespaceStringValueForPrefix:(NSString *)prefix withDefaultValue:(NSString *)defaultValue +{ + NSXMLNode *namespace = [self namespaceForPrefix:prefix]; + return (namespace) ? [namespace stringValue] : defaultValue; +} + +@end diff --git a/Core/XMPP.h b/Core/XMPP.h new file mode 100644 index 0000000..eae4465 --- /dev/null +++ b/Core/XMPP.h @@ -0,0 +1,31 @@ +// +// Core classes +// + +#import "XMPPJID.h" +#import "XMPPStream.h" +#import "XMPPElement.h" +#import "XMPPIQ.h" +#import "XMPPMessage.h" +#import "XMPPPresence.h" +#import "XMPPModule.h" + +// +// Authentication +// + +#import "XMPPSASLAuthentication.h" +#import "XMPPCustomBinding.h" +#import "XMPPDigestMD5Authentication.h" +#import "XMPPSCRAMSHA1Authentication.h" +#import "XMPPPlainAuthentication.h" +#import "XMPPXFacebookPlatformAuthentication.h" +#import "XMPPAnonymousAuthentication.h" +#import "XMPPDeprecatedPlainAuthentication.h" +#import "XMPPDeprecatedDigestAuthentication.h" + +// +// Categories +// + +#import "NSXMLElement+XMPP.h" diff --git a/Core/XMPPConstants.h b/Core/XMPPConstants.h new file mode 100644 index 0000000..63e94e0 --- /dev/null +++ b/Core/XMPPConstants.h @@ -0,0 +1,16 @@ +#import + +/** +* This class is provided to house various namespaces that are reused throughout +* the project. Feel free to add to the constants as you see necessary. If a +* particular namespace is only applicable to a particular extension, then it +* should be inside that extension rather than here. +*/ + +extern NSString *const XMPPSINamespace; +extern NSString *const XMPPSIProfileFileTransferNamespace; +extern NSString *const XMPPFeatureNegNamespace; +extern NSString *const XMPPBytestreamsNamespace; +extern NSString *const XMPPIBBNamespace; +extern NSString *const XMPPDiscoItemsNamespace; +extern NSString *const XMPPDiscoInfoNamespace; diff --git a/Core/XMPPConstants.m b/Core/XMPPConstants.m new file mode 100644 index 0000000..d341b56 --- /dev/null +++ b/Core/XMPPConstants.m @@ -0,0 +1,10 @@ +#import "XMPPConstants.h" + +NSString *const XMPPSINamespace = @"http://jabber.org/protocol/si"; +NSString *const XMPPSIProfileFileTransferNamespace = + @"http://jabber.org/protocol/si/profile/file-transfer"; +NSString *const XMPPFeatureNegNamespace = @"http://jabber.org/protocol/feature-neg"; +NSString *const XMPPBytestreamsNamespace = @"http://jabber.org/protocol/bytestreams"; +NSString *const XMPPIBBNamespace = @"http://jabber.org/protocol/ibb"; +NSString *const XMPPDiscoItemsNamespace = @"http://jabber.org/protocol/disco#items"; +NSString *const XMPPDiscoInfoNamespace = @"http://jabber.org/protocol/disco#info"; diff --git a/Core/XMPPElement.h b/Core/XMPPElement.h new file mode 100644 index 0000000..4c02e49 --- /dev/null +++ b/Core/XMPPElement.h @@ -0,0 +1,44 @@ +#import +#import "XMPPJID.h" + +#if TARGET_OS_IPHONE + #import "DDXML.h" +#endif + + +/** + * The XMPPElement provides the base class for XMPPIQ, XMPPMessage & XMPPPresence. + * + * This class extends NSXMLElement. + * The NSXML classes (NSXMLElement & NSXMLNode) provide a full-featured library for working with XML elements. + * + * On the iPhone, the KissXML library provides a drop-in replacement for Apple's NSXML classes. +**/ + +@interface XMPPElement : NSXMLElement + +#pragma mark Common Jabber Methods + +- (NSString *)elementID; + +- (XMPPJID *)to; +- (XMPPJID *)from; + +- (NSString *)toStr; +- (NSString *)fromStr; + +#pragma mark To and From Methods + +- (BOOL)isTo:(XMPPJID *)to; +- (BOOL)isTo:(XMPPJID *)to options:(XMPPJIDCompareOptions)mask; + +- (BOOL)isFrom:(XMPPJID *)from; +- (BOOL)isFrom:(XMPPJID *)from options:(XMPPJIDCompareOptions)mask; + +- (BOOL)isToOrFrom:(XMPPJID *)toOrFrom; +- (BOOL)isToOrFrom:(XMPPJID *)toOrFrom options:(XMPPJIDCompareOptions)mask; + +- (BOOL)isTo:(XMPPJID *)to from:(XMPPJID *)from; +- (BOOL)isTo:(XMPPJID *)to from:(XMPPJID *)from options:(XMPPJIDCompareOptions)mask; + +@end diff --git a/Core/XMPPElement.m b/Core/XMPPElement.m new file mode 100644 index 0000000..55a1113 --- /dev/null +++ b/Core/XMPPElement.m @@ -0,0 +1,192 @@ +#import "XMPPElement.h" +#import "XMPPJID.h" +#import "NSXMLElement+XMPP.h" + +#import + + +@implementation XMPPElement + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Encoding, Decoding +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if ! TARGET_OS_IPHONE +- (id)replacementObjectForPortCoder:(NSPortCoder *)encoder +{ + if([encoder isBycopy]) + return self; + else + return [super replacementObjectForPortCoder:encoder]; + // return [NSDistantObject proxyWithLocal:self connection:[encoder connection]]; +} +#endif + +- (id)initWithCoder:(NSCoder *)coder +{ + NSString *xmlString; + if([coder allowsKeyedCoding]) + { + if([coder respondsToSelector:@selector(requiresSecureCoding)] && + [coder requiresSecureCoding]) + { + xmlString = [coder decodeObjectOfClass:[NSString class] forKey:@"xmlString"]; + } + else + { + xmlString = [coder decodeObjectForKey:@"xmlString"]; + } + } + else + { + xmlString = [coder decodeObject]; + } + + // The method [super initWithXMLString:error:] may return a different self. + // In other words, it may [self release], and alloc/init/return a new self. + // + // So to maintain the proper class (XMPPIQ, XMPPMessage, XMPPPresence, etc) + // we need to get a reference to the class before invoking super. + + Class selfClass = [self class]; + + if ((self = [super initWithXMLString:xmlString error:nil])) + { + object_setClass(self, selfClass); + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + NSString *xmlString = [self compactXMLString]; + + if([coder allowsKeyedCoding]) + { + [coder encodeObject:xmlString forKey:@"xmlString"]; + } + else + { + [coder encodeObject:xmlString]; + } +} + ++ (BOOL) supportsSecureCoding +{ + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Copying +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)copyWithZone:(NSZone *)zone +{ + NSXMLElement *elementCopy = [super copyWithZone:zone]; + object_setClass(elementCopy, [self class]); + + return elementCopy; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Common Jabber Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSString *)elementID +{ + return [[self attributeForName:@"id"] stringValue]; +} + +- (NSString *)toStr +{ + return [[self attributeForName:@"to"] stringValue]; +} + +- (NSString *)fromStr +{ + return [[self attributeForName:@"from"] stringValue]; +} + +- (XMPPJID *)to +{ + return [XMPPJID jidWithString:[self toStr]]; +} + +- (XMPPJID *)from +{ + return [XMPPJID jidWithString:[self fromStr]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark To and From Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isTo:(XMPPJID *)to +{ + return [self.to isEqualToJID:to]; +} + +- (BOOL)isTo:(XMPPJID *)to options:(XMPPJIDCompareOptions)mask +{ + return [self.to isEqualToJID:to options:mask]; +} + +- (BOOL)isFrom:(XMPPJID *)from +{ + return [self.from isEqualToJID:from]; +} + +- (BOOL)isFrom:(XMPPJID *)from options:(XMPPJIDCompareOptions)mask +{ + return [self.from isEqualToJID:from options:mask]; +} + +- (BOOL)isToOrFrom:(XMPPJID *)toOrFrom +{ + if([self isTo:toOrFrom] || [self isFrom:toOrFrom]) + { + return YES; + } + else + { + return NO; + } +} + +- (BOOL)isToOrFrom:(XMPPJID *)toOrFrom options:(XMPPJIDCompareOptions)mask +{ + if([self isTo:toOrFrom options:mask] || [self isFrom:toOrFrom options:mask]) + { + return YES; + } + else + { + return NO; + } +} + +- (BOOL)isTo:(XMPPJID *)to from:(XMPPJID *)from +{ + if([self isTo:to] && [self isFrom:from]) + { + return YES; + } + else + { + return NO; + } +} + +- (BOOL)isTo:(XMPPJID *)to from:(XMPPJID *)from options:(XMPPJIDCompareOptions)mask +{ + if([self isTo:to options:mask] && [self isFrom:from options:mask]) + { + return YES; + } + else + { + return NO; + } +} + +@end diff --git a/Core/XMPPIQ.h b/Core/XMPPIQ.h new file mode 100644 index 0000000..4c63801 --- /dev/null +++ b/Core/XMPPIQ.h @@ -0,0 +1,83 @@ +#import +#import "XMPPElement.h" + +/** + * The XMPPIQ class represents an element. + * It extends XMPPElement, which in turn extends NSXMLElement. + * All elements that go in and out of the + * xmpp stream will automatically be converted to XMPPIQ objects. + * + * This class exists to provide developers an easy way to add functionality to IQ processing. + * Simply add your own category to XMPPIQ to extend it with your own custom methods. +**/ + +@interface XMPPIQ : XMPPElement + +/** + * Converts an NSXMLElement to an XMPPIQ element in place (no memory allocations or copying) +**/ ++ (XMPPIQ *)iqFromElement:(NSXMLElement *)element; + +/** + * Creates and returns a new autoreleased XMPPIQ element. + * If the type or elementID parameters are nil, those attributes will not be added. +**/ ++ (XMPPIQ *)iq; ++ (XMPPIQ *)iqWithType:(NSString *)type; ++ (XMPPIQ *)iqWithType:(NSString *)type to:(XMPPJID *)jid; ++ (XMPPIQ *)iqWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid; ++ (XMPPIQ *)iqWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement; ++ (XMPPIQ *)iqWithType:(NSString *)type elementID:(NSString *)eid; ++ (XMPPIQ *)iqWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement; ++ (XMPPIQ *)iqWithType:(NSString *)type child:(NSXMLElement *)childElement; + +/** + * Creates and returns a new XMPPIQ element. + * If the type or elementID parameters are nil, those attributes will not be added. +**/ +- (id)init; +- (id)initWithType:(NSString *)type; +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid; +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid; +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement; +- (id)initWithType:(NSString *)type elementID:(NSString *)eid; +- (id)initWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement; +- (id)initWithType:(NSString *)type child:(NSXMLElement *)childElement; + +/** + * Returns the type attribute of the IQ. + * According to the XMPP protocol, the type should be one of 'get', 'set', 'result' or 'error'. + * + * This method converts the attribute to lowercase so + * case-sensitive string comparisons are safe (regardless of server treatment). +**/ +- (NSString *)type; + +/** + * Convenience methods for determining the IQ type. +**/ +- (BOOL)isGetIQ; +- (BOOL)isSetIQ; +- (BOOL)isResultIQ; +- (BOOL)isErrorIQ; + +/** + * Convenience method for determining if the IQ is of type 'get' or 'set'. +**/ +- (BOOL)requiresResponse; + +/** + * The XMPP RFC has various rules for the number of child elements an IQ is allowed to have: + * + * - An IQ stanza of type "get" or "set" MUST contain one and only one child element. + * - An IQ stanza of type "result" MUST include zero or one child elements. + * - An IQ stanza of type "error" SHOULD include the child element contained in the + * associated "get" or "set" and MUST include an child. + * + * The childElement returns the single non-error element, if one exists, or nil. + * The childErrorElement returns the error element, if one exists, or nil. +**/ +- (NSXMLElement *)childElement; +- (NSXMLElement *)childErrorElement; + +@end diff --git a/Core/XMPPIQ.m b/Core/XMPPIQ.m new file mode 100644 index 0000000..2edba24 --- /dev/null +++ b/Core/XMPPIQ.m @@ -0,0 +1,222 @@ +#import "XMPPIQ.h" +#import "XMPPJID.h" +#import "NSXMLElement+XMPP.h" + +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation XMPPIQ + +#if DEBUG + ++ (void)initialize +{ + // We use the object_setClass method below to dynamically change the class from a standard NSXMLElement. + // The size of the two classes is expected to be the same. + // + // If a developer adds instance methods to this class, bad things happen at runtime that are very hard to debug. + // This check is here to aid future developers who may make this mistake. + // + // For Fearless And Experienced Objective-C Developers: + // It may be possible to support adding instance variables to this class if you seriously need it. + // To do so, try realloc'ing self after altering the class, and then initialize your variables. + + size_t superSize = class_getInstanceSize([NSXMLElement class]); + size_t ourSize = class_getInstanceSize([XMPPIQ class]); + + if (superSize != ourSize) + { + NSLog(@"Adding instance variables to XMPPIQ is not currently supported!"); + exit(15); + } +} + +#endif + ++ (XMPPIQ *)iqFromElement:(NSXMLElement *)element +{ + object_setClass(element, [XMPPIQ class]); + + return (XMPPIQ *)element; +} + ++ (XMPPIQ *)iq +{ + return [[XMPPIQ alloc] initWithType:nil to:nil elementID:nil child:nil]; +} + ++ (XMPPIQ *)iqWithType:(NSString *)type +{ + return [[XMPPIQ alloc] initWithType:type to:nil elementID:nil child:nil]; +} + ++ (XMPPIQ *)iqWithType:(NSString *)type to:(XMPPJID *)jid +{ + return [[XMPPIQ alloc] initWithType:type to:jid elementID:nil child:nil]; +} + ++ (XMPPIQ *)iqWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid +{ + return [[XMPPIQ alloc] initWithType:type to:jid elementID:eid child:nil]; +} + ++ (XMPPIQ *)iqWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement +{ + return [[XMPPIQ alloc] initWithType:type to:jid elementID:eid child:childElement]; +} + ++ (XMPPIQ *)iqWithType:(NSString *)type elementID:(NSString *)eid +{ + return [[XMPPIQ alloc] initWithType:type to:nil elementID:eid child:nil]; +} + ++ (XMPPIQ *)iqWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement +{ + return [[XMPPIQ alloc] initWithType:type to:nil elementID:eid child:childElement]; +} + ++ (XMPPIQ *)iqWithType:(NSString *)type child:(NSXMLElement *)childElement +{ + return [[XMPPIQ alloc] initWithType:type to:nil elementID:nil child:childElement]; +} + +- (id)init +{ + return [self initWithType:nil to:nil elementID:nil child:nil]; +} + +- (id)initWithType:(NSString *)type +{ + return [self initWithType:type to:nil elementID:nil child:nil]; +} + +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid +{ + return [self initWithType:type to:jid elementID:nil child:nil]; +} + +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid +{ + return [self initWithType:type to:jid elementID:eid child:nil]; +} + +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement +{ + if ((self = [super initWithName:@"iq"])) + { + if (type) + [self addAttributeWithName:@"type" stringValue:type]; + + if (jid) + [self addAttributeWithName:@"to" stringValue:[jid full]]; + + if (eid) + [self addAttributeWithName:@"id" stringValue:eid]; + + if (childElement) + [self addChild:childElement]; + } + return self; +} + +- (id)initWithType:(NSString *)type elementID:(NSString *)eid +{ + return [self initWithType:type to:nil elementID:eid child:nil]; +} + +- (id)initWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement +{ + return [self initWithType:type to:nil elementID:eid child:childElement]; +} + +- (id)initWithType:(NSString *)type child:(NSXMLElement *)childElement +{ + return [self initWithType:type to:nil elementID:nil child:childElement]; +} + +- (id)initWithXMLString:(NSString *)string error:(NSError *__autoreleasing *)error +{ + if((self = [super initWithXMLString:string error:error])){ + self = [XMPPIQ iqFromElement:self]; + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone +{ + NSXMLElement *element = [super copyWithZone:zone]; + return [XMPPIQ iqFromElement:element]; +} + +- (NSString *)type +{ + return [[self attributeStringValueForName:@"type"] lowercaseString]; +} + +- (BOOL)isGetIQ +{ + return [[self type] isEqualToString:@"get"]; +} + +- (BOOL)isSetIQ +{ + return [[self type] isEqualToString:@"set"]; +} + +- (BOOL)isResultIQ +{ + return [[self type] isEqualToString:@"result"]; +} + +- (BOOL)isErrorIQ +{ + return [[self type] isEqualToString:@"error"]; +} + +- (BOOL)requiresResponse +{ + // An entity that receives an IQ request of type "get" or "set" MUST reply with an IQ response + // of type "result" or "error" (the response MUST preserve the 'id' attribute of the request). + + return [self isGetIQ] || [self isSetIQ]; +} + +- (NSXMLElement *)childElement +{ + NSArray *children = [self children]; + for (NSXMLElement *child in children) + { + // Careful: NSOrderedSame == 0 + + NSString *childName = [child name]; + if (childName && ([childName caseInsensitiveCompare:@"error"] != NSOrderedSame)) + { + return child; + } + } + + return nil; +} + +- (NSXMLElement *)childErrorElement +{ + NSArray *children = [self children]; + for (NSXMLElement *child in children) + { + // Careful: NSOrderedSame == 0 + + NSString *childName = [child name]; + if (childName && ([childName caseInsensitiveCompare:@"error"] == NSOrderedSame)) + { + return child; + } + } + + return nil; +} + +@end diff --git a/Core/XMPPInternal.h b/Core/XMPPInternal.h new file mode 100644 index 0000000..9b58ae2 --- /dev/null +++ b/Core/XMPPInternal.h @@ -0,0 +1,118 @@ +// +// This file is for XMPPStream and various internal components. +// + +#import "XMPPStream.h" +#import "XMPPModule.h" + +// Define the various states we'll use to track our progress +typedef NS_ENUM(NSInteger, XMPPStreamState) { + STATE_XMPP_DISCONNECTED, + STATE_XMPP_RESOLVING_SRV, + STATE_XMPP_CONNECTING, + STATE_XMPP_OPENING, + STATE_XMPP_NEGOTIATING, + STATE_XMPP_STARTTLS_1, + STATE_XMPP_STARTTLS_2, + STATE_XMPP_POST_NEGOTIATION, + STATE_XMPP_REGISTERING, + STATE_XMPP_AUTH, + STATE_XMPP_BINDING, + STATE_XMPP_START_SESSION, + STATE_XMPP_CONNECTED, +}; + +/** + * It is recommended that storage classes cache a stream's myJID. + * This prevents them from constantly querying the property from the xmppStream instance, + * as doing so goes through xmppStream's dispatch queue. + * Caching the stream's myJID frees the dispatch queue to handle xmpp processing tasks. + * + * The object of the notification will be the XMPPStream instance. + * + * Note: We're not using the typical MulticastDelegate paradigm for this task as + * storage classes are not typically added as a delegate of the xmppStream. +**/ +extern NSString *const XMPPStreamDidChangeMyJIDNotification; + +@interface XMPPStream (/* Internal */) + +/** + * XMPPStream maintains thread safety by dispatching through the internal serial xmppQueue. + * Subclasses of XMPPStream MUST follow the same technique: + * + * dispatch_block_t block = ^{ + * // Code goes here + * }; + * + * if (dispatch_get_specific(xmppQueueTag)) + * block(); + * else + * dispatch_sync(xmppQueue, block); + * + * Category methods may or may not need to dispatch through the xmppQueue. + * It depends entirely on what properties of xmppStream the category method needs to access. + * For example, if a category only accesses a single property, such as the rootElement, + * then it can simply fetch the atomic property, inspect it, and complete its job. + * However, if the category needs to fetch multiple properties, then it likely needs to fetch all such + * properties in an atomic fashion. In this case, the category should likely go through the xmppQueue, + * to ensure that it gets an atomic state of the xmppStream in order to complete its job. +**/ +@property (nonatomic, readonly) dispatch_queue_t xmppQueue; +@property (nonatomic, readonly) void *xmppQueueTag; + +/** + * Returns the current state of the xmppStream. +**/ +@property (atomic, readonly) XMPPStreamState state; + +/** + * This method is for use by xmpp authentication mechanism classes. + * They should send elements using this method instead of the public sendElement methods, + * as those methods don't send the elements while authentication is in progress. + * + * @see XMPPSASLAuthentication +**/ +- (void)sendAuthElement:(NSXMLElement *)element; + +/** + * This method is for use by xmpp custom binding classes. + * They should send elements using this method instead of the public sendElement methods, + * as those methods don't send the elements while authentication/binding is in progress. + * + * @see XMPPCustomBinding +**/ +- (void)sendBindElement:(NSXMLElement *)element; + +/** + * This method allows you to inject an element into the stream as if it was received on the socket. + * This is an advanced technique, but makes for some interesting possibilities. +**/ +- (void)injectElement:(NSXMLElement *)element; + +/** + * The XMPP standard only supports , and stanzas (excluding session setup stuff). + * But some extensions use non-standard element types. + * The standard example is XEP-0198, which uses & elements. + * + * XMPPStream will assume that any non-standard element types are errors, unless you register them. + * Once registered the stream can recognize them, and will use the following delegate methods: + * + * xmppStream:didSendCustomElement: + * xmppStream:didReceiveCustomElement: +**/ +- (void)registerCustomElementNames:(NSSet *)names; +- (void)unregisterCustomElementNames:(NSSet *)names; + +@end + +@interface XMPPModule (/* Internal */) + +/** + * Used internally by methods like XMPPStream's unregisterModule:. + * Normally removing a delegate is a synchronous operation, but due to multiple dispatch_sync operations, + * it must occasionally be done asynchronously to avoid deadlock. +**/ +- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue synchronously:(BOOL)synchronously; + +@end diff --git a/Core/XMPPJID.h b/Core/XMPPJID.h new file mode 100644 index 0000000..cfe51eb --- /dev/null +++ b/Core/XMPPJID.h @@ -0,0 +1,74 @@ +#import + +enum XMPPJIDCompareOptions +{ + XMPPJIDCompareUser = 1, // 001 + XMPPJIDCompareDomain = 2, // 010 + XMPPJIDCompareResource = 4, // 100 + + XMPPJIDCompareBare = 3, // 011 + XMPPJIDCompareFull = 7, // 111 +}; +typedef enum XMPPJIDCompareOptions XMPPJIDCompareOptions; + + +@interface XMPPJID : NSObject +{ + __strong NSString *user; + __strong NSString *domain; + __strong NSString *resource; +} + ++ (XMPPJID *)jidWithString:(NSString *)jidStr; ++ (XMPPJID *)jidWithString:(NSString *)jidStr resource:(NSString *)resource; ++ (XMPPJID *)jidWithUser:(NSString *)user domain:(NSString *)domain resource:(NSString *)resource; + +@property (strong, readonly) NSString *user; +@property (strong, readonly) NSString *domain; +@property (strong, readonly) NSString *resource; + +/** + * Terminology (from RFC 6120): + * + * The term "bare JID" refers to an XMPP address of the form (for an account at a server) + * or of the form (for a server). + * + * The term "full JID" refers to an XMPP address of the form + * (for a particular authorized client or device associated with an account) + * or of the form (for a particular resource or script associated with a server). + * + * Thus a bareJID is one that does not have a resource. + * And a fullJID is one that does have a resource. + * + * For convenience, there are also methods that that check for a user component as well. +**/ + +- (XMPPJID *)bareJID; +- (XMPPJID *)domainJID; + +- (NSString *)bare; +- (NSString *)full; + +- (BOOL)isBare; +- (BOOL)isBareWithUser; + +- (BOOL)isFull; +- (BOOL)isFullWithUser; + +/** + * A server JID does not have a user component. +**/ +- (BOOL)isServer; + +/** + * Returns a new jid with the given resource. +**/ +- (XMPPJID *)jidWithNewResource:(NSString *)resource; + +/** + * When you know both objects are JIDs, this method is a faster way to check equality than isEqual:. +**/ +- (BOOL)isEqualToJID:(XMPPJID *)aJID; +- (BOOL)isEqualToJID:(XMPPJID *)aJID options:(XMPPJIDCompareOptions)mask; + +@end diff --git a/Core/XMPPJID.m b/Core/XMPPJID.m new file mode 100644 index 0000000..fac24e0 --- /dev/null +++ b/Core/XMPPJID.m @@ -0,0 +1,603 @@ +#import "XMPPJID.h" +#import "XMPPStringPrep.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation XMPPJID + ++ (BOOL)validateDomain:(NSString *)domain +{ + // Domain is the only required part of a JID + if ((domain == nil) || ([domain length] == 0)) + return NO; + + // If there's an @ symbol in the domain it probably means user put @ in their username + NSRange invalidAtRange = [domain rangeOfString:@"@"]; + if (invalidAtRange.location != NSNotFound) + return NO; + + return YES; +} + ++ (BOOL)validateResource:(NSString *)resource +{ + // Can't use an empty string resource name + if ((resource != nil) && ([resource length] == 0)) + return NO; + + return YES; +} + ++ (BOOL)validateUser:(NSString *)user domain:(NSString *)domain resource:(NSString *)resource +{ + if (![self validateDomain:domain]) + return NO; + + if (![self validateResource:resource]) + return NO; + + return YES; +} + ++ (BOOL)parse:(NSString *)jidStr + outUser:(NSString **)user + outDomain:(NSString **)domain + outResource:(NSString **)resource +{ + if(user) *user = nil; + if(domain) *domain = nil; + if(resource) *resource = nil; + + if(jidStr == nil) return NO; + + NSString *rawUser = nil; + NSString *rawDomain = nil; + NSString *rawResource = nil; + + NSRange atRange = [jidStr rangeOfString:@"@"]; + + if (atRange.location != NSNotFound) + { + rawUser = [jidStr substringToIndex:atRange.location]; + + NSString *minusUser = [jidStr substringFromIndex:atRange.location+1]; + + NSRange slashRange = [minusUser rangeOfString:@"/"]; + + if (slashRange.location != NSNotFound) + { + rawDomain = [minusUser substringToIndex:slashRange.location]; + rawResource = [minusUser substringFromIndex:slashRange.location+1]; + } + else + { + rawDomain = minusUser; + } + } + else + { + NSRange slashRange = [jidStr rangeOfString:@"/"]; + + if (slashRange.location != NSNotFound) + { + rawDomain = [jidStr substringToIndex:slashRange.location]; + rawResource = [jidStr substringFromIndex:slashRange.location+1]; + } + else + { + rawDomain = jidStr; + } + } + + NSString *prepUser = [XMPPStringPrep prepNode:rawUser]; + NSString *prepDomain = [XMPPStringPrep prepDomain:rawDomain]; + NSString *prepResource = [XMPPStringPrep prepResource:rawResource]; + + if ([XMPPJID validateUser:prepUser domain:prepDomain resource:prepResource]) + { + if(user) *user = prepUser; + if(domain) *domain = prepDomain; + if(resource) *resource = prepResource; + + return YES; + } + + return NO; +} + ++ (XMPPJID *)jidWithString:(NSString *)jidStr +{ + NSString *user; + NSString *domain; + NSString *resource; + + if ([XMPPJID parse:jidStr outUser:&user outDomain:&domain outResource:&resource]) + { + XMPPJID *jid = [[XMPPJID alloc] init]; + jid->user = [user copy]; + jid->domain = [domain copy]; + jid->resource = [resource copy]; + + return jid; + } + + return nil; +} + ++ (XMPPJID *)jidWithString:(NSString *)jidStr resource:(NSString *)resource +{ + NSString *prepResource = [XMPPStringPrep prepResource:resource]; + if (![self validateResource:prepResource]) return nil; + + NSString *user; + NSString *domain; + + if ([XMPPJID parse:jidStr outUser:&user outDomain:&domain outResource:nil]) + { + XMPPJID *jid = [[XMPPJID alloc] init]; + jid->user = [user copy]; + jid->domain = [domain copy]; + jid->resource = [prepResource copy]; + + return jid; + } + + return nil; +} + ++ (XMPPJID *)jidWithUser:(NSString *)user domain:(NSString *)domain resource:(NSString *)resource +{ + NSString *prepUser = [XMPPStringPrep prepNode:user]; + NSString *prepDomain = [XMPPStringPrep prepDomain:domain]; + NSString *prepResource = [XMPPStringPrep prepResource:resource]; + + if ([XMPPJID validateUser:prepUser domain:prepDomain resource:prepResource]) + { + XMPPJID *jid = [[XMPPJID alloc] init]; + jid->user = [prepUser copy]; + jid->domain = [prepDomain copy]; + jid->resource = [prepResource copy]; + + return jid; + } + + return nil; +} + ++ (XMPPJID *)jidWithPrevalidatedUser:(NSString *)user + prevalidatedDomain:(NSString *)domain + prevalidatedResource:(NSString *)resource +{ + XMPPJID *jid = [[XMPPJID alloc] init]; + jid->user = [user copy]; + jid->domain = [domain copy]; + jid->resource = [resource copy]; + + return jid; +} + ++ (XMPPJID *)jidWithPrevalidatedUser:(NSString *)user + prevalidatedDomain:(NSString *)domain + resource:(NSString *)resource +{ + NSString *prepResource = [XMPPStringPrep prepResource:resource]; + if (![self validateResource:prepResource]) return nil; + + XMPPJID *jid = [[XMPPJID alloc] init]; + jid->user = [user copy]; + jid->domain = [domain copy]; + jid->resource = [prepResource copy]; + + return jid; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Encoding, Decoding: +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if ! TARGET_OS_IPHONE +- (id)replacementObjectForPortCoder:(NSPortCoder *)encoder +{ + if([encoder isBycopy]) + return self; + else + return [super replacementObjectForPortCoder:encoder]; + // return [NSDistantObject proxyWithLocal:self connection:[encoder connection]]; +} +#endif + +- (id)initWithCoder:(NSCoder *)coder +{ + if ((self = [super init])) + { + if ([coder allowsKeyedCoding]) + { + if([coder respondsToSelector:@selector(requiresSecureCoding)] && + [coder requiresSecureCoding]) + { + user = [[coder decodeObjectOfClass:[NSString class] forKey:@"user"] copy]; + domain = [[coder decodeObjectOfClass:[NSString class] forKey:@"domain"] copy]; + resource = [[coder decodeObjectOfClass:[NSString class] forKey:@"resource"] copy]; + } + else + { + user = [[coder decodeObjectForKey:@"user"] copy]; + domain = [[coder decodeObjectForKey:@"domain"] copy]; + resource = [[coder decodeObjectForKey:@"resource"] copy]; + } + } + else + { + user = [[coder decodeObject] copy]; + domain = [[coder decodeObject] copy]; + resource = [[coder decodeObject] copy]; + } + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + if ([coder allowsKeyedCoding]) + { + [coder encodeObject:user forKey:@"user"]; + [coder encodeObject:domain forKey:@"domain"]; + [coder encodeObject:resource forKey:@"resource"]; + } + else + { + [coder encodeObject:user]; + [coder encodeObject:domain]; + [coder encodeObject:resource]; + } +} + ++ (BOOL) supportsSecureCoding +{ + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Copying: +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)copyWithZone:(NSZone *)zone +{ + // This class is immutable + return self; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Normal Methods: +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Why didn't we just synthesize these properties? +// +// Since these variables are readonly within the class, +// we want the synthesized methods to work like a nonatomic property. +// In order to do this, we have to mark the properties as nonatomic in the header. +// However we don't like marking the property as nonatomic in the header because +// then people might think it's not thread-safe when in fact it is. + +- (NSString *)user +{ + return user; // Why didn't we just synthesize this? See comment above. +} + +- (NSString *)domain +{ + return domain; // Why didn't we just synthesize this? See comment above. +} + +- (NSString *)resource +{ + return resource; // Why didn't we just synthesize this? See comment above. +} + +- (XMPPJID *)bareJID +{ + if (resource == nil) + { + return self; + } + else + { + return [XMPPJID jidWithPrevalidatedUser:user prevalidatedDomain:domain prevalidatedResource:nil]; + } +} + +- (XMPPJID *)domainJID +{ + if (user == nil && resource == nil) + { + return self; + } + else + { + return [XMPPJID jidWithPrevalidatedUser:nil prevalidatedDomain:domain prevalidatedResource:nil]; + } +} + +- (NSString *)bare +{ + if (user) + return [NSString stringWithFormat:@"%@@%@", user, domain]; + else + return domain; +} + +- (NSString *)full +{ + if (user) + { + if (resource) + return [NSString stringWithFormat:@"%@@%@/%@", user, domain, resource]; + else + return [NSString stringWithFormat:@"%@@%@", user, domain]; + } + else + { + if (resource) + return [NSString stringWithFormat:@"%@/%@", domain, resource]; + else + return domain; + } +} + +- (BOOL)isBare +{ + // From RFC 6120 Terminology: + // + // The term "bare JID" refers to an XMPP address of the form (for an account at a server) + // or of the form (for a server). + + return (resource == nil); +} + +- (BOOL)isBareWithUser +{ + return (user != nil && resource == nil); +} + +- (BOOL)isFull +{ + // From RFC 6120 Terminology: + // + // The term "full JID" refers to an XMPP address of the form + // (for a particular authorized client or device associated with an account) + // or of the form (for a particular resource or script associated with a server). + + return (resource != nil); +} + +- (BOOL)isFullWithUser +{ + return (user != nil && resource != nil); +} + +- (BOOL)isServer +{ + return (user == nil); +} + +- (XMPPJID *)jidWithNewResource:(NSString *)newResource +{ + return [XMPPJID jidWithPrevalidatedUser:user prevalidatedDomain:domain resource:newResource]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark NSObject Methods: +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSUInteger)hash +{ + // We used to do this: + // return [[self full] hash]; + // + // It was functional but less than optimal because it required the creation of a new NSString everytime. + // Now the hashing of a string itself is extremely fast, + // so combining 3 hashes is much faster than creating a new string. + // To accomplish this we use the murmur hashing algorithm. + // + // MurmurHash2 was written by Austin Appleby, and is placed in the public domain. + // http://code.google.com/p/smhasher + + NSUInteger uhash = [user hash]; + NSUInteger dhash = [domain hash]; + NSUInteger rhash = [resource hash]; + + if (NSUIntegerMax == UINT32_MAX) // Should be optimized out via compiler since these are constants + { + // MurmurHash2 (32-bit) + // + // uint32_t MurmurHash2 ( const void * key, int len, uint32_t seed ) + // + // Normally one would pass a chunk of data ('key') and associated data chunk length ('len'). + // Instead we're going to use our 3 hashes. + // And we're going to randomly make up a 'seed'. + + const uint32_t seed = 0xa2f1b6f; // Some random value I made up + const uint32_t len = 12; // 3 hashes, each 4 bytes = 12 bytes + + // 'm' and 'r' are mixing constants generated offline. + // They're not really 'magic', they just happen to work well. + + const uint32_t m = 0x5bd1e995; + const int r = 24; + + // Initialize the hash to a 'random' value + + uint32_t h = seed ^ len; + uint32_t k; + + // Mix uhash + + k = uhash; + + k *= m; + k ^= k >> r; + k *= m; + + h *= m; + h ^= k; + + // Mix dhash + + k = dhash; + + k *= m; + k ^= k >> r; + k *= m; + + h *= m; + h ^= k; + + // Mix rhash + + k = rhash; + + k *= m; + k ^= k >> r; + k *= m; + + h *= m; + h ^= k; + + // Do a few final mixes of the hash to ensure the last few + // bytes are well-incorporated. + + h ^= h >> 13; + h *= m; + h ^= h >> 15; + + return (NSUInteger)h; + } + else + { + // MurmurHash2 (64-bit) + // + // uint64_t MurmurHash64A ( const void * key, int len, uint64_t seed ) + // + // Normally one would pass a chunk of data ('key') and associated data chunk length ('len'). + // Instead we're going to use our 3 hashes. + // And we're going to randomly make up a 'seed'. + + const uint32_t seed = 0xa2f1b6f; // Some random value I made up + const uint32_t len = 24; // 3 hashes, each 8 bytes = 24 bytes + + // 'm' and 'r' are mixing constants generated offline. + // They're not really 'magic', they just happen to work well. + + const uint64_t m = 0xc6a4a7935bd1e995LLU; + const int r = 47; + + // Initialize the hash to a 'random' value + + uint64_t h = seed ^ (len * m); + uint64_t k; + + // Mix uhash + + k = uhash; + + k *= m; + k ^= k >> r; + k *= m; + + h ^= k; + h *= m; + + // Mix dhash + + k = dhash; + + k *= m; + k ^= k >> r; + k *= m; + + h ^= k; + h *= m; + + // Mix rhash + + k = rhash; + + k *= m; + k ^= k >> r; + k *= m; + + h ^= k; + h *= m; + + // Do a few final mixes of the hash to ensure the last few + // bytes are well-incorporated. + + h ^= h >> r; + h *= m; + h ^= h >> r; + + return (NSUInteger)h; + } +} + +- (BOOL)isEqual:(id)anObject +{ + if ([anObject isMemberOfClass:[self class]]) + { + return [self isEqualToJID:(XMPPJID *)anObject options:XMPPJIDCompareFull]; + } + return NO; +} + +- (BOOL)isEqualToJID:(XMPPJID *)aJID +{ + return [self isEqualToJID:aJID options:XMPPJIDCompareFull]; +} + +- (BOOL)isEqualToJID:(XMPPJID *)aJID options:(XMPPJIDCompareOptions)mask +{ + if (aJID == nil) return NO; + + if (mask & XMPPJIDCompareUser) + { + if (user) { + if (![user isEqualToString:aJID->user]) return NO; + } + else { + if (aJID->user) return NO; + } + } + + if (mask & XMPPJIDCompareDomain) + { + if (domain) { + if (![domain isEqualToString:aJID->domain]) return NO; + } + else { + if (aJID->domain) return NO; + } + } + + if (mask & XMPPJIDCompareResource) + { + if (resource) { + if (![resource isEqualToString:aJID->resource]) return NO; + } + else { + if (aJID->resource) return NO; + } + } + + return YES; +} + +- (NSString *)description +{ + return [self full]; +} + + +@end diff --git a/Core/XMPPLogging.h b/Core/XMPPLogging.h new file mode 100644 index 0000000..975ee67 --- /dev/null +++ b/Core/XMPPLogging.h @@ -0,0 +1,191 @@ +/** + * In order to provide fast and flexible logging, this project uses Cocoa Lumberjack. + * + * The GitHub project page has a wealth of documentation if you have any questions. + * https://github.com/robbiehanson/CocoaLumberjack + * + * Here's what you need to know concerning how logging is setup for XMPPFramework: + * + * There are 4 log levels: + * - Error + * - Warning + * - Info + * - Verbose + * + * In addition to this, there is a Trace flag that can be enabled. + * When tracing is enabled, it spits out the methods that are being called. + * + * Please note that tracing is separate from the log levels. + * For example, one could set the log level to warning, and enable tracing. + * + * All logging is asynchronous, except errors. + * To use logging within your own custom files, follow the steps below. + * + * Step 1: + * Import this header in your implementation file: + * + * #import "XMPPLogging.h" + * + * Step 2: + * Define your logging level in your implementation file: + * + * // Log levels: off, error, warn, info, verbose + * static const int xmppLogLevel = XMPP_LOG_LEVEL_VERBOSE; + * + * If you wish to enable tracing, you could do something like this: + * + * // Log levels: off, error, warn, info, verbose + * static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO | XMPP_LOG_FLAG_TRACE; + * + * Step 3: + * Replace your NSLog statements with XMPPLog statements according to the severity of the message. + * + * NSLog(@"Fatal error, no dohickey found!"); -> XMPPLogError(@"Fatal error, no dohickey found!"); + * + * XMPPLog has the same syntax as NSLog. + * This means you can pass it multiple variables just like NSLog. + * + * You may optionally choose to define different log levels for debug and release builds. + * You can do so like this: + * + * // Log levels: off, error, warn, info, verbose + * #if DEBUG + * static const int xmppLogLevel = XMPP_LOG_LEVEL_VERBOSE; + * #else + * static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; + * #endif + * + * Xcode projects created with Xcode 4 automatically define DEBUG via the project's preprocessor macros. + * If you created your project with a previous version of Xcode, you may need to add the DEBUG macro manually. +**/ + +#import "CocoaLumberJack/DDLog.h" + +// Global flag to enable/disable logging throughout the entire xmpp framework. + +#ifndef XMPP_LOGGING_ENABLED +#define XMPP_LOGGING_ENABLED 1 +#endif + +// Define logging context for every log message coming from the XMPP framework. +// The logging context can be extracted from the DDLogMessage from within the logging framework. +// This gives loggers, formatters, and filters the ability to optionally process them differently. + +#define XMPP_LOG_CONTEXT 5222 + +// Configure log levels. + +#define XMPP_LOG_FLAG_ERROR (1 << 0) // 0...00001 +#define XMPP_LOG_FLAG_WARN (1 << 1) // 0...00010 +#define XMPP_LOG_FLAG_INFO (1 << 2) // 0...00100 +#define XMPP_LOG_FLAG_VERBOSE (1 << 3) // 0...01000 + +#define XMPP_LOG_LEVEL_OFF 0 // 0...00000 +#define XMPP_LOG_LEVEL_ERROR (XMPP_LOG_LEVEL_OFF | XMPP_LOG_FLAG_ERROR) // 0...00001 +#define XMPP_LOG_LEVEL_WARN (XMPP_LOG_LEVEL_ERROR | XMPP_LOG_FLAG_WARN) // 0...00011 +#define XMPP_LOG_LEVEL_INFO (XMPP_LOG_LEVEL_WARN | XMPP_LOG_FLAG_INFO) // 0...00111 +#define XMPP_LOG_LEVEL_VERBOSE (XMPP_LOG_LEVEL_INFO | XMPP_LOG_FLAG_VERBOSE) // 0...01111 + +// Setup fine grained logging. +// The first 4 bits are being used by the standard log levels (0 - 3) +// +// We're going to add tracing, but NOT as a log level. +// Tracing can be turned on and off independently of log level. + +#define XMPP_LOG_FLAG_TRACE (1 << 4) // 0...10000 + +// Setup the usual boolean macros. + +#define XMPP_LOG_ERROR (xmppLogLevel & XMPP_LOG_FLAG_ERROR) +#define XMPP_LOG_WARN (xmppLogLevel & XMPP_LOG_FLAG_WARN) +#define XMPP_LOG_INFO (xmppLogLevel & XMPP_LOG_FLAG_INFO) +#define XMPP_LOG_VERBOSE (xmppLogLevel & XMPP_LOG_FLAG_VERBOSE) +#define XMPP_LOG_TRACE (xmppLogLevel & XMPP_LOG_FLAG_TRACE) + +// Configure asynchronous logging. +// We follow the default configuration, +// but we reserve a special macro to easily disable asynchronous logging for debugging purposes. + +#if DEBUG +#define XMPP_LOG_ASYNC_ENABLED NO +#else +#define XMPP_LOG_ASYNC_ENABLED YES +#endif + +#define XMPP_LOG_ASYNC_ERROR ( NO && XMPP_LOG_ASYNC_ENABLED) +#define XMPP_LOG_ASYNC_WARN (YES && XMPP_LOG_ASYNC_ENABLED) +#define XMPP_LOG_ASYNC_INFO (YES && XMPP_LOG_ASYNC_ENABLED) +#define XMPP_LOG_ASYNC_VERBOSE (YES && XMPP_LOG_ASYNC_ENABLED) +#define XMPP_LOG_ASYNC_TRACE (YES && XMPP_LOG_ASYNC_ENABLED) + +// Define logging primitives. +// These are primarily wrappers around the macros defined in Lumberjack's DDLog.h header file. + +#define XMPP_LOG_OBJC_MAYBE(async, lvl, flg, ctx, frmt, ...) \ + do{ if(XMPP_LOGGING_ENABLED) LOG_MAYBE(async, lvl, flg, ctx, sel_getName(_cmd), frmt, ##__VA_ARGS__); } while(0) + +#define XMPP_LOG_C_MAYBE(async, lvl, flg, ctx, frmt, ...) \ + do{ if(XMPP_LOGGING_ENABLED) LOG_MAYBE(async, lvl, flg, ctx, __FUNCTION__, frmt, ##__VA_ARGS__); } while(0) + + +#define XMPPLogError(frmt, ...) XMPP_LOG_OBJC_MAYBE(XMPP_LOG_ASYNC_ERROR, xmppLogLevel, XMPP_LOG_FLAG_ERROR, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define XMPPLogWarn(frmt, ...) XMPP_LOG_OBJC_MAYBE(XMPP_LOG_ASYNC_WARN, xmppLogLevel, XMPP_LOG_FLAG_WARN, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define XMPPLogInfo(frmt, ...) XMPP_LOG_OBJC_MAYBE(XMPP_LOG_ASYNC_INFO, xmppLogLevel, XMPP_LOG_FLAG_INFO, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define XMPPLogVerbose(frmt, ...) XMPP_LOG_OBJC_MAYBE(XMPP_LOG_ASYNC_VERBOSE, xmppLogLevel, XMPP_LOG_FLAG_VERBOSE, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define XMPPLogTrace() XMPP_LOG_OBJC_MAYBE(XMPP_LOG_ASYNC_TRACE, xmppLogLevel, XMPP_LOG_FLAG_TRACE, \ + XMPP_LOG_CONTEXT, @"%@: %@", THIS_FILE, THIS_METHOD) + +#define XMPPLogTrace2(frmt, ...) XMPP_LOG_OBJC_MAYBE(XMPP_LOG_ASYNC_TRACE, xmppLogLevel, XMPP_LOG_FLAG_TRACE, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + + +#define XMPPLogCError(frmt, ...) XMPP_LOG_C_MAYBE(XMPP_LOG_ASYNC_ERROR, xmppLogLevel, XMPP_LOG_FLAG_ERROR, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define XMPPLogCWarn(frmt, ...) XMPP_LOG_C_MAYBE(XMPP_LOG_ASYNC_WARN, xmppLogLevel, XMPP_LOG_FLAG_WARN, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define XMPPLogCInfo(frmt, ...) XMPP_LOG_C_MAYBE(XMPP_LOG_ASYNC_INFO, xmppLogLevel, XMPP_LOG_FLAG_INFO, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define XMPPLogCVerbose(frmt, ...) XMPP_LOG_C_MAYBE(XMPP_LOG_ASYNC_VERBOSE, xmppLogLevel, XMPP_LOG_FLAG_VERBOSE, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +#define XMPPLogCTrace() XMPP_LOG_C_MAYBE(XMPP_LOG_ASYNC_TRACE, xmppLogLevel, XMPP_LOG_FLAG_TRACE, \ + XMPP_LOG_CONTEXT, @"%@: %s", THIS_FILE, __FUNCTION__) + +#define XMPPLogCTrace2(frmt, ...) XMPP_LOG_C_MAYBE(XMPP_LOG_ASYNC_TRACE, xmppLogLevel, XMPP_LOG_FLAG_TRACE, \ + XMPP_LOG_CONTEXT, frmt, ##__VA_ARGS__) + +// Setup logging for XMPPStream (and subclasses such as XMPPStreamFacebook) + +#define XMPP_LOG_FLAG_SEND (1 << 5) +#define XMPP_LOG_FLAG_RECV_PRE (1 << 6) // Prints data before it goes to the parser +#define XMPP_LOG_FLAG_RECV_POST (1 << 7) // Prints data as it comes out of the parser + +#define XMPP_LOG_FLAG_SEND_RECV (XMPP_LOG_FLAG_SEND | XMPP_LOG_FLAG_RECV_POST) + +#define XMPP_LOG_SEND (xmppLogLevel & XMPP_LOG_FLAG_SEND) +#define XMPP_LOG_RECV_PRE (xmppLogLevel & XMPP_LOG_FLAG_RECV_PRE) +#define XMPP_LOG_RECV_POST (xmppLogLevel & XMPP_LOG_FLAG_RECV_POST) + +#define XMPP_LOG_ASYNC_SEND (YES && XMPP_LOG_ASYNC_ENABLED) +#define XMPP_LOG_ASYNC_RECV_PRE (YES && XMPP_LOG_ASYNC_ENABLED) +#define XMPP_LOG_ASYNC_RECV_POST (YES && XMPP_LOG_ASYNC_ENABLED) + +#define XMPPLogSend(format, ...) XMPP_LOG_OBJC_MAYBE(XMPP_LOG_ASYNC_SEND, xmppLogLevel, \ + XMPP_LOG_FLAG_SEND, XMPP_LOG_CONTEXT, format, ##__VA_ARGS__) + +#define XMPPLogRecvPre(format, ...) XMPP_LOG_OBJC_MAYBE(XMPP_LOG_ASYNC_RECV_PRE, xmppLogLevel, \ + XMPP_LOG_FLAG_RECV_PRE, XMPP_LOG_CONTEXT, format, ##__VA_ARGS__) + +#define XMPPLogRecvPost(format, ...) XMPP_LOG_OBJC_MAYBE(XMPP_LOG_ASYNC_RECV_POST, xmppLogLevel, \ + XMPP_LOG_FLAG_RECV_POST, XMPP_LOG_CONTEXT, format, ##__VA_ARGS__) diff --git a/Core/XMPPMessage.h b/Core/XMPPMessage.h new file mode 100644 index 0000000..f34e6d3 --- /dev/null +++ b/Core/XMPPMessage.h @@ -0,0 +1,55 @@ +#import +#import "XMPPElement.h" + +/** + * The XMPPMessage class represents a element. + * It extends XMPPElement, which in turn extends NSXMLElement. + * All elements that go in and out of the + * xmpp stream will automatically be converted to XMPPMessage objects. + * + * This class exists to provide developers an easy way to add functionality to message processing. + * Simply add your own category to XMPPMessage to extend it with your own custom methods. +**/ + +@interface XMPPMessage : XMPPElement + +// Converts an NSXMLElement to an XMPPMessage element in place (no memory allocations or copying) ++ (XMPPMessage *)messageFromElement:(NSXMLElement *)element; + ++ (XMPPMessage *)message; ++ (XMPPMessage *)messageWithType:(NSString *)type; ++ (XMPPMessage *)messageWithType:(NSString *)type to:(XMPPJID *)to; ++ (XMPPMessage *)messageWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid; ++ (XMPPMessage *)messageWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement; ++ (XMPPMessage *)messageWithType:(NSString *)type elementID:(NSString *)eid; ++ (XMPPMessage *)messageWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement; ++ (XMPPMessage *)messageWithType:(NSString *)type child:(NSXMLElement *)childElement; + +- (id)init; +- (id)initWithType:(NSString *)type; +- (id)initWithType:(NSString *)type to:(XMPPJID *)to; +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid; +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement; +- (id)initWithType:(NSString *)type elementID:(NSString *)eid; +- (id)initWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement; +- (id)initWithType:(NSString *)type child:(NSXMLElement *)childElement; + +- (NSString *)type; +- (NSString *)subject; +- (NSString *)body; +- (NSString *)bodyForLanguage:(NSString *)language; +- (NSString *)thread; + +- (void)addSubject:(NSString *)subject; +- (void)addBody:(NSString *)body; +- (void)addBody:(NSString *)body withLanguage:(NSString *)language; +- (void)addThread:(NSString *)thread; + +- (BOOL)isChatMessage; +- (BOOL)isChatMessageWithBody; +- (BOOL)isErrorMessage; +- (BOOL)isMessageWithBody; + +- (NSError *)errorMessage; + +@end diff --git a/Core/XMPPMessage.m b/Core/XMPPMessage.m new file mode 100644 index 0000000..2a44aea --- /dev/null +++ b/Core/XMPPMessage.m @@ -0,0 +1,267 @@ +#import "XMPPMessage.h" +#import "XMPPJID.h" +#import "NSXMLElement+XMPP.h" + +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation XMPPMessage + +#if DEBUG + ++ (void)initialize +{ + // We use the object_setClass method below to dynamically change the class from a standard NSXMLElement. + // The size of the two classes is expected to be the same. + // + // If a developer adds instance methods to this class, bad things happen at runtime that are very hard to debug. + // This check is here to aid future developers who may make this mistake. + // + // For Fearless And Experienced Objective-C Developers: + // It may be possible to support adding instance variables to this class if you seriously need it. + // To do so, try realloc'ing self after altering the class, and then initialize your variables. + + size_t superSize = class_getInstanceSize([NSXMLElement class]); + size_t ourSize = class_getInstanceSize([XMPPMessage class]); + + if (superSize != ourSize) + { + NSLog(@"Adding instance variables to XMPPMessage is not currently supported!"); + exit(15); + } +} + +#endif + ++ (XMPPMessage *)messageFromElement:(NSXMLElement *)element +{ + object_setClass(element, [XMPPMessage class]); + + return (XMPPMessage *)element; +} + ++ (XMPPMessage *)message +{ + return [[XMPPMessage alloc] init]; +} + ++ (XMPPMessage *)messageWithType:(NSString *)type +{ + return [[XMPPMessage alloc] initWithType:type to:nil]; +} + ++ (XMPPMessage *)messageWithType:(NSString *)type to:(XMPPJID *)to +{ + return [[XMPPMessage alloc] initWithType:type to:to]; +} + ++ (XMPPMessage *)messageWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid +{ + return [[XMPPMessage alloc] initWithType:type to:jid elementID:eid]; +} + ++ (XMPPMessage *)messageWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement +{ + return [[XMPPMessage alloc] initWithType:type to:jid elementID:eid child:childElement]; +} + ++ (XMPPMessage *)messageWithType:(NSString *)type elementID:(NSString *)eid +{ + return [[XMPPMessage alloc] initWithType:type elementID:eid]; +} + ++ (XMPPMessage *)messageWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement +{ + return [[XMPPMessage alloc] initWithType:type elementID:eid child:childElement]; +} + ++ (XMPPMessage *)messageWithType:(NSString *)type child:(NSXMLElement *)childElement +{ + return [[XMPPMessage alloc] initWithType:type child:childElement]; +} + +- (id)init +{ + return [self initWithType:nil to:nil elementID:nil child:nil]; +} + +- (id)initWithType:(NSString *)type +{ + return [self initWithType:type to:nil elementID:nil child:nil]; +} + +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid +{ + return [self initWithType:type to:jid elementID:nil child:nil]; +} + +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid +{ + return [self initWithType:type to:jid elementID:eid child:nil]; +} + +- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement +{ + if ((self = [super initWithName:@"message"])) + { + if (type) + [self addAttributeWithName:@"type" stringValue:type]; + + if (jid) + [self addAttributeWithName:@"to" stringValue:[jid full]]; + + if (eid) + [self addAttributeWithName:@"id" stringValue:eid]; + + if (childElement) + [self addChild:childElement]; + } + return self; +} + +- (id)initWithType:(NSString *)type elementID:(NSString *)eid +{ + return [self initWithType:type to:nil elementID:eid child:nil]; +} + +- (id)initWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement +{ + return [self initWithType:type to:nil elementID:eid child:childElement]; +} + +- (id)initWithType:(NSString *)type child:(NSXMLElement *)childElement +{ + return [self initWithType:type to:nil elementID:nil child:childElement]; +} + +- (id)initWithXMLString:(NSString *)string error:(NSError *__autoreleasing *)error +{ + if((self = [super initWithXMLString:string error:error])){ + self = [XMPPMessage messageFromElement:self]; + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone +{ + NSXMLElement *element = [super copyWithZone:zone]; + return [XMPPMessage messageFromElement:element]; +} + +- (NSString *)type +{ + return [[self attributeForName:@"type"] stringValue]; +} + +- (NSString *)subject +{ + return [[self elementForName:@"subject"] stringValue]; +} + +- (NSString *)body +{ + return [[self elementForName:@"body"] stringValue]; +} + +- (NSString *)bodyForLanguage:(NSString *)language +{ + NSString *bodyForLanguage = nil; + + for (NSXMLElement *bodyElement in [self elementsForName:@"body"]) + { + NSString *lang = [[bodyElement attributeForName:@"xml:lang"] stringValue]; + + // Openfire strips off the xml prefix + if (lang == nil) + { + lang = [[bodyElement attributeForName:@"lang"] stringValue]; + } + + if ([language isEqualToString:lang] || ([language length] == 0 && [lang length] == 0)) + { + bodyForLanguage = [bodyElement stringValue]; + break; + } + } + + return bodyForLanguage; +} + +- (NSString *)thread +{ + return [[self elementForName:@"thread"] stringValue]; +} + +- (void)addSubject:(NSString *)subject +{ + NSXMLElement *subjectElement = [NSXMLElement elementWithName:@"subject" stringValue:subject]; + [self addChild:subjectElement]; +} + +- (void)addBody:(NSString *)body +{ + NSXMLElement *bodyElement = [NSXMLElement elementWithName:@"body" stringValue:body]; + [self addChild:bodyElement]; +} + +- (void)addBody:(NSString *)body withLanguage:(NSString *)language +{ + NSXMLElement *bodyElement = [NSXMLElement elementWithName:@"body" stringValue:body]; + + if ([language length]) + { + [bodyElement addAttributeWithName:@"xml:lang" stringValue:language]; + } + + [self addChild:bodyElement]; +} + +- (void)addThread:(NSString *)thread +{ + NSXMLElement *threadElement = [NSXMLElement elementWithName:@"thread" stringValue:thread]; + [self addChild:threadElement]; +} + +- (BOOL)isChatMessage +{ + return [[[self attributeForName:@"type"] stringValue] isEqualToString:@"chat"]; +} + +- (BOOL)isChatMessageWithBody +{ + if ([self isChatMessage]) + { + return [self isMessageWithBody]; + } + + return NO; +} + +- (BOOL)isErrorMessage +{ + return [[[self attributeForName:@"type"] stringValue] isEqualToString:@"error"]; +} + +- (NSError *)errorMessage +{ + if (![self isErrorMessage]) { + return nil; + } + + NSXMLElement *error = [self elementForName:@"error"]; + return [NSError errorWithDomain:@"urn:ietf:params:xml:ns:xmpp-stanzas" + code:[error attributeIntValueForName:@"code"] + userInfo:@{NSLocalizedDescriptionKey : [error compactXMLString]}]; + +} + +- (BOOL)isMessageWithBody +{ + return ([self elementForName:@"body"] != nil); +} + +@end diff --git a/Core/XMPPModule.h b/Core/XMPPModule.h new file mode 100644 index 0000000..73af16c --- /dev/null +++ b/Core/XMPPModule.h @@ -0,0 +1,43 @@ +#import +#import "GCDMulticastDelegate.h" + +@class XMPPStream; + +/** + * XMPPModule is the base class that all extensions/modules inherit. + * They automatically get: + * + * - A dispatch queue. + * - A multicast delegate that automatically invokes added delegates. + * + * The module also automatically registers/unregisters itself with the + * xmpp stream during the activate/deactive methods. +**/ +@interface XMPPModule : NSObject +{ + XMPPStream *xmppStream; + + dispatch_queue_t moduleQueue; + void *moduleQueueTag; + + id multicastDelegate; +} + +@property (readonly) dispatch_queue_t moduleQueue; +@property (readonly) void *moduleQueueTag; + +@property (strong, readonly) XMPPStream *xmppStream; + +- (id)init; +- (id)initWithDispatchQueue:(dispatch_queue_t)queue; + +- (BOOL)activate:(XMPPStream *)aXmppStream; +- (void)deactivate; + +- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; +- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; +- (void)removeDelegate:(id)delegate; + +- (NSString *)moduleName; + +@end diff --git a/Core/XMPPModule.m b/Core/XMPPModule.m new file mode 100644 index 0000000..24ba9d3 --- /dev/null +++ b/Core/XMPPModule.m @@ -0,0 +1,224 @@ +#import "XMPPModule.h" +#import "XMPPStream.h" +#import "XMPPLogging.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation XMPPModule + +/** + * Standard init method. +**/ +- (id)init +{ + return [self initWithDispatchQueue:NULL]; +} + +/** + * Designated initializer. +**/ +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super init])) + { + if (queue) + { + moduleQueue = queue; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(moduleQueue); + #endif + } + else + { + const char *moduleQueueName = [[self moduleName] UTF8String]; + moduleQueue = dispatch_queue_create(moduleQueueName, NULL); + } + + moduleQueueTag = &moduleQueueTag; + dispatch_queue_set_specific(moduleQueue, moduleQueueTag, moduleQueueTag, NULL); + + multicastDelegate = [[GCDMulticastDelegate alloc] init]; + } + return self; +} + +- (void)dealloc +{ + #if !OS_OBJECT_USE_OBJC + dispatch_release(moduleQueue); + #endif +} + +/** + * The activate method is the point at which the module gets plugged into the xmpp stream. + * + * It is recommended that subclasses override didActivate, instead of this method, + * to perform any custom actions upon activation. +**/ +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + __block BOOL result = YES; + + dispatch_block_t block = ^{ + + if (xmppStream != nil) + { + result = NO; + } + else + { + xmppStream = aXmppStream; + + [xmppStream addDelegate:self delegateQueue:moduleQueue]; + [xmppStream registerModule:self]; + + [self didActivate]; + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +/** + * It is recommended that subclasses override this method (instead of activate:) + * to perform tasks after the module has been activated. + * + * This method is only invoked if the module is successfully activated. + * This method is always invoked on the moduleQueue. +**/ +- (void)didActivate +{ + // Override me to do custom work after the module is activated +} + +/** + * The deactivate method unplugs a module from the xmpp stream. + * When this method returns, no further delegate methods on this module will be dispatched. + * However, there may be delegate methods that have already been dispatched. + * If this is the case, the module will be properly retained until the delegate methods have completed. + * If your custom module requires that delegate methods are not run after the deactivate method has been run, + * then simply check the xmppStream variable in your delegate methods. + * + * It is recommended that subclasses override didDeactivate, instead of this method, + * to perform any custom actions upon deactivation. +**/ +- (void)deactivate +{ + dispatch_block_t block = ^{ + + if (xmppStream) + { + [self willDeactivate]; + + [xmppStream removeDelegate:self delegateQueue:moduleQueue]; + [xmppStream unregisterModule:self]; + + xmppStream = nil; + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); +} + +/** + * It is recommended that subclasses override this method (instead of deactivate:) + * to perform tasks after the module has been deactivated. + * + * This method is only invoked if the module is transitioning from activated to deactivated. + * This method is always invoked on the moduleQueue. +**/ +- (void)willDeactivate +{ + // Override me to do custom work after the module is deactivated +} + +- (dispatch_queue_t)moduleQueue +{ + return moduleQueue; +} + +- (void *)moduleQueueTag +{ + return moduleQueueTag; +} + +- (XMPPStream *)xmppStream +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return xmppStream; + } + else + { + __block XMPPStream *result; + + dispatch_sync(moduleQueue, ^{ + result = xmppStream; + }); + + return result; + } +} + +- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue +{ + // Asynchronous operation (if outside xmppQueue) + + dispatch_block_t block = ^{ + [multicastDelegate addDelegate:delegate delegateQueue:delegateQueue]; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + [multicastDelegate removeDelegate:delegate delegateQueue:delegateQueue]; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else if (synchronously) + dispatch_sync(moduleQueue, block); + else + dispatch_async(moduleQueue, block); + +} +- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue +{ + // Synchronous operation (common-case default) + + [self removeDelegate:delegate delegateQueue:delegateQueue synchronously:YES]; +} + +- (void)removeDelegate:(id)delegate +{ + // Synchronous operation (common-case default) + + [self removeDelegate:delegate delegateQueue:NULL synchronously:YES]; +} + +- (NSString *)moduleName +{ + // Override me (if needed) to provide a customized module name. + // This name is used as the name of the dispatch_queue which could aid in debugging. + + return NSStringFromClass([self class]); +} + +@end diff --git a/Core/XMPPParser.h b/Core/XMPPParser.h new file mode 100644 index 0000000..cff2587 --- /dev/null +++ b/Core/XMPPParser.h @@ -0,0 +1,40 @@ +#import + +#if TARGET_OS_IPHONE + #import "DDXML.h" +#endif + + +@interface XMPPParser : NSObject + +- (id)initWithDelegate:(id)delegate delegateQueue:(dispatch_queue_t)dq; +- (id)initWithDelegate:(id)delegate delegateQueue:(dispatch_queue_t)dq parserQueue:(dispatch_queue_t)pq; + +- (void)setDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; + +/** + * Asynchronously parses the given data. + * The delegate methods will be dispatch_async'd as events occur. +**/ +- (void)parseData:(NSData *)data; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPParserDelegate +@optional + +- (void)xmppParser:(XMPPParser *)sender didReadRoot:(NSXMLElement *)root; + +- (void)xmppParser:(XMPPParser *)sender didReadElement:(NSXMLElement *)element; + +- (void)xmppParserDidEnd:(XMPPParser *)sender; + +- (void)xmppParser:(XMPPParser *)sender didFail:(NSError *)error; + +- (void)xmppParserDidParseData:(XMPPParser *)sender; + +@end diff --git a/Core/XMPPParser.m b/Core/XMPPParser.m new file mode 100644 index 0000000..692da86 --- /dev/null +++ b/Core/XMPPParser.m @@ -0,0 +1,850 @@ +#import "XMPPParser.h" +#import "XMPPLogging.h" +#import +#import + +#if TARGET_OS_IPHONE + #import "DDXMLPrivate.h" +#endif + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +#define CHECK_FOR_NULL(value) \ + do { \ + if (value == NULL) { \ + xmpp_xmlAbortDueToMemoryShortage(ctxt); \ + return; \ + } \ + } while(false) + +#if !TARGET_OS_IPHONE + static void xmpp_recursiveAddChild(NSXMLElement *parent, xmlNodePtr childNode); +#endif + +@implementation XMPPParser +{ + #if __has_feature(objc_arc_weak) + __weak id delegate; + #else + __unsafe_unretained id delegate; + #endif + dispatch_queue_t delegateQueue; + + dispatch_queue_t parserQueue; + void *xmppParserQueueTag; + + BOOL hasReportedRoot; + unsigned depth; + + xmlParserCtxt *parserCtxt; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark iPhone +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_OS_IPHONE + +static void xmpp_onDidReadRoot(XMPPParser *parser, xmlNodePtr root) +{ + if (parser->delegateQueue && [parser->delegate respondsToSelector:@selector(xmppParser:didReadRoot:)]) + { + // We first copy the root node. + // We do this to allow the delegate to retain and make changes to the reported root + // without affecting the underlying xmpp parser. + + // xmlCopyNode(const xmlNodePtr node, int extended) + // + // node: + // the node to copy + // extended: + // if 1 do a recursive copy (properties, namespaces and children when applicable) + // if 2 copy properties and namespaces (when applicable) + + xmlNodePtr rootCopy = xmlCopyNode(root, 2); + DDXMLElement *rootCopyWrapper = [DDXMLElement nodeWithElementPrimitive:rootCopy owner:nil]; + + __strong id theDelegate = parser->delegate; + + dispatch_async(parser->delegateQueue, ^{ @autoreleasepool { + + [theDelegate xmppParser:parser didReadRoot:rootCopyWrapper]; + }}); + + // Note: DDXMLElement will properly free the rootCopy when it's deallocated. + } +} + +static void xmpp_onDidReadElement(XMPPParser *parser, xmlNodePtr child) +{ + // Detach the child from the xml tree. + // + // clean: Nullify next, prev, parent and doc pointers of child. + // fixNamespaces: Recurse through subtree, and ensure no namespaces are pointing to xmlNs nodes outside the tree. + // E.G. in a parent node that will no longer be available after the child is detached. + // + // We don't need to fix namespaces since we used xmpp_xmlSearchNs() to ensure we never created any + // namespaces outside the subtree of the child in the first place. + + [DDXMLNode detachChild:child andClean:YES andFixNamespaces:NO]; + + DDXMLElement *childWrapper = [DDXMLElement nodeWithElementPrimitive:child owner:nil]; + + // Note: We want to detach the child from the root even if the delegate method isn't setup. + // This prevents the doc from growing infinitely large. + + if (parser->delegateQueue && [parser->delegate respondsToSelector:@selector(xmppParser:didReadElement:)]) + { + __strong id theDelegate = parser->delegate; + + dispatch_async(parser->delegateQueue, ^{ @autoreleasepool { + + [theDelegate xmppParser:parser didReadElement:childWrapper]; + }}); + } + + // Note: DDXMLElement will properly free the child when it's deallocated. +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Mac +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#else + +static void xmpp_setName(NSXMLElement *element, xmlNodePtr node) +{ + // Remember: The NSString initWithUTF8String raises an exception if passed NULL + + if (node->name == NULL) + { + [element setName:@""]; + return; + } + + if ((node->ns != NULL) && (node->ns->prefix != NULL)) + { + // E.g: + + NSString *prefix = [[NSString alloc] initWithUTF8String:(const char *)node->ns->prefix]; + NSString *name = [[NSString alloc] initWithUTF8String:(const char *)node->name]; + + NSString *elementName = [[NSString alloc] initWithFormat:@"%@:%@", prefix, name]; + [element setName:elementName]; + + } + else + { + NSString *elementName = [[NSString alloc] initWithUTF8String:(const char *)node->name]; + [element setName:elementName]; + } +} + +static void xmpp_addNamespaces(NSXMLElement *element, xmlNodePtr node) +{ + // Remember: The NSString initWithUTF8String raises an exception if passed NULL + + xmlNsPtr nsNode = node->nsDef; + while (nsNode != NULL) + { + if (nsNode->href == NULL) + { + // Namespace doesn't have a value! + } + else + { + NSXMLNode *ns = [[NSXMLNode alloc] initWithKind:NSXMLNamespaceKind]; + + if (nsNode->prefix != NULL) + { + NSString *nsName = [[NSString alloc] initWithUTF8String:(const char *)nsNode->prefix]; + [ns setName:nsName]; + } + else + { + // Default namespace. + // E.g: xmlns="deusty.com" + + [ns setName:@""]; + } + + NSString *nsValue = [[NSString alloc] initWithUTF8String:(const char *)nsNode->href]; + [ns setStringValue:nsValue]; + + [element addNamespace:ns]; + } + + nsNode = nsNode->next; + } +} + +static void xmpp_addChildren(NSXMLElement *element, xmlNodePtr node) +{ + // Remember: The NSString initWithUTF8String raises an exception if passed NULL + + xmlNodePtr childNode = node->children; + while (childNode != NULL) + { + if (childNode->type == XML_ELEMENT_NODE) + { + xmpp_recursiveAddChild(element, childNode); + } + else if (childNode->type == XML_TEXT_NODE) + { + if (childNode->content != NULL) + { + NSString *value = [[NSString alloc] initWithUTF8String:(const char *)childNode->content]; + [element setStringValue:value]; + } + } + + childNode = childNode->next; + } +} + +static void xmpp_addAttributes(NSXMLElement *element, xmlNodePtr node) +{ + // Remember: The NSString initWithUTF8String raises an exception if passed NULL + + xmlAttrPtr attrNode = node->properties; + while (attrNode != NULL) + { + if (attrNode->name == NULL) + { + // Attribute doesn't have a name! + } + else if (attrNode->children == NULL) + { + // Attribute doesn't have a value node! + } + else if (attrNode->children->content == NULL) + { + // Attribute doesn't have a value! + } + else + { + NSXMLNode *attr = [[NSXMLNode alloc] initWithKind:NSXMLAttributeKind]; + + if ((attrNode->ns != NULL) && (attrNode->ns->prefix != NULL)) + { + // E.g: + + NSString *prefix = [[NSString alloc] initWithUTF8String:(const char *)attrNode->ns->prefix]; + NSString *name = [[NSString alloc] initWithUTF8String:(const char *)attrNode->name]; + + NSString *attrName = [[NSString alloc] initWithFormat:@"%@:%@", prefix, name]; + [attr setName:attrName]; + + } + else + { + NSString *attrName = [[NSString alloc] initWithUTF8String:(const char *)attrNode->name]; + [attr setName:attrName]; + } + + NSString *attrValue = [[NSString alloc] initWithUTF8String:(const char *)attrNode->children->content]; + [attr setStringValue:attrValue]; + + [element addAttribute:attr]; + } + + attrNode = attrNode->next; + } +} + +/** + * Recursively adds all the child elements to the given parent. + * + * Note: This method is almost the same as xmpp_nsxmlFromLibxml, with one important difference. + * It doen't add any objects to the autorelease pool (xmpp_nsxmlFromLibXml has return value). +**/ +static void xmpp_recursiveAddChild(NSXMLElement *parent, xmlNodePtr childNode) +{ + // Remember: The NSString initWithUTF8String raises an exception if passed NULL + + NSXMLElement *child = [[NSXMLElement alloc] initWithKind:NSXMLElementKind]; + + xmpp_setName(child, childNode); + + xmpp_addNamespaces(child, childNode); + + xmpp_addChildren(child, childNode); + xmpp_addAttributes(child, childNode); + + [parent addChild:child]; +} + +/** + * Creates and returns an NSXMLElement from the given node. + * Use this method after finding the root element, or root.child element. +**/ +static NSXMLElement* xmpp_nsxmlFromLibxml(xmlNodePtr rootNode) +{ + // Remember: The NSString initWithUTF8String raises an exception if passed NULL + + NSXMLElement *root = [[NSXMLElement alloc] initWithKind:NSXMLElementKind]; + + xmpp_setName(root, rootNode); + + xmpp_addNamespaces(root, rootNode); + + xmpp_addChildren(root, rootNode); + xmpp_addAttributes(root, rootNode); + + return root; +} + +static void xmpp_onDidReadRoot(XMPPParser *parser, xmlNodePtr root) +{ + if (parser->delegateQueue && [parser->delegate respondsToSelector:@selector(xmppParser:didReadRoot:)]) + { + NSXMLElement *nsRoot = xmpp_nsxmlFromLibxml(root); + + __strong id theDelegate = parser->delegate; + + dispatch_async(parser->delegateQueue, ^{ @autoreleasepool { + + [theDelegate xmppParser:parser didReadRoot:nsRoot]; + }}); + } +} + +static void xmpp_onDidReadElement(XMPPParser *parser, xmlNodePtr child) +{ + if (parser->delegateQueue && [parser->delegate respondsToSelector:@selector(xmppParser:didReadElement:)]) + { + NSXMLElement *nsChild = xmpp_nsxmlFromLibxml(child); + + __strong id theDelegate = parser->delegate; + + dispatch_async(parser->delegateQueue, ^{ @autoreleasepool { + + [theDelegate xmppParser:parser didReadElement:nsChild]; + }}); + } + + // Note: We want to detach the child from the root even if the delegate method isn't setup. + // This prevents the doc from growing infinitely large. + + // Detach and free child to keep memory footprint small + xmlUnlinkNode(child); + xmlFreeNode(child); +} + +#endif + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Common +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method is called at the end of the xmlStartElement method. + * This allows us to inspect the parser and xml tree, and determine if we need to invoke any delegate methods. +**/ +static void xmpp_postStartElement(xmlParserCtxt *ctxt) +{ + XMPPParser *parser = (__bridge XMPPParser *)ctxt->_private; + parser->depth++; + + if (!(parser->hasReportedRoot) && (parser->depth == 1)) + { + // We've received the full root - report it to the delegate + + if (ctxt->myDoc) + { + xmlNodePtr root = xmlDocGetRootElement(ctxt->myDoc); + if (root) + { + xmpp_onDidReadRoot(parser, root); + + parser->hasReportedRoot = YES; + } + } + } +} + +/** + * This method is called at the end of the xmlEndElement method. + * This allows us to inspect the parser and xml tree, and determine if we need to invoke any delegate methods. +**/ +static void xmpp_postEndElement(xmlParserCtxt *ctxt) +{ + XMPPParser *parser = (__bridge XMPPParser *)ctxt->_private; + parser->depth--; + + if (parser->depth == 1) + { + // End of full xmpp element. + // That is, a child of the root element. + // Extract the child, and pass it to the delegate. + + xmlDocPtr doc = ctxt->myDoc; + xmlNodePtr root = xmlDocGetRootElement(doc); + + xmlNodePtr child = root->children; + while (child != NULL) + { + if (child->type == XML_ELEMENT_NODE) + { + xmpp_onDidReadElement(parser, child); + + // Exit while loop + break; + } + + child = child->next; + } + } + else if (parser->depth == 0) + { + // End of the root element + + if (parser->delegateQueue && [parser->delegate respondsToSelector:@selector(xmppParserDidEnd:)]) + { + __strong id theDelegate = parser->delegate; + + dispatch_async(parser->delegateQueue, ^{ @autoreleasepool { + + [theDelegate xmppParserDidEnd:parser]; + }}); + } + } +} + +/** + * We're screwed... +**/ +static void xmpp_xmlAbortDueToMemoryShortage(xmlParserCtxt *ctxt) +{ + XMPPParser *parser = (__bridge XMPPParser *)ctxt->_private; + + xmlStopParser(ctxt); + + if (parser->delegateQueue && [parser->delegate respondsToSelector:@selector(xmppParser:didFail:)]) + { + NSString *errMsg = @"Unable to allocate memory in xmpp parser"; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + NSError *error = [NSError errorWithDomain:@"libxmlErrorDomain" code:1001 userInfo:info]; + + __strong id theDelegate = parser->delegate; + + dispatch_async(parser->delegateQueue, ^{ @autoreleasepool { + + [theDelegate xmppParser:parser didFail:error]; + }}); + } +} + +/** + * (Similar to the libxml "xmlSearchNs" method, with one very important difference.) + * + * This method searches for an existing xmlNsPtr in the given node, + * recursing on the parents but stopping before it reaches the root node of the document. + * + * Why do we skip the root node? + * Because all nodes are going to be detached from the root node. + * So it makes no sense to allow them to reference namespaces stored in the root node, + * since the detach algorithm will be forced to copy the namespaces later anyway. +**/ +static xmlNsPtr xmpp_xmlSearchNs(xmlDocPtr doc, xmlNodePtr node, const xmlChar *nameSpace) +{ + xmlNodePtr rootNode = xmlDocGetRootElement(doc); + + xmlNodePtr currentNode = node; + while (currentNode && currentNode != rootNode) + { + xmlNsPtr currentNs = currentNode->nsDef; + while (currentNs) + { + if (currentNs->href != NULL) + { + if ((currentNs->prefix == NULL) && (nameSpace == NULL)) + { + return currentNs; + } + if ((currentNs->prefix != NULL) && (nameSpace != NULL)) + { + if (xmlStrEqual(currentNs->prefix, nameSpace)) + return currentNs; + } + } + + currentNs = currentNs->next; + } + + currentNode = currentNode->parent; + } + + return NULL; +} + +/** + * SAX parser C-style callback. + * Invoked when a new node element is started. +**/ +static void xmpp_xmlStartElement(void *ctx, const xmlChar *nodeName, + const xmlChar *nodePrefix, + const xmlChar *nodeUri, + int nb_namespaces, + const xmlChar **namespaces, + int nb_attributes, + int nb_defaulted, + const xmlChar **attributes) +{ + int i, j; + xmlNsPtr lastAddedNs = NULL; + + xmlParserCtxt *ctxt = (xmlParserCtxt *)ctx; + + // We store the parent node in the context's node pointer. + // We keep this updated by "pushing" the node in the startElement method, + // and "popping" the node in the endElement method. + xmlNodePtr parent = ctxt->node; + + // Create the node + xmlNodePtr newNode = xmlNewDocNode(ctxt->myDoc, NULL, nodeName, NULL); + CHECK_FOR_NULL(newNode); + + // Add the node to the tree + if (parent == NULL) + { + // Root node + xmlAddChild((xmlNodePtr)ctxt->myDoc, newNode); + } + else + { + xmlAddChild(parent, newNode); + } + + // Process the namespaces + for (i = 0, j = 0; j < nb_namespaces; j++) + { + // Extract namespace prefix and uri + const xmlChar *nsPrefix = namespaces[i++]; + const xmlChar *nsUri = namespaces[i++]; + + // Create the namespace + xmlNsPtr newNs = xmlNewNs(NULL, nsUri, nsPrefix); + CHECK_FOR_NULL(newNs); + + // Add namespace to node. + // Each node has a linked list of nodes (in the nsDef variable). + // The linked list is forward only. + // In other words, each ns has a next, but not a prev pointer. + + if (newNode->nsDef == NULL) + { + newNode->nsDef = newNs; + lastAddedNs = newNs; + } + else + { + if(lastAddedNs != NULL) + { + lastAddedNs->next = newNs; + } + + lastAddedNs = newNs; + } + + // Is this the namespace for the node? + + if (nodeUri && (nodePrefix == nsPrefix)) + { + // Ex 1: node == && newNs == stream:url + // Ex 2: node == && newNs == null:url + + newNode->ns = newNs; + } + } + + // Search for the node's namespace if it wasn't already found + if ((nodeUri) && (newNode->ns == NULL)) + { + newNode->ns = xmpp_xmlSearchNs(ctxt->myDoc, newNode, nodePrefix); + + if (newNode->ns == NULL) + { + // We use href==NULL in the case of an element creation where the namespace was not defined. + // + // We do NOT use xmlNewNs(newNode, nodeUri, nodePrefix) because that method doesn't properly add + // the namespace to BOTH nsDef and ns. + + xmlNsPtr newNs = xmlNewNs(NULL, nodeUri, nodePrefix); + CHECK_FOR_NULL(newNs); + + if (newNode->nsDef == NULL) + { + newNode->nsDef = newNs; + } + else if(lastAddedNs != NULL) + { + lastAddedNs->next = newNs; + } + + newNode->ns = newNs; + } + } + + // Process all the attributes + for (i = 0, j = 0; j < nb_attributes; j++) + { + const xmlChar *attrName = attributes[i++]; + const xmlChar *attrPrefix = attributes[i++]; + const xmlChar *attrUri = attributes[i++]; + const xmlChar *valueBegin = attributes[i++]; + const xmlChar *valueEnd = attributes[i++]; + + // The attribute value might contain character references which need to be decoded. + // + // "Franks & Beans" -> "Franks & Beans" + + xmlChar *value = xmlStringLenDecodeEntities(ctxt, // the parser context + valueBegin, // the input string + (int)(valueEnd - valueBegin), // the input string length + (XML_SUBSTITUTE_REF), // what to substitue + 0, 0, 0); // end markers, 0 if none + CHECK_FOR_NULL(value); + + if ((attrPrefix == NULL) && (attrUri == NULL)) + { + // Normal attribute - no associated namespace + xmlAttrPtr newAttr = xmlNewProp(newNode, attrName, value); + CHECK_FOR_NULL(newAttr); + } + else + { + // Find the namespace for the attribute + xmlNsPtr attrNs = xmpp_xmlSearchNs(ctxt->myDoc, newNode, attrPrefix); + + if (attrNs != NULL) + { + xmlAttrPtr newAttr = xmlNewNsProp(newNode, attrNs, attrName, value); + CHECK_FOR_NULL(newAttr); + } + else + { + attrNs = xmlNewNs(NULL, NULL, nodePrefix); + CHECK_FOR_NULL(attrNs); + + xmlAttrPtr newAttr = xmlNewNsProp(newNode, attrNs, attrName, value); + CHECK_FOR_NULL(newAttr); + } + } + + xmlFree(value); + } + + // Update our parent node pointer + ctxt->node = newNode; + + // Invoke delegate methods if needed + xmpp_postStartElement(ctxt); +} + +/** + * SAX parser C-style callback. + * Invoked when characters are found within a node. +**/ +static void xmpp_xmlCharacters(void *ctx, const xmlChar *ch, int len) +{ + xmlParserCtxt *ctxt = (xmlParserCtxt *)ctx; + + if (ctxt->node != NULL) + { + xmlNodePtr textNode = xmlNewTextLen(ch, len); + + // xmlAddChild(xmlNodePtr parent, xmlNodePtr cur) + // + // Add a new node to @parent, at the end of the child list + // merging adjacent TEXT nodes (in which case @cur is freed). + + xmlAddChild(ctxt->node, textNode); + } +} + +/** + * SAX parser C-style callback. + * Invoked when a new node element is ended. +**/ +static void xmpp_xmlEndElement(void *ctx, const xmlChar *localname, + const xmlChar *prefix, + const xmlChar *URI) +{ + xmlParserCtxt *ctxt = (xmlParserCtxt *)ctx; + + // Update our parent node pointer + if (ctxt->node != NULL) + ctxt->node = ctxt->node->parent; + + // Invoke delegate methods if needed + xmpp_postEndElement(ctxt); +} + +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq +{ + return [self initWithDelegate:aDelegate delegateQueue:dq parserQueue:NULL]; +} + +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq parserQueue:(dispatch_queue_t)pq +{ + if ((self = [super init])) + { + delegate = aDelegate; + delegateQueue = dq; + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) + dispatch_retain(delegateQueue); + #endif + + if (pq) { + parserQueue = pq; + + #if !OS_OBJECT_USE_OBJC + dispatch_retain(parserQueue); + #endif + } + else { + parserQueue = dispatch_queue_create("xmpp.parser", NULL); + } + + xmppParserQueueTag = &xmppParserQueueTag; + dispatch_queue_set_specific(parserQueue, xmppParserQueueTag, xmppParserQueueTag, NULL); + + hasReportedRoot = NO; + depth = 0; + + // Create SAX handler + xmlSAXHandler saxHandler; + memset(&saxHandler, 0, sizeof(xmlSAXHandler)); + + saxHandler.initialized = XML_SAX2_MAGIC; + saxHandler.startElementNs = xmpp_xmlStartElement; + saxHandler.characters = xmpp_xmlCharacters; + saxHandler.endElementNs = xmpp_xmlEndElement; + + // Create the push parser context + parserCtxt = xmlCreatePushParserCtxt(&saxHandler, NULL, NULL, 0, NULL); + + // Note: This method copies the saxHandler, so we don't have to keep it around. + + // Create the document to hold the parsed elements + parserCtxt->myDoc = xmlNewDoc(parserCtxt->version); + + // Store reference to ourself + parserCtxt->_private = (__bridge void *)(self); + + // Note: The parserCtxt also has a userData variable, but it is used by the DOM building functions. + // If we put a value there, it actually causes a crash! + // We need to be sure to use the _private variable which libxml won't touch. + } + return self; +} + +- (void)dealloc +{ + if (parserCtxt) + { + // The xmlFreeParserCtxt method will not free the created document in parserCtxt->myDoc. + if (parserCtxt->myDoc) + { + // Free the created xmlDoc + xmlFreeDoc(parserCtxt->myDoc); + } + + xmlFreeParserCtxt(parserCtxt); + } + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) + dispatch_release(delegateQueue); + if (parserQueue) + dispatch_release(parserQueue); + #endif +} + +- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + #if !OS_OBJECT_USE_OBJC + if (newDelegateQueue) + dispatch_retain(newDelegateQueue); + #endif + + dispatch_block_t block = ^{ + + delegate = newDelegate; + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) + dispatch_release(delegateQueue); + #endif + + delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(xmppParserQueueTag)) + block(); + else + dispatch_async(parserQueue, block); +} + +- (void)parseData:(NSData *)data +{ + dispatch_block_t block = ^{ @autoreleasepool { + + int result = xmlParseChunk(parserCtxt, (const char *)[data bytes], (int)[data length], 0); + + if (result == 0) + { + if (delegateQueue && [delegate respondsToSelector:@selector(xmppParserDidParseData:)]) + { + __strong id theDelegate = delegate; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate xmppParserDidParseData:self]; + }}); + } + } + else + { + if (delegateQueue && [delegate respondsToSelector:@selector(xmppParser:didFail:)]) + { + NSError *error; + + xmlError *xmlErr = xmlCtxtGetLastError(parserCtxt); + + if (xmlErr->message) + { + NSString *errMsg = [NSString stringWithFormat:@"%s", xmlErr->message]; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + error = [NSError errorWithDomain:@"libxmlErrorDomain" code:xmlErr->code userInfo:info]; + } + else + { + error = [NSError errorWithDomain:@"libxmlErrorDomain" code:xmlErr->code userInfo:nil]; + } + + __strong id theDelegate = delegate; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate xmppParser:self didFail:error]; + }}); + } + } + }}; + + if (dispatch_get_specific(xmppParserQueueTag)) + block(); + else + dispatch_async(parserQueue, block); +} + +@end diff --git a/Core/XMPPPresence.h b/Core/XMPPPresence.h new file mode 100644 index 0000000..d77554b --- /dev/null +++ b/Core/XMPPPresence.h @@ -0,0 +1,38 @@ +#import +#import "XMPPElement.h" + +/** + * The XMPPPresence class represents a element. + * It extends XMPPElement, which in turn extends NSXMLElement. + * All elements that go in and out of the + * xmpp stream will automatically be converted to XMPPPresence objects. + * + * This class exists to provide developers an easy way to add functionality to presence processing. + * Simply add your own category to XMPPPresence to extend it with your own custom methods. +**/ + +@interface XMPPPresence : XMPPElement + +// Converts an NSXMLElement to an XMPPPresence element in place (no memory allocations or copying) ++ (XMPPPresence *)presenceFromElement:(NSXMLElement *)element; + ++ (XMPPPresence *)presence; ++ (XMPPPresence *)presenceWithType:(NSString *)type; ++ (XMPPPresence *)presenceWithType:(NSString *)type to:(XMPPJID *)to; + +- (id)init; +- (id)initWithType:(NSString *)type; +- (id)initWithType:(NSString *)type to:(XMPPJID *)to; + +- (NSString *)type; + +- (NSString *)show; +- (NSString *)status; + +- (int)priority; + +- (int)intShow; + +- (BOOL)isErrorPresence; + +@end diff --git a/Core/XMPPPresence.m b/Core/XMPPPresence.m new file mode 100644 index 0000000..a01e46e --- /dev/null +++ b/Core/XMPPPresence.m @@ -0,0 +1,145 @@ +#import "XMPPPresence.h" +#import "XMPPJID.h" +#import "NSXMLElement+XMPP.h" + +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation XMPPPresence + +#if DEBUG + ++ (void)initialize +{ + // We use the object_setClass method below to dynamically change the class from a standard NSXMLElement. + // The size of the two classes is expected to be the same. + // + // If a developer adds instance methods to this class, bad things happen at runtime that are very hard to debug. + // This check is here to aid future developers who may make this mistake. + // + // For Fearless And Experienced Objective-C Developers: + // It may be possible to support adding instance variables to this class if you seriously need it. + // To do so, try realloc'ing self after altering the class, and then initialize your variables. + + size_t superSize = class_getInstanceSize([NSXMLElement class]); + size_t ourSize = class_getInstanceSize([XMPPPresence class]); + + if (superSize != ourSize) + { + NSLog(@"Adding instance variables to XMPPPresence is not currently supported!"); + exit(15); + } +} + +#endif + ++ (XMPPPresence *)presenceFromElement:(NSXMLElement *)element +{ + object_setClass(element, [XMPPPresence class]); + + return (XMPPPresence *)element; +} + ++ (XMPPPresence *)presence +{ + return [[XMPPPresence alloc] init]; +} + ++ (XMPPPresence *)presenceWithType:(NSString *)type +{ + return [[XMPPPresence alloc] initWithType:type to:nil]; +} + ++ (XMPPPresence *)presenceWithType:(NSString *)type to:(XMPPJID *)to +{ + return [[XMPPPresence alloc] initWithType:type to:to]; +} + +- (id)init +{ + self = [super initWithName:@"presence"]; + return self; +} + +- (id)initWithType:(NSString *)type +{ + return [self initWithType:type to:nil]; +} + +- (id)initWithType:(NSString *)type to:(XMPPJID *)to +{ + if ((self = [super initWithName:@"presence"])) + { + if (type) + [self addAttributeWithName:@"type" stringValue:type]; + + if (to) + [self addAttributeWithName:@"to" stringValue:[to full]]; + } + return self; +} + +- (id)initWithXMLString:(NSString *)string error:(NSError *__autoreleasing *)error +{ + if((self = [super initWithXMLString:string error:error])){ + self = [XMPPPresence presenceFromElement:self]; + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone +{ + NSXMLElement *element = [super copyWithZone:zone]; + return [XMPPPresence presenceFromElement:element]; +} + +- (NSString *)type +{ + NSString *type = [self attributeStringValueForName:@"type"]; + if(type) + return [type lowercaseString]; + else + return @"available"; +} + +- (NSString *)show +{ + return [[self elementForName:@"show"] stringValue]; +} + +- (NSString *)status +{ + return [[self elementForName:@"status"] stringValue]; +} + +- (int)priority +{ + return [[[self elementForName:@"priority"] stringValue] intValue]; +} + +- (int)intShow +{ + NSString *show = [self show]; + + if([show isEqualToString:@"dnd"]) + return 0; + if([show isEqualToString:@"xa"]) + return 1; + if([show isEqualToString:@"away"]) + return 2; + if([show isEqualToString:@"chat"]) + return 4; + + return 3; +} + +- (BOOL)isErrorPresence +{ + return [[self type] isEqualToString:@"error"]; +} + +@end diff --git a/Core/XMPPStream.h b/Core/XMPPStream.h new file mode 100644 index 0000000..39d4c0d --- /dev/null +++ b/Core/XMPPStream.h @@ -0,0 +1,1111 @@ +#import +#import "XMPPSASLAuthentication.h" +#import "XMPPCustomBinding.h" +#import "GCDMulticastDelegate.h" +#import "CocoaAsyncSocket/GCDAsyncSocket.h" + +#if TARGET_OS_IPHONE + #import "DDXML.h" +#endif + +@class XMPPSRVResolver; +@class XMPPParser; +@class XMPPJID; +@class XMPPIQ; +@class XMPPMessage; +@class XMPPPresence; +@class XMPPModule; +@class XMPPElement; +@class XMPPElementReceipt; +@protocol XMPPStreamDelegate; + +#if TARGET_OS_IPHONE + #define MIN_KEEPALIVE_INTERVAL 20.0 // 20 Seconds + #define DEFAULT_KEEPALIVE_INTERVAL 120.0 // 2 Minutes +#else + #define MIN_KEEPALIVE_INTERVAL 10.0 // 10 Seconds + #define DEFAULT_KEEPALIVE_INTERVAL 300.0 // 5 Minutes +#endif + +extern NSString *const XMPPStreamErrorDomain; + +typedef NS_ENUM(NSUInteger, XMPPStreamErrorCode) { + XMPPStreamInvalidType, // Attempting to access P2P methods in a non-P2P stream, or vice-versa + XMPPStreamInvalidState, // Invalid state for requested action, such as connect when already connected + XMPPStreamInvalidProperty, // Missing a required property, such as myJID + XMPPStreamInvalidParameter, // Invalid parameter, such as a nil JID + XMPPStreamUnsupportedAction, // The server doesn't support the requested action +}; + +typedef NS_ENUM(NSUInteger, XMPPStreamStartTLSPolicy) { + XMPPStreamStartTLSPolicyAllowed, // TLS will be used if the server requires it + XMPPStreamStartTLSPolicyPreferred, // TLS will be used if the server offers it + XMPPStreamStartTLSPolicyRequired // TLS will be used if the server offers it, else the stream won't connect +}; + +extern const NSTimeInterval XMPPStreamTimeoutNone; + +@interface XMPPStream : NSObject + +/** + * Standard XMPP initialization. + * The stream is a standard client to server connection. + * + * P2P streams using XEP-0174 are also supported. + * See the P2P section below. +**/ +- (id)init; + +/** + * Peer to Peer XMPP initialization. + * The stream is a direct client to client connection as outlined in XEP-0174. +**/ +- (id)initP2PFrom:(XMPPJID *)myJID; + +/** + * XMPPStream uses a multicast delegate. + * This allows one to add multiple delegates to a single XMPPStream instance, + * which makes it easier to separate various components and extensions. + * + * For example, if you were implementing two different custom extensions on top of XMPP, + * you could put them in separate classes, and simply add each as a delegate. +**/ +- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; +- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; +- (void)removeDelegate:(id)delegate; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The server's hostname that should be used to make the TCP connection. + * This may be a domain name (e.g. "deusty.com") or an IP address (e.g. "70.85.193.226"). + * + * Note that this may be different from the virtual xmpp hostname. + * Just as HTTP servers can support mulitple virtual hosts from a single server, so too can xmpp servers. + * A prime example is google via google apps. + * + * For example, say you own the domain "mydomain.com". + * If you go to mydomain.com in a web browser, + * you are directed to your apache server running on your webserver somewhere in the cloud. + * But you use google apps for your email and xmpp needs. + * So if somebody sends you an email, it actually goes to google's servers, where you later access it from. + * Similarly, you connect to google's servers to sign into xmpp. + * + * In the example above, your hostname is "talk.google.com" and your JID is "me@mydomain.com". + * + * This hostName property is optional. + * If you do not set the hostName, then the framework will follow the xmpp specification using jid's domain. + * That is, it first do an SRV lookup (as specified in the xmpp RFC). + * If that fails, it will fall back to simply attempting to connect to the jid's domain. +**/ +@property (readwrite, copy) NSString *hostName; + +/** + * The port the xmpp server is running on. + * If you do not explicitly set the port, the default port will be used. + * If you set the port to zero, the default port will be used. + * + * The default port is 5222. +**/ +@property (readwrite, assign) UInt16 hostPort; + +/** + * The stream's policy on when to Start TLS. + * + * The default is XMPPStreamStartTLSPolicyAllowed. + * + * @see XMPPStreamStartTLSPolicy +**/ +@property (readwrite, assign) XMPPStreamStartTLSPolicy startTLSPolicy; + +/** + * The JID of the user. + * + * This value is required, and is used in many parts of the underlying implementation. + * When connecting, the domain of the JID is used to properly specify the correct xmpp virtual host. + * It is used during registration to supply the username of the user to create an account for. + * It is used during authentication to supply the username of the user to authenticate with. + * And the resource may be used post-authentication during the required xmpp resource binding step. + * + * A proper JID is of the form user@domain/resource. + * For example: robbiehanson@deusty.com/work + * + * The resource is optional, in the sense that if one is not supplied, + * one will be automatically generated for you (either by us or by the server). + * + * Please note: + * Resource collisions are handled in different ways depending on server configuration. + * + * For example: + * You are signed in with user1@domain.com/home on your desktop. + * Then you attempt to sign in with user1@domain.com/home on your laptop. + * + * The server could possibly: + * - Reject the resource request for the laptop. + * - Accept the resource request for the laptop, and immediately disconnect the desktop. + * - Automatically assign the laptop another resource without a conflict. + * + * For this reason, you may wish to check the myJID variable after the stream has been connected, + * just in case the resource was changed by the server. +**/ +@property (readwrite, copy) XMPPJID *myJID; + +/** + * Only used in P2P streams. +**/ +@property (strong, readonly) XMPPJID *remoteJID; + +/** + * Many routers will teardown a socket mapping if there is no activity on the socket. + * For this reason, the xmpp stream supports sending keep-alive data. + * This is simply whitespace, which is ignored by the xmpp protocol. + * + * Keep-alive data is only sent in the absence of any other data being sent/received. + * + * The default value is defined in DEFAULT_KEEPALIVE_INTERVAL. + * The minimum value is defined in MIN_KEEPALIVE_INTERVAL. + * + * To disable keep-alive, set the interval to zero (or any non-positive number). + * + * The keep-alive timer (if enabled) fires every (keepAliveInterval / 4) seconds. + * Upon firing it checks when data was last sent/received, + * and sends keep-alive data if the elapsed time has exceeded the keepAliveInterval. + * Thus the effective resolution of the keepalive timer is based on the interval. + * + * @see keepAliveWhitespaceCharacter +**/ +@property (readwrite, assign) NSTimeInterval keepAliveInterval; + +/** + * The keep-alive mechanism sends whitespace which is ignored by the xmpp protocol. + * The default whitespace character is a space (' '). + * + * This can be changed, for whatever reason, to another whitespace character. + * Valid whitespace characters are space(' '), tab('\t') and newline('\n'). + * + * If you attempt to set the character to any non-whitespace character, the attempt is ignored. + * + * @see keepAliveInterval +**/ +@property (readwrite, assign) char keepAliveWhitespaceCharacter; + +/** + * Represents the last sent presence element concerning the presence of myJID on the server. + * In other words, it represents the presence as others see us. + * + * This excludes presence elements sent concerning subscriptions, MUC rooms, etc. + * + * @see resendMyPresence +**/ +@property (strong, readonly) XMPPPresence *myPresence; + +/** + * Returns the total number of bytes bytes sent/received by the xmpp stream. + * + * By default this is the byte count since the xmpp stream object has been created. + * If the stream has connected/disconnected/reconnected multiple times, + * the count will be the summation of all connections. + * + * The functionality may optionaly be changed to count only the current socket connection. + * @see resetByteCountPerConnection +**/ +@property (readonly) uint64_t numberOfBytesSent; +@property (readonly) uint64_t numberOfBytesReceived; + +/** + * Same as the individual properties, + * but provides a way to fetch them in one atomic operation. +**/ +- (void)getNumberOfBytesSent:(uint64_t *)bytesSentPtr numberOfBytesReceived:(uint64_t *)bytesReceivedPtr; + +/** + * Affects the funtionality of the byte counter. + * + * The default value is NO. + * + * If set to YES, the byte count will be reset just prior to a new connection (in the connect methods). +**/ +@property (readwrite, assign) BOOL resetByteCountPerConnection; + +/** + * The tag property allows you to associate user defined information with the stream. + * Tag values are not used internally, and should not be used by xmpp modules. +**/ +@property (readwrite, strong) id tag; + +/** + * RFC 6121 states that starting a session is no longer required. + * To skip this step set skipStartSession to YES. + * + * [RFC3921] specified one additional + * precondition: formal establishment of an instant messaging and + * presence session. Implementation and deployment experience has + * shown that this additional step is unnecessary. However, for + * backward compatibility an implementation MAY still offer that + * feature. This enables older software to connect while letting + * newer software save a round trip. + * + * The default value is NO. +**/ +@property (readwrite, assign) BOOL skipStartSession; + +/** + * Validates that a response element is FROM the jid that the request element was sent TO. + * Supports validating responses when request didn't specify a TO. + * + * @see isValidResponseElementFrom:forRequestElementTo: + * @see isValidResponseElement:forRequestElement: + * + * The default value is NO. +**/ +@property (readwrite, assign) BOOL validatesResponses; + +#if TARGET_OS_IPHONE + +/** + * If set, the kCFStreamNetworkServiceTypeVoIP flags will be set on the underlying CFRead/Write streams. + * + * The default value is NO. +**/ +@property (readwrite, assign) BOOL enableBackgroundingOnSocket; + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark State +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns YES if the connection is closed, and thus no stream is open. + * If the stream is neither disconnected, nor connected, then a connection is currently being established. +**/ +- (BOOL)isDisconnected; + +/** + * Returns YES is the connection is currently connecting +**/ +- (BOOL)isConnecting; + +/** + * Returns YES if the connection is open, and the stream has been properly established. + * If the stream is neither disconnected, nor connected, then a connection is currently being established. + * + * If this method returns YES, then it is ready for you to start sending and receiving elements. +**/ +- (BOOL)isConnected; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Connect & Disconnect +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Connects to the configured hostName on the configured hostPort. + * The timeout is optional. To not time out use XMPPStreamTimeoutNone. + * If the hostName or myJID are not set, this method will return NO and set the error parameter. +**/ +- (BOOL)connectWithTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +/** + * THIS IS DEPRECATED BY THE XMPP SPECIFICATION. + * + * The xmpp specification outlines the proper use of SSL/TLS by negotiating + * the startTLS upgrade within the stream negotiation. + * This method exists for those ancient servers that still require the connection to be secured prematurely. + * The timeout is optional. To not time out use XMPPStreamTimeoutNone. + * + * Note: Such servers generally use port 5223 for this, which you will need to set. +**/ +- (BOOL)oldSchoolSecureConnectWithTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +/** + * Starts a P2P connection to the given user and given address. + * The timeout is optional. To not time out use XMPPStreamTimeoutNone. + * This method only works with XMPPStream objects created using the initP2P method. + * + * The given address is specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetservice's addresses method. +**/ +- (BOOL)connectTo:(XMPPJID *)remoteJID + withAddress:(NSData *)remoteAddr + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Starts a P2P connection with the given accepted socket. + * This method only works with XMPPStream objects created using the initP2P method. + * + * The given socket should be a socket that has already been accepted. + * The remoteJID will be extracted from the opening stream negotiation. +**/ +- (BOOL)connectP2PWithSocket:(GCDAsyncSocket *)acceptedSocket error:(NSError **)errPtr; + +/** + * Disconnects from the remote host by closing the underlying TCP socket connection. + * The terminating element is not sent to the server. + * + * This method is synchronous. + * Meaning that the disconnect will happen immediately, even if there are pending elements yet to be sent. + * + * The xmppStreamDidDisconnect:withError: delegate method will immediately be dispatched onto the delegate queue. +**/ +- (void)disconnect; + +/** + * Disconnects from the remote host by sending the terminating element, + * and then closing the underlying TCP socket connection. + * + * This method is asynchronous. + * The disconnect will happen after all pending elements have been sent. + * Attempting to send elements after this method has been called will not work (the elements won't get sent). +**/ +- (void)disconnectAfterSending; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns YES if SSL/TLS was used to establish a connection to the server. + * + * Some servers may require an "upgrade to TLS" in order to start communication, + * so even if the connection was not explicitly secured, an ugrade to TLS may have occured. + * + * See also the xmppStream:willSecureWithSettings: delegate method. +**/ +- (BOOL)isSecure; + +/** + * Returns whether or not the server supports securing the connection via SSL/TLS. + * + * Some servers will actually require a secure connection, + * in which case the stream will attempt to secure the connection during the opening process. + * + * If the connection has already been secured, this method may return NO. +**/ +- (BOOL)supportsStartTLS; + +/** + * Attempts to secure the connection via SSL/TLS. + * + * This method is asynchronous. + * The SSL/TLS handshake will occur in the background, and + * the xmppStreamDidSecure: delegate method will be called after the TLS process has completed. + * + * This method returns immediately. + * If the secure process was started, it will return YES. + * If there was an issue while starting the security process, + * this method will return NO and set the error parameter. + * + * The errPtr parameter is optional - you may pass nil. + * + * You may wish to configure the security settings via the xmppStream:willSecureWithSettings: delegate method. + * + * If the SSL/TLS handshake fails, the connection will be closed. + * The reason for the error will be reported via the xmppStreamDidDisconnect:withError: delegate method. + * The error parameter will be an NSError object, and may have an error domain of kCFStreamErrorDomainSSL. + * The corresponding error code is documented in Apple's Security framework, in SecureTransport.h +**/ +- (BOOL)secureConnection:(NSError **)errPtr; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Registration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * In Band Registration. + * Creating a user account on the xmpp server within the xmpp protocol. + * + * The registerWithElements:error: method is asynchronous. + * It will return immediately, and the delegate methods are used to determine success. + * See the xmppStreamDidRegister: and xmppStream:didNotRegister: methods. + * + * If there is something immediately wrong, such as the stream is not connected, + * this method will return NO and set the error. + * + * The errPtr parameter is optional - you may pass nil. + * + * registerWithPassword:error: is a convience method for creating an account using the given username and password. + * + * Security Note: + * The password will be sent in the clear unless the stream has been secured. +**/ +- (BOOL)supportsInBandRegistration; +- (BOOL)registerWithElements:(NSArray *)elements error:(NSError **)errPtr; +- (BOOL)registerWithPassword:(NSString *)password error:(NSError **)errPtr; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Authentication +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns the server's list of supported authentication mechanisms. + * Each item in the array will be of type NSString. + * + * For example, if the server supplied this stanza within it's reported stream:features: + * + * + * DIGEST-MD5 + * PLAIN + * + * + * Then this method would return [@"DIGEST-MD5", @"PLAIN"]. +**/ +- (NSArray *)supportedAuthenticationMechanisms; + +/** + * Returns whether or not the given authentication mechanism name was specified in the + * server's list of supported authentication mechanisms. + * + * Note: The authentication classes often provide a category on XMPPStream, adding useful methods. + * + * @see XMPPPlainAuthentication - supportsPlainAuthentication + * @see XMPPDigestMD5Authentication - supportsDigestMD5Authentication + * @see XMPPXFacebookPlatformAuthentication - supportsXFacebookPlatformAuthentication + * @see XMPPDeprecatedPlainAuthentication - supportsDeprecatedPlainAuthentication + * @see XMPPDeprecatedDigestAuthentication - supportsDeprecatedDigestAuthentication +**/ +- (BOOL)supportsAuthenticationMechanism:(NSString *)mechanism; + +/** + * This is the root authentication method. + * All other authentication methods go through this one. + * + * This method attempts to start the authentication process given the auth instance. + * That is, this method will invoke start: on the given auth instance. + * If it returns YES, then the stream will enter into authentication mode. + * It will then continually invoke the handleAuth: method on the given instance until authentication is complete. + * + * This method is asynchronous. + * + * If there is something immediately wrong, such as the stream is not connected, + * the method will return NO and set the error. + * Otherwise the delegate callbacks are used to communicate auth success or failure. + * + * @see xmppStreamDidAuthenticate: + * @see xmppStream:didNotAuthenticate: + * + * @see authenticateWithPassword:error: + * + * Note: The security process is abstracted in order to provide flexibility, + * and allow developers to easily implement their own custom authentication protocols. + * The authentication classes often provide a category on XMPPStream, adding useful methods. + * + * @see XMPPXFacebookPlatformAuthentication - authenticateWithFacebookAccessToken:error: +**/ +- (BOOL)authenticate:(id )auth error:(NSError **)errPtr; + +/** + * This method applies to standard password authentication schemes only. + * This is NOT the primary authentication method. + * + * @see authenticate:error: + * + * This method exists for backwards compatibility, and may disappear in future versions. +**/ +- (BOOL)authenticateWithPassword:(NSString *)password error:(NSError **)errPtr; + +/** + * Returns whether or not the xmpp stream is currently authenticating with the XMPP Server. +**/ +- (BOOL)isAuthenticating; + +/** + * Returns whether or not the xmpp stream has successfully authenticated with the server. +**/ +- (BOOL)isAuthenticated; + +/** + * Returns the date when the xmpp stream successfully authenticated with the server. + **/ +- (NSDate *)authenticationDate; + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Compression +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + +/** + * Returns the server's list of supported compression methods in accordance to XEP-0138: Stream Compression + * Each item in the array will be of type NSString. + * + * For example, if the server supplied this stanza within it's reported stream:features: + * + * + * zlib + * lzw + * + * + * Then this method would return [@"zlib", @"lzw"]. + **/ +- (NSArray *)supportedCompressionMethods; + + +/** + * Returns whether or not the given compression method name was specified in the + * server's list of supported compression methods. + * + * Note: The XMPPStream doesn't currently support any compression methods +**/ + +- (BOOL)supportsCompressionMethod:(NSString *)compressionMethod; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Server Info +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method will return the root element of the document. + * This element contains the opening and tags received from the server. + * + * If multiple have been received during the course of stream negotiation, + * the root element contains only the most recent (current) version. + * + * Note: The rootElement is "empty", in-so-far as it does not contain all the XML elements the stream has + * received during it's connection. This is done for performance reasons and for the obvious benefit + * of being more memory efficient. +**/ +- (NSXMLElement *)rootElement; + +/** + * Returns the version attribute from the servers's element. + * This should be at least 1.0 to be RFC 3920 compliant. + * If no version number was set, the server is not RFC compliant, and 0 is returned. +**/ +- (float)serverXmppStreamVersionNumber; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Sending +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Sends the given XML element. + * If the stream is not yet connected, this method does nothing. +**/ +- (void)sendElement:(NSXMLElement *)element; + +/** + * Just like the sendElement: method above, + * but allows you to receive a receipt that can later be used to verify the element has been sent. + * + * If you later want to check to see if the element has been sent: + * + * if ([receipt wait:0]) { + * // Element has been sent + * } + * + * If you later want to wait until the element has been sent: + * + * if ([receipt wait:-1]) { + * // Element was sent + * } else { + * // Element failed to send due to disconnection + * } + * + * It is important to understand what it means when [receipt wait:timeout] returns YES. + * It does NOT mean the server has received the element. + * It only means the data has been queued for sending in the underlying OS socket buffer. + * + * So at this point the OS will do everything in its capacity to send the data to the server, + * which generally means the server will eventually receive the data. + * Unless, of course, something horrible happens such as a network failure, + * or a system crash, or the server crashes, etc. + * + * Even if you close the xmpp stream after this point, the OS will still do everything it can to send the data. +**/ +- (void)sendElement:(NSXMLElement *)element andGetReceipt:(XMPPElementReceipt **)receiptPtr; + +/** + * Fetches and resends the myPresence element (if available) in a single atomic operation. + * + * There are various xmpp extensions that hook into the xmpp stream and append information to outgoing presence stanzas. + * For example, the XMPPCapabilities module automatically appends capabilities information (as a hash). + * When these modules need to update/change their appended information, + * they should use this method to do so. + * + * The alternative is to fetch the myPresence element, and resend it manually using the sendElement method. + * However, that is 2 seperate operations, and the user, may send a different presence element inbetween. + * Using this method guarantees everything is done as an atomic operation. +**/ +- (void)resendMyPresence; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Stanza Validation +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Validates that a response element is FROM the jid that the request element was sent TO. + * Supports validating responses when request didn't specify a TO. +**/ +- (BOOL)isValidResponseElementFrom:(XMPPJID *)from forRequestElementTo:(XMPPJID *)to; + +- (BOOL)isValidResponseElement:(XMPPElement *)response forRequestElement:(XMPPElement *)request; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Module Plug-In System +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The XMPPModule class automatically invokes these methods when it is activated/deactivated. + * + * The registerModule method registers the module with the xmppStream. + * If there are any other modules that have requested to be automatically added as delegates to modules of this type, + * then those modules are automatically added as delegates during the asynchronous execution of this method. + * + * The registerModule method is asynchronous. + * + * The unregisterModule method unregisters the module with the xmppStream, + * and automatically removes it as a delegate of any other module. + * + * The unregisterModule method is fully synchronous. + * That is, after this method returns, the module will not be scheduled in any more delegate calls from other modules. + * However, if the module was already scheduled in an existing asynchronous delegate call from another module, + * the scheduled delegate invocation remains queued and will fire in the near future. + * Since the delegate invocation is already queued, + * the module's retainCount has been incremented, + * and the module will not be deallocated until after the delegate invocation has fired. +**/ +- (void)registerModule:(XMPPModule *)module; +- (void)unregisterModule:(XMPPModule *)module; + +/** + * Automatically registers the given delegate with all current and future registered modules of the given class. + * + * That is, the given delegate will be added to the delegate list ([module addDelegate:delegate delegateQueue:dq]) to + * all current and future registered modules that respond YES to [module isKindOfClass:aClass]. + * + * This method is used by modules to automatically integrate with other modules. + * For example, a module may auto-add itself as a delegate to XMPPCapabilities + * so that it can broadcast its implemented features. + * + * This may also be useful to clients, for example, to add a delegate to instances of something like XMPPChatRoom, + * where there may be multiple instances of the module that get created during the course of an xmpp session. + * + * If you auto register on multiple queues, you can remove all registrations with a single + * call to removeAutoDelegate::: by passing NULL as the 'dq' parameter. + * + * If you auto register for multiple classes, you can remove all registrations with a single + * call to removeAutoDelegate::: by passing nil as the 'aClass' parameter. +**/ +- (void)autoAddDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue toModulesOfClass:(Class)aClass; +- (void)removeAutoDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue fromModulesOfClass:(Class)aClass; + +/** + * Allows for enumeration of the currently registered modules. + * + * This may be useful if the stream needs to be queried for modules of a particular type. +**/ +- (void)enumerateModulesWithBlock:(void (^)(XMPPModule *module, NSUInteger idx, BOOL *stop))block; + +/** + * Allows for enumeration of the currently registered modules that are a kind of Class. + * idx is in relation to all modules not just those of the given class. +**/ +- (void)enumerateModulesOfClass:(Class)aClass withBlock:(void (^)(XMPPModule *module, NSUInteger idx, BOOL *stop))block; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Generates and returns a new autoreleased UUID. + * UUIDs (Universally Unique Identifiers) may also be known as GUIDs (Globally Unique Identifiers). + * + * The UUID is generated using the CFUUID library, which generates a unique 128 bit value. + * The uuid is then translated into a string using the standard format for UUIDs: + * "68753A44-4D6F-1226-9C60-0050E4C00067" + * + * This method is most commonly used to generate a unique id value for an xmpp element. +**/ ++ (NSString *)generateUUID; +- (NSString *)generateUUID; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPElementReceipt : NSObject +{ + uint32_t atomicFlags; + dispatch_semaphore_t semaphore; +} + +/** + * Element receipts allow you to check to see if the element has been sent. + * The timeout parameter allows you to do any of the following: + * + * - Do an instantaneous check (pass timeout == 0) + * - Wait until the element has been sent (pass timeout < 0) + * - Wait up to a certain amount of time (pass timeout > 0) + * + * It is important to understand what it means when [receipt wait:timeout] returns YES. + * It does NOT mean the server has received the element. + * It only means the data has been queued for sending in the underlying OS socket buffer. + * + * So at this point the OS will do everything in its capacity to send the data to the server, + * which generally means the server will eventually receive the data. + * Unless, of course, something horrible happens such as a network failure, + * or a system crash, or the server crashes, etc. + * + * Even if you close the xmpp stream after this point, the OS will still do everything it can to send the data. +**/ +- (BOOL)wait:(NSTimeInterval)timeout; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPStreamDelegate +@optional + +/** + * This method is called before the stream begins the connection process. + * + * If developing an iOS app that runs in the background, this may be a good place to indicate + * that this is a task that needs to continue running in the background. +**/ +- (void)xmppStreamWillConnect:(XMPPStream *)sender; + +/** + * This method is called after the tcp socket has connected to the remote host. + * It may be used as a hook for various things, such as updating the UI or extracting the server's IP address. + * + * If developing an iOS app that runs in the background, + * please use XMPPStream's enableBackgroundingOnSocket property as opposed to doing it directly on the socket here. +**/ +- (void)xmppStream:(XMPPStream *)sender socketDidConnect:(GCDAsyncSocket *)socket; + +/** + * This method is called after a TCP connection has been established with the server, + * and the opening XML stream negotiation has started. +**/ +- (void)xmppStreamDidStartNegotiation:(XMPPStream *)sender; + +/** + * This method is called immediately prior to the stream being secured via TLS/SSL. + * Note that this delegate may be called even if you do not explicitly invoke the startTLS method. + * Servers have the option of requiring connections to be secured during the opening process. + * If this is the case, the XMPPStream will automatically attempt to properly secure the connection. + * + * The dictionary of settings is what will be passed to the startTLS method of the underlying GCDAsyncSocket. + * The GCDAsyncSocket header file contains a discussion of the available key/value pairs, + * as well as the security consequences of various options. + * It is recommended reading if you are planning on implementing this method. + * + * The dictionary of settings that are initially passed will be an empty dictionary. + * If you choose not to implement this method, or simply do not edit the dictionary, + * then the default settings will be used. + * That is, the kCFStreamSSLPeerName will be set to the configured host name, + * and the default security validation checks will be performed. + * + * This means that authentication will fail if the name on the X509 certificate of + * the server does not match the value of the hostname for the xmpp stream. + * It will also fail if the certificate is self-signed, or if it is expired, etc. + * + * These settings are most likely the right fit for most production environments, + * but may need to be tweaked for development or testing, + * where the development server may be using a self-signed certificate. + * + * Note: If your development server is using a self-signed certificate, + * you likely need to add GCDAsyncSocketManuallyEvaluateTrust=YES to the settings. + * Then implement the xmppStream:didReceiveTrust:completionHandler: delegate method to perform custom validation. +**/ +- (void)xmppStream:(XMPPStream *)sender willSecureWithSettings:(NSMutableDictionary *)settings; + +/** + * Allows a delegate to hook into the TLS handshake and manually validate the peer it's connecting to. + * + * This is only called if the stream is secured with settings that include: + * - GCDAsyncSocketManuallyEvaluateTrust == YES + * That is, if a delegate implements xmppStream:willSecureWithSettings:, and plugs in that key/value pair. + * + * Thus this delegate method is forwarding the TLS evaluation callback from the underlying GCDAsyncSocket. + * + * Typically the delegate will use SecTrustEvaluate (and related functions) to properly validate the peer. + * + * Note from Apple's documentation: + * Because [SecTrustEvaluate] might look on the network for certificates in the certificate chain, + * [it] might block while attempting network access. You should never call it from your main thread; + * call it only from within a function running on a dispatch queue or on a separate thread. + * + * This is why this method uses a completionHandler block rather than a normal return value. + * The idea is that you should be performing SecTrustEvaluate on a background thread. + * The completionHandler block is thread-safe, and may be invoked from a background queue/thread. + * It is safe to invoke the completionHandler block even if the socket has been closed. + * + * Keep in mind that you can do all kinds of cool stuff here. + * For example: + * + * If your development server is using a self-signed certificate, + * then you could embed info about the self-signed cert within your app, and use this callback to ensure that + * you're actually connecting to the expected dev server. + * + * Also, you could present certificates that don't pass SecTrustEvaluate to the client. + * That is, if SecTrustEvaluate comes back with problems, you could invoke the completionHandler with NO, + * and then ask the client if the cert can be trusted. This is similar to how most browsers act. + * + * Generally, only one delegate should implement this method. + * However, if multiple delegates implement this method, then the first to invoke the completionHandler "wins". + * And subsequent invocations of the completionHandler are ignored. +**/ +- (void)xmppStream:(XMPPStream *)sender didReceiveTrust:(SecTrustRef)trust + completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler; + +/** + * This method is called after the stream has been secured via SSL/TLS. + * This method may be called if the server required a secure connection during the opening process, + * or if the secureConnection: method was manually invoked. +**/ +- (void)xmppStreamDidSecure:(XMPPStream *)sender; + +/** + * This method is called after the XML stream has been fully opened. + * More precisely, this method is called after an opening and tag have been sent and received, + * and after the stream features have been received, and any required features have been fullfilled. + * At this point it's safe to begin communication with the server. +**/ +- (void)xmppStreamDidConnect:(XMPPStream *)sender; + +/** + * This method is called after registration of a new user has successfully finished. + * If registration fails for some reason, the xmppStream:didNotRegister: method will be called instead. +**/ +- (void)xmppStreamDidRegister:(XMPPStream *)sender; + +/** + * This method is called if registration fails. +**/ +- (void)xmppStream:(XMPPStream *)sender didNotRegister:(NSXMLElement *)error; + +/** + * This method is called after authentication has successfully finished. + * If authentication fails for some reason, the xmppStream:didNotAuthenticate: method will be called instead. +**/ +- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender; + +/** + * This method is called if authentication fails. +**/ +- (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(NSXMLElement *)error; + +/** + * Binding a JID resource is a standard part of the authentication process, + * and occurs after SASL authentication completes (which generally authenticates the JID username). + * + * This delegate method allows for a custom binding procedure to be used. + * For example: + * - a custom SASL authentication scheme might combine auth with binding + * - stream management (xep-0198) replaces binding if it can resume a previous session + * + * Return nil (or don't implement this method) if you wish to use the standard binding procedure. +**/ +- (id )xmppStreamWillBind:(XMPPStream *)sender; + +/** + * This method is called if the XMPP server doesn't allow our resource of choice + * because it conflicts with an existing resource. + * + * Return an alternative resource or return nil to let the server automatically pick a resource for us. +**/ +- (NSString *)xmppStream:(XMPPStream *)sender alternativeResourceForConflictingResource:(NSString *)conflictingResource; + +/** + * These methods are called before their respective XML elements are broadcast as received to the rest of the stack. + * These methods can be used to modify elements on the fly. + * (E.g. perform custom decryption so the rest of the stack sees readable text.) + * + * You may also filter incoming elements by returning nil. + * + * When implementing these methods to modify the element, you do not need to copy the given element. + * You can simply edit the given element, and return it. + * The reason these methods return an element, instead of void, is to allow filtering. + * + * Concerning thread-safety, delegates implementing the method are invoked one-at-a-time to + * allow thread-safe modification of the given elements. + * + * You should NOT implement these methods unless you have good reason to do so. + * For general processing and notification of received elements, please use xmppStream:didReceiveX: methods. + * + * @see xmppStream:didReceiveIQ: + * @see xmppStream:didReceiveMessage: + * @see xmppStream:didReceivePresence: +**/ +- (XMPPIQ *)xmppStream:(XMPPStream *)sender willReceiveIQ:(XMPPIQ *)iq; +- (XMPPMessage *)xmppStream:(XMPPStream *)sender willReceiveMessage:(XMPPMessage *)message; +- (XMPPPresence *)xmppStream:(XMPPStream *)sender willReceivePresence:(XMPPPresence *)presence; + +/** + * This method is called if any of the xmppStream:willReceiveX: methods filter the incoming stanza. + * + * It may be useful for some extensions to know that something was received, + * even if it was filtered for some reason. +**/ +- (void)xmppStreamDidFilterStanza:(XMPPStream *)sender; + +/** + * These methods are called after their respective XML elements are received on the stream. + * + * In the case of an IQ, the delegate method should return YES if it has or will respond to the given IQ. + * If the IQ is of type 'get' or 'set', and no delegates respond to the IQ, + * then xmpp stream will automatically send an error response. + * + * Concerning thread-safety, delegates shouldn't modify the given elements. + * As documented in NSXML / KissXML, elements are read-access thread-safe, but write-access thread-unsafe. + * If you have need to modify an element for any reason, + * you should copy the element first, and then modify and use the copy. +**/ +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq; +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message; +- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence; + +/** + * This method is called if an XMPP error is received. + * In other words, a . + * + * However, this method may also be called for any unrecognized xml stanzas. + * + * Note that standard errors ( for example) are delivered normally, + * via the other didReceive...: methods. +**/ +- (void)xmppStream:(XMPPStream *)sender didReceiveError:(NSXMLElement *)error; + +/** + * These methods are called before their respective XML elements are sent over the stream. + * These methods can be used to modify outgoing elements on the fly. + * (E.g. add standard information for custom protocols.) + * + * You may also filter outgoing elements by returning nil. + * + * When implementing these methods to modify the element, you do not need to copy the given element. + * You can simply edit the given element, and return it. + * The reason these methods return an element, instead of void, is to allow filtering. + * + * Concerning thread-safety, delegates implementing the method are invoked one-at-a-time to + * allow thread-safe modification of the given elements. + * + * You should NOT implement these methods unless you have good reason to do so. + * For general processing and notification of sent elements, please use xmppStream:didSendX: methods. + * + * @see xmppStream:didSendIQ: + * @see xmppStream:didSendMessage: + * @see xmppStream:didSendPresence: +**/ +- (XMPPIQ *)xmppStream:(XMPPStream *)sender willSendIQ:(XMPPIQ *)iq; +- (XMPPMessage *)xmppStream:(XMPPStream *)sender willSendMessage:(XMPPMessage *)message; +- (XMPPPresence *)xmppStream:(XMPPStream *)sender willSendPresence:(XMPPPresence *)presence; + +/** + * These methods are called after their respective XML elements are sent over the stream. + * These methods may be used to listen for certain events (such as an unavailable presence having been sent), + * or for general logging purposes. (E.g. a central history logging mechanism). +**/ +- (void)xmppStream:(XMPPStream *)sender didSendIQ:(XMPPIQ *)iq; +- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message; +- (void)xmppStream:(XMPPStream *)sender didSendPresence:(XMPPPresence *)presence; + +/** + * These methods are called after failing to send the respective XML elements over the stream. + * This occurs when the stream gets disconnected before the element can get sent out. +**/ +- (void)xmppStream:(XMPPStream *)sender didFailToSendIQ:(XMPPIQ *)iq error:(NSError *)error; +- (void)xmppStream:(XMPPStream *)sender didFailToSendMessage:(XMPPMessage *)message error:(NSError *)error; +- (void)xmppStream:(XMPPStream *)sender didFailToSendPresence:(XMPPPresence *)presence error:(NSError *)error; + +/** + * This method is called if the XMPP Stream's jid changes. +**/ +- (void)xmppStreamDidChangeMyJID:(XMPPStream *)xmppStream; + +/** + * This method is called if the disconnect method is called. + * It may be used to determine if a disconnection was purposeful, or due to an error. + * + * Note: A disconnect may be either "clean" or "dirty". + * A "clean" disconnect is when the stream sends the closing stanza before disconnecting. + * A "dirty" disconnect is when the stream simply closes its TCP socket. + * In most cases it makes no difference how the disconnect occurs, + * but there are a few contexts in which the difference has various protocol implications. + * + * @see xmppStreamDidSendClosingStreamStanza +**/ +- (void)xmppStreamWasToldToDisconnect:(XMPPStream *)sender; + +/** + * This method is called after the stream has sent the closing stanza. + * This signifies a "clean" disconnect. + * + * Note: A disconnect may be either "clean" or "dirty". + * A "clean" disconnect is when the stream sends the closing stanza before disconnecting. + * A "dirty" disconnect is when the stream simply closes its TCP socket. + * In most cases it makes no difference how the disconnect occurs, + * but there are a few contexts in which the difference has various protocol implications. +**/ +- (void)xmppStreamDidSendClosingStreamStanza:(XMPPStream *)sender; + +/** + * This method is called if the XMPP stream's connect times out. +**/ +- (void)xmppStreamConnectDidTimeout:(XMPPStream *)sender; + +/** + * This method is called after the stream is closed. + * + * The given error parameter will be non-nil if the error was due to something outside the general xmpp realm. + * Some examples: + * - The TCP socket was unexpectedly disconnected. + * - The SRV resolution of the domain failed. + * - Error parsing xml sent from server. + * + * @see xmppStreamConnectDidTimeout: +**/ +- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error; + +/** + * This method is only used in P2P mode when the connectTo:withAddress: method was used. + * + * It allows the delegate to read the element if/when they arrive. + * Recall that the XEP specifies that SHOULD be sent. +**/ +- (void)xmppStream:(XMPPStream *)sender didReceiveP2PFeatures:(NSXMLElement *)streamFeatures; + +/** + * This method is only used in P2P mode when the connectTo:withSocket: method was used. + * + * It allows the delegate to customize the element, + * adding any specific featues the delegate might support. +**/ +- (void)xmppStream:(XMPPStream *)sender willSendP2PFeatures:(NSXMLElement *)streamFeatures; + +/** + * These methods are called as xmpp modules are registered and unregistered with the stream. + * This generally corresponds to xmpp modules being initailzed and deallocated. + * + * The methods may be useful, for example, if a more precise auto delegation mechanism is needed + * than what is available with the autoAddDelegate:toModulesOfClass: method. +**/ +- (void)xmppStream:(XMPPStream *)sender didRegisterModule:(id)module; +- (void)xmppStream:(XMPPStream *)sender willUnregisterModule:(id)module; + +/** + * Custom elements are Non-XMPP elements. + * In other words, not , or elements. + * + * Typically these kinds of elements are not allowed by the XMPP server. + * But some custom implementations may use them. + * The standard example is XEP-0198, which uses & elements. + * + * If you're using custom elements, you must register the custom element name(s). + * Otherwise the xmppStream will treat non-XMPP elements as errors (xmppStream:didReceiveError:). + * + * @see registerCustomElementNames (in XMPPInternal.h) +**/ +- (void)xmppStream:(XMPPStream *)sender didSendCustomElement:(NSXMLElement *)element; +- (void)xmppStream:(XMPPStream *)sender didReceiveCustomElement:(NSXMLElement *)element; + +@end diff --git a/Core/XMPPStream.m b/Core/XMPPStream.m new file mode 100644 index 0000000..1e329e9 --- /dev/null +++ b/Core/XMPPStream.m @@ -0,0 +1,5105 @@ +#import "XMPP.h" +#import "XMPPParser.h" +#import "XMPPLogging.h" +#import "XMPPInternal.h" +#import "XMPPIDTracker.h" +#import "XMPPSRVResolver.h" +#import "NSData+XMPP.h" + +#import +#import + +#if TARGET_OS_IPHONE + // Note: You may need to add the CFNetwork Framework to your project + #import +#endif + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO | XMPP_LOG_FLAG_SEND_RECV; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +/** + * Seeing a return statements within an inner block + * can sometimes be mistaken for a return point of the enclosing method. + * This makes inline blocks a bit easier to read. +**/ +#define return_from_block return + +// Define the timeouts (in seconds) for retreiving various parts of the XML stream +#define TIMEOUT_XMPP_WRITE -1 +#define TIMEOUT_XMPP_READ_START 10 +#define TIMEOUT_XMPP_READ_STREAM -1 + +// Define the tags we'll use to differentiate what it is we're currently reading or writing +#define TAG_XMPP_READ_START 100 +#define TAG_XMPP_READ_STREAM 101 +#define TAG_XMPP_WRITE_START 200 +#define TAG_XMPP_WRITE_STOP 201 +#define TAG_XMPP_WRITE_STREAM 202 +#define TAG_XMPP_WRITE_RECEIPT 203 + +// Define the timeouts (in seconds) for SRV +#define TIMEOUT_SRV_RESOLUTION 30.0 + +NSString *const XMPPStreamErrorDomain = @"XMPPStreamErrorDomain"; +NSString *const XMPPStreamDidChangeMyJIDNotification = @"XMPPStreamDidChangeMyJID"; + +const NSTimeInterval XMPPStreamTimeoutNone = -1; + +enum XMPPStreamFlags +{ + kP2PInitiator = 1 << 0, // If set, we are the P2P initializer + kIsSecure = 1 << 1, // If set, connection has been secured via SSL/TLS + kIsAuthenticated = 1 << 2, // If set, authentication has succeeded + kDidStartNegotiation = 1 << 3, // If set, negotiation has started at least once +}; + +enum XMPPStreamConfig +{ + kP2PMode = 1 << 0, // If set, the XMPPStream was initialized in P2P mode + kResetByteCountPerConnection = 1 << 1, // If set, byte count should be reset per connection +#if TARGET_OS_IPHONE + kEnableBackgroundingOnSocket = 1 << 2, // If set, the VoIP flag should be set on the socket +#endif +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPStream () +{ + dispatch_queue_t xmppQueue; + void *xmppQueueTag; + + dispatch_queue_t willSendIqQueue; + dispatch_queue_t willSendMessageQueue; + dispatch_queue_t willSendPresenceQueue; + + dispatch_queue_t willReceiveStanzaQueue; + + dispatch_queue_t didReceiveIqQueue; + + dispatch_source_t connectTimer; + + GCDMulticastDelegate *multicastDelegate; + + XMPPStreamState state; + + GCDAsyncSocket *asyncSocket; + + uint64_t numberOfBytesSent; + uint64_t numberOfBytesReceived; + + XMPPParser *parser; + NSError *parserError; + NSError *otherError; + + Byte flags; + Byte config; + + NSString *hostName; + UInt16 hostPort; + + XMPPStreamStartTLSPolicy startTLSPolicy; + BOOL skipStartSession; + BOOL validatesResponses; + + id auth; + id customBinding; + NSDate *authenticationDate; + + XMPPJID *myJID_setByClient; + XMPPJID *myJID_setByServer; + XMPPJID *remoteJID; + + XMPPPresence *myPresence; + NSXMLElement *rootElement; + + NSTimeInterval keepAliveInterval; + dispatch_source_t keepAliveTimer; + NSTimeInterval lastSendReceiveTime; + NSData *keepAliveData; + + NSMutableArray *registeredModules; + NSMutableDictionary *autoDelegateDict; + + XMPPSRVResolver *srvResolver; + NSArray *srvResults; + NSUInteger srvResultsIndex; + + XMPPIDTracker *idTracker; + + NSMutableArray *receipts; + NSCountedSet *customElementNames; + + id userTag; +} + +@end + +@interface XMPPElementReceipt (PrivateAPI) + +- (void)signalSuccess; +- (void)signalFailure; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPStream + +@synthesize tag = userTag; + +/** + * Shared initialization between the various init methods. +**/ +- (void)commonInit +{ + xmppQueueTag = &xmppQueueTag; + xmppQueue = dispatch_queue_create("xmpp", DISPATCH_QUEUE_SERIAL); + dispatch_queue_set_specific(xmppQueue, xmppQueueTag, xmppQueueTag, NULL); + + willSendIqQueue = dispatch_queue_create("xmpp.willSendIq", DISPATCH_QUEUE_SERIAL); + willSendMessageQueue = dispatch_queue_create("xmpp.willSendMessage", DISPATCH_QUEUE_SERIAL); + willSendPresenceQueue = dispatch_queue_create("xmpp.willSendPresence", DISPATCH_QUEUE_SERIAL); + + didReceiveIqQueue = dispatch_queue_create("xmpp.didReceiveIq", DISPATCH_QUEUE_SERIAL); + + multicastDelegate = (GCDMulticastDelegate *)[[GCDMulticastDelegate alloc] init]; + + state = STATE_XMPP_DISCONNECTED; + + flags = 0; + config = 0; + + numberOfBytesSent = 0; + numberOfBytesReceived = 0; + + hostPort = 5222; + keepAliveInterval = DEFAULT_KEEPALIVE_INTERVAL; + keepAliveData = [@" " dataUsingEncoding:NSUTF8StringEncoding]; + + registeredModules = [[NSMutableArray alloc] init]; + autoDelegateDict = [[NSMutableDictionary alloc] init]; + + idTracker = [[XMPPIDTracker alloc] initWithStream:self dispatchQueue:xmppQueue]; + + receipts = [[NSMutableArray alloc] init]; +} + +/** + * Standard XMPP initialization. + * The stream is a standard client to server connection. +**/ +- (id)init +{ + if ((self = [super init])) + { + // Common initialization + [self commonInit]; + + // Initialize socket + asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:xmppQueue]; + } + return self; +} + +/** + * Peer to Peer XMPP initialization. + * The stream is a direct client to client connection as outlined in XEP-0174. +**/ +- (id)initP2PFrom:(XMPPJID *)jid +{ + if ((self = [super init])) + { + // Common initialization + [self commonInit]; + + // Store JID + myJID_setByClient = jid; + + // We do not initialize the socket, since the connectP2PWithSocket: method might be used. + + // Initialize configuration + config = kP2PMode; + } + return self; +} + +/** + * Standard deallocation method. + * Every object variable declared in the header file should be released here. +**/ +- (void)dealloc +{ + #if !OS_OBJECT_USE_OBJC + dispatch_release(xmppQueue); + + dispatch_release(willSendIqQueue); + dispatch_release(willSendMessageQueue); + dispatch_release(willSendPresenceQueue); + + if (willReceiveStanzaQueue) { + dispatch_release(willReceiveStanzaQueue); + } + + dispatch_release(didReceiveIqQueue); + #endif + + [asyncSocket setDelegate:nil delegateQueue:NULL]; + [asyncSocket disconnect]; + + [parser setDelegate:nil delegateQueue:NULL]; + + if (keepAliveTimer) + { + dispatch_source_cancel(keepAliveTimer); + } + + [idTracker removeAllIDs]; + + for (XMPPElementReceipt *receipt in receipts) + { + [receipt signalFailure]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@synthesize xmppQueue; +@synthesize xmppQueueTag; + +- (XMPPStreamState)state +{ + __block XMPPStreamState result = STATE_XMPP_DISCONNECTED; + + dispatch_block_t block = ^{ + result = state; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (NSString *)hostName +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return hostName; + } + else + { + __block NSString *result; + + dispatch_sync(xmppQueue, ^{ + result = hostName; + }); + + return result; + } +} + +- (void)setHostName:(NSString *)newHostName +{ + if (dispatch_get_specific(xmppQueueTag)) + { + if (hostName != newHostName) + { + hostName = [newHostName copy]; + } + } + else + { + NSString *newHostNameCopy = [newHostName copy]; + + dispatch_async(xmppQueue, ^{ + hostName = newHostNameCopy; + }); + + } +} + +- (UInt16)hostPort +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return hostPort; + } + else + { + __block UInt16 result; + + dispatch_sync(xmppQueue, ^{ + result = hostPort; + }); + + return result; + } +} + +- (void)setHostPort:(UInt16)newHostPort +{ + dispatch_block_t block = ^{ + hostPort = newHostPort; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (XMPPStreamStartTLSPolicy)startTLSPolicy +{ + __block XMPPStreamStartTLSPolicy result; + + dispatch_block_t block = ^{ + result = startTLSPolicy; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)setStartTLSPolicy:(XMPPStreamStartTLSPolicy)flag +{ + dispatch_block_t block = ^{ + startTLSPolicy = flag; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (XMPPJID *)myJID +{ + __block XMPPJID *result = nil; + + dispatch_block_t block = ^{ + + if (myJID_setByServer) + result = myJID_setByServer; + else + result = myJID_setByClient; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)setMyJID_setByClient:(XMPPJID *)newMyJID +{ + // XMPPJID is an immutable class (copy == retain) + + dispatch_block_t block = ^{ + + if (![myJID_setByClient isEqualToJID:newMyJID]) + { + myJID_setByClient = newMyJID; + + if (myJID_setByServer == nil) + { + [[NSNotificationCenter defaultCenter] postNotificationName:XMPPStreamDidChangeMyJIDNotification + object:self]; + [multicastDelegate xmppStreamDidChangeMyJID:self]; + } + } + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (void)setMyJID_setByServer:(XMPPJID *)newMyJID +{ + // XMPPJID is an immutable class (copy == retain) + + dispatch_block_t block = ^{ + + if (![myJID_setByServer isEqualToJID:newMyJID]) + { + XMPPJID *oldMyJID; + if (myJID_setByServer) + oldMyJID = myJID_setByServer; + else + oldMyJID = myJID_setByClient; + + myJID_setByServer = newMyJID; + + if (![oldMyJID isEqualToJID:newMyJID]) + { + [[NSNotificationCenter defaultCenter] postNotificationName:XMPPStreamDidChangeMyJIDNotification + object:self]; + [multicastDelegate xmppStreamDidChangeMyJID:self]; + } + } + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (void)setMyJID:(XMPPJID *)newMyJID +{ + [self setMyJID_setByClient:newMyJID]; +} + +- (XMPPJID *)remoteJID +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return remoteJID; + } + else + { + __block XMPPJID *result; + + dispatch_sync(xmppQueue, ^{ + result = remoteJID; + }); + + return result; + } +} + +- (XMPPPresence *)myPresence +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return myPresence; + } + else + { + __block XMPPPresence *result; + + dispatch_sync(xmppQueue, ^{ + result = myPresence; + }); + + return result; + } +} + +- (NSTimeInterval)keepAliveInterval +{ + __block NSTimeInterval result = 0.0; + + dispatch_block_t block = ^{ + result = keepAliveInterval; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)setKeepAliveInterval:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ + + if (keepAliveInterval != interval) + { + if (interval <= 0.0) + keepAliveInterval = interval; + else + keepAliveInterval = MAX(interval, MIN_KEEPALIVE_INTERVAL); + + [self setupKeepAliveTimer]; + } + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (char)keepAliveWhitespaceCharacter +{ + __block char keepAliveChar = ' '; + + dispatch_block_t block = ^{ + + NSString *keepAliveString = [[NSString alloc] initWithData:keepAliveData encoding:NSUTF8StringEncoding]; + if ([keepAliveString length] > 0) + { + keepAliveChar = (char)[keepAliveString characterAtIndex:0]; + } + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return keepAliveChar; +} + +- (void)setKeepAliveWhitespaceCharacter:(char)keepAliveChar +{ + dispatch_block_t block = ^{ + + if (keepAliveChar == ' ' || keepAliveChar == '\n' || keepAliveChar == '\t') + { + keepAliveData = [[NSString stringWithFormat:@"%c", keepAliveChar] dataUsingEncoding:NSUTF8StringEncoding]; + } + else + { + XMPPLogWarn(@"Invalid whitespace character! Must be: space, newline, or tab"); + } + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (uint64_t)numberOfBytesSent +{ + __block uint64_t result = 0; + + dispatch_block_t block = ^{ + result = numberOfBytesSent; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (uint64_t)numberOfBytesReceived +{ + __block uint64_t result = 0; + + dispatch_block_t block = ^{ + result = numberOfBytesReceived; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)getNumberOfBytesSent:(uint64_t *)bytesSentPtr numberOfBytesReceived:(uint64_t *)bytesReceivedPtr +{ + __block uint64_t bytesSent = 0; + __block uint64_t bytesReceived = 0; + + dispatch_block_t block = ^{ + bytesSent = numberOfBytesSent; + bytesReceived = numberOfBytesReceived; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (bytesSentPtr) *bytesSentPtr = bytesSent; + if (bytesReceivedPtr) *bytesReceivedPtr = bytesReceived; +} + +- (BOOL)resetByteCountPerConnection +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (config & kResetByteCountPerConnection) ? YES : NO; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)setResetByteCountPerConnection:(BOOL)flag +{ + dispatch_block_t block = ^{ + if (flag) + config |= kResetByteCountPerConnection; + else + config &= ~kResetByteCountPerConnection; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (BOOL)skipStartSession +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = skipStartSession; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)setSkipStartSession:(BOOL)flag +{ + dispatch_block_t block = ^{ + skipStartSession = flag; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (BOOL)validatesResponses +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = validatesResponses; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)setValidatesResponses:(BOOL)flag +{ + dispatch_block_t block = ^{ + validatesResponses = flag; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +#if TARGET_OS_IPHONE + +- (BOOL)enableBackgroundingOnSocket +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (config & kEnableBackgroundingOnSocket) ? YES : NO; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)setEnableBackgroundingOnSocket:(BOOL)flag +{ + dispatch_block_t block = ^{ + if (flag) + config |= kEnableBackgroundingOnSocket; + else + config &= ~kEnableBackgroundingOnSocket; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue +{ + // Asynchronous operation (if outside xmppQueue) + + dispatch_block_t block = ^{ + [multicastDelegate addDelegate:delegate delegateQueue:delegateQueue]; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue +{ + // Synchronous operation + + dispatch_block_t block = ^{ + [multicastDelegate removeDelegate:delegate delegateQueue:delegateQueue]; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); +} + +- (void)removeDelegate:(id)delegate +{ + // Synchronous operation + + dispatch_block_t block = ^{ + [multicastDelegate removeDelegate:delegate]; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); +} + +/** + * Returns YES if the stream was opened in P2P mode. + * In other words, the stream was created via initP2PFrom: to use XEP-0174. +**/ +- (BOOL)isP2P +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return (config & kP2PMode) ? YES : NO; + } + else + { + __block BOOL result; + + dispatch_sync(xmppQueue, ^{ + result = (config & kP2PMode) ? YES : NO; + }); + + return result; + } +} + +- (BOOL)isP2PInitiator +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return ((config & kP2PMode) && (flags & kP2PInitiator)); + } + else + { + __block BOOL result; + + dispatch_sync(xmppQueue, ^{ + result = ((config & kP2PMode) && (flags & kP2PInitiator)); + }); + + return result; + } +} + +- (BOOL)isP2PRecipient +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return ((config & kP2PMode) && !(flags & kP2PInitiator)); + } + else + { + __block BOOL result; + + dispatch_sync(xmppQueue, ^{ + result = ((config & kP2PMode) && !(flags & kP2PInitiator)); + }); + + return result; + } +} + +- (BOOL)didStartNegotiation +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + return (flags & kDidStartNegotiation) ? YES : NO; +} + +- (void)setDidStartNegotiation:(BOOL)flag +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + if (flag) + flags |= kDidStartNegotiation; + else + flags &= ~kDidStartNegotiation; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Connection State +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns YES if the connection is closed, and thus no stream is open. + * If the stream is neither disconnected, nor connected, then a connection is currently being established. +**/ +- (BOOL)isDisconnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (state == STATE_XMPP_DISCONNECTED); + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +/** + * Returns YES is the connection is currently connecting + **/ + +- (BOOL)isConnecting +{ + XMPPLogTrace(); + + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool { + result = (state == STATE_XMPP_CONNECTING); + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} +/** + * Returns YES if the connection is open, and the stream has been properly established. + * If the stream is neither disconnected, nor connected, then a connection is currently being established. +**/ +- (BOOL)isConnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (state == STATE_XMPP_CONNECTED); + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Connect Timeout +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Start Connect Timeout +**/ +- (void)startConnectTimeout:(NSTimeInterval)timeout +{ + XMPPLogTrace(); + + if (timeout >= 0.0 && !connectTimer) + { + connectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, xmppQueue); + + dispatch_source_set_event_handler(connectTimer, ^{ @autoreleasepool { + + [self doConnectTimeout]; + }}); + +#if !OS_OBJECT_USE_OBJC + dispatch_source_t theConnectTimer = connectTimer; + dispatch_source_set_cancel_handler(connectTimer, ^{ + XMPPLogVerbose(@"%@: dispatch_release(connectTimer)", THIS_FILE); + dispatch_release(theConnectTimer); + }); +#endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC)); + dispatch_source_set_timer(connectTimer, tt, DISPATCH_TIME_FOREVER, 0); + + dispatch_resume(connectTimer); + } +} + +/** + * End Connect Timeout +**/ +- (void)endConnectTimeout +{ + XMPPLogTrace(); + + if (connectTimer) + { + dispatch_source_cancel(connectTimer); + connectTimer = NULL; + } +} + +/** + * Connect has timed out, so inform the delegates and close the connection +**/ +- (void)doConnectTimeout +{ + XMPPLogTrace(); + + [self endConnectTimeout]; + + if (state != STATE_XMPP_DISCONNECTED) + { + [multicastDelegate xmppStreamConnectDidTimeout:self]; + + if (state == STATE_XMPP_RESOLVING_SRV) + { + [srvResolver stop]; + srvResolver = nil; + + state = STATE_XMPP_DISCONNECTED; + } + else + { + [asyncSocket disconnect]; + + // Everthing will be handled in socketDidDisconnect:withError: + } + } + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark C2S Connection +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)connectToHost:(NSString *)host onPort:(UInt16)port withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + BOOL result = [asyncSocket connectToHost:host onPort:port error:errPtr]; + + if (result && [self resetByteCountPerConnection]) + { + numberOfBytesSent = 0; + numberOfBytesReceived = 0; + } + + if(result) + { + [self startConnectTimeout:timeout]; + } + + return result; +} + +- (BOOL)connectWithTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_DISCONNECTED) + { + NSString *errMsg = @"Attempting to connect while already connected or connecting."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + result = NO; + return_from_block; + } + + if ([self isP2P]) + { + NSString *errMsg = @"P2P streams must use either connectTo:withAddress: or connectP2PWithSocket:."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidType userInfo:info]; + + result = NO; + return_from_block; + } + + if (myJID_setByClient == nil) + { + // Note: If you wish to use anonymous authentication, you should still set myJID prior to calling connect. + // You can simply set it to something like "anonymous@", where "" is the proper domain. + // After the authentication process, you can query the myJID property to see what your assigned JID is. + // + // Setting myJID allows the framework to follow the xmpp protocol properly, + // and it allows the framework to connect to servers without a DNS entry. + // + // For example, one may setup a private xmpp server for internal testing on their local network. + // The xmpp domain of the server may be something like "testing.mycompany.com", + // but since the server is internal, an IP (192.168.1.22) is used as the hostname to connect. + // + // Proper connection requires a TCP connection to the IP (192.168.1.22), + // but the xmpp handshake requires the xmpp domain (testing.mycompany.com). + + NSString *errMsg = @"You must set myJID before calling connect."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidProperty userInfo:info]; + + result = NO; + return_from_block; + } + + // Notify delegates + [multicastDelegate xmppStreamWillConnect:self]; + + if ([hostName length] == 0) + { + // Resolve the hostName via myJID SRV resolution + + state = STATE_XMPP_RESOLVING_SRV; + + srvResolver = [[XMPPSRVResolver alloc] initWithdDelegate:self delegateQueue:xmppQueue resolverQueue:NULL]; + + srvResults = nil; + srvResultsIndex = 0; + + NSString *srvName = [XMPPSRVResolver srvNameFromXMPPDomain:[myJID_setByClient domain]]; + + [srvResolver startWithSRVName:srvName timeout:TIMEOUT_SRV_RESOLUTION]; + + result = YES; + } + else + { + // Open TCP connection to the configured hostName. + + state = STATE_XMPP_CONNECTING; + + NSError *connectErr = nil; + result = [self connectToHost:hostName onPort:hostPort withTimeout:XMPPStreamTimeoutNone error:&connectErr]; + + if (!result) + { + err = connectErr; + state = STATE_XMPP_DISCONNECTED; + } + } + + if(result) + { + [self startConnectTimeout:timeout]; + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +- (BOOL)oldSchoolSecureConnectWithTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Go through the regular connect routine + NSError *connectErr = nil; + result = [self connectWithTimeout:timeout error:&connectErr]; + + if (result) + { + // Mark the secure flag. + // We will check the flag in socket:didConnectToHost:port: + + [self setIsSecure:YES]; + } + else + { + err = connectErr; + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark P2P Connection +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Starts a P2P connection to the given user and given address. + * This method only works with XMPPStream objects created using the initP2P method. + * + * The given address is specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetservice's addresses method. +**/ +- (BOOL)connectTo:(XMPPJID *)jid withAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = YES; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_DISCONNECTED) + { + NSString *errMsg = @"Attempting to connect while already connected or connecting."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + result = NO; + return_from_block; + } + + if (![self isP2P]) + { + NSString *errMsg = @"Non P2P streams must use the connect: method"; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidType userInfo:info]; + + result = NO; + return_from_block; + } + + // Turn on P2P initiator flag + flags |= kP2PInitiator; + + // Store remoteJID + remoteJID = [jid copy]; + + NSAssert((asyncSocket == nil), @"Forgot to release the previous asyncSocket instance."); + + // Notify delegates + [multicastDelegate xmppStreamWillConnect:self]; + + // Update state + state = STATE_XMPP_CONNECTING; + + // Initailize socket + asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:xmppQueue]; + + NSError *connectErr = nil; + result = [asyncSocket connectToAddress:remoteAddr error:&connectErr]; + + if (result == NO) + { + err = connectErr; + state = STATE_XMPP_DISCONNECTED; + } + else if ([self resetByteCountPerConnection]) + { + numberOfBytesSent = 0; + numberOfBytesReceived = 0; + } + + if(result) + { + [self startConnectTimeout:timeout]; + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +/** + * Starts a P2P connection with the given accepted socket. + * This method only works with XMPPStream objects created using the initP2P method. + * + * The given socket should be a socket that has already been accepted. + * The remoteJID will be extracted from the opening stream negotiation. +**/ +- (BOOL)connectP2PWithSocket:(GCDAsyncSocket *)acceptedSocket error:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = YES; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_DISCONNECTED) + { + NSString *errMsg = @"Attempting to connect while already connected or connecting."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + result = NO; + return_from_block; + } + + if (![self isP2P]) + { + NSString *errMsg = @"Non P2P streams must use the connect: method"; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidType userInfo:info]; + + result = NO; + return_from_block; + } + + if (acceptedSocket == nil) + { + NSString *errMsg = @"Parameter acceptedSocket is nil."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidParameter userInfo:info]; + + result = NO; + return_from_block; + } + + // Turn off P2P initiator flag + flags &= ~kP2PInitiator; + + NSAssert((asyncSocket == nil), @"Forgot to release the previous asyncSocket instance."); + + // Store and configure socket + asyncSocket = acceptedSocket; + [asyncSocket setDelegate:self delegateQueue:xmppQueue]; + + // Notify delegates + [multicastDelegate xmppStream:self socketDidConnect:asyncSocket]; + + // Update state + state = STATE_XMPP_CONNECTING; + + if ([self resetByteCountPerConnection]) + { + numberOfBytesSent = 0; + numberOfBytesReceived = 0; + } + + // Start the XML stream + [self startNegotiation]; + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Disconnect +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Closes the connection to the remote host. +**/ +- (void)disconnect +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_DISCONNECTED) + { + [multicastDelegate xmppStreamWasToldToDisconnect:self]; + + if (state == STATE_XMPP_RESOLVING_SRV) + { + [srvResolver stop]; + srvResolver = nil; + + state = STATE_XMPP_DISCONNECTED; + + [multicastDelegate xmppStreamDidDisconnect:self withError:nil]; + } + else + { + [asyncSocket disconnect]; + + // Everthing will be handled in socketDidDisconnect:withError: + } + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); +} + +- (void)disconnectAfterSending +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_DISCONNECTED) + { + [multicastDelegate xmppStreamWasToldToDisconnect:self]; + + if (state == STATE_XMPP_RESOLVING_SRV) + { + [srvResolver stop]; + srvResolver = nil; + + state = STATE_XMPP_DISCONNECTED; + + [multicastDelegate xmppStreamDidDisconnect:self withError:nil]; + } + else + { + NSString *termStr = @""; + NSData *termData = [termStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", termStr); + numberOfBytesSent += [termData length]; + + [asyncSocket writeData:termData withTimeout:TIMEOUT_XMPP_WRITE tag:TAG_XMPP_WRITE_STOP]; + [asyncSocket disconnectAfterWriting]; + + // Everthing will be handled in socketDidDisconnect:withError: + } + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns YES if SSL/TLS has been used to secure the connection. +**/ +- (BOOL)isSecure +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return (flags & kIsSecure) ? YES : NO; + } + else + { + __block BOOL result; + + dispatch_sync(xmppQueue, ^{ + result = (flags & kIsSecure) ? YES : NO; + }); + + return result; + } +} + +- (void)setIsSecure:(BOOL)flag +{ + dispatch_block_t block = ^{ + if(flag) + flags |= kIsSecure; + else + flags &= ~kIsSecure; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (BOOL)supportsStartTLS +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool { + + // The root element can be properly queried for authentication mechanisms anytime after the + // stream:features are received, and TLS has been setup (if required) + if (state >= STATE_XMPP_POST_NEGOTIATION) + { + NSXMLElement *features = [rootElement elementForName:@"stream:features"]; + NSXMLElement *starttls = [features elementForName:@"starttls" xmlns:@"urn:ietf:params:xml:ns:xmpp-tls"]; + + result = (starttls != nil); + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)sendStartTLSRequest +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + NSString *starttls = @""; + + NSData *outgoingData = [starttls dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", starttls); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; +} + +- (BOOL)secureConnection:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = YES; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_CONNECTED) + { + NSString *errMsg = @"Please wait until the stream is connected."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + result = NO; + return_from_block; + } + + if ([self isSecure]) + { + NSString *errMsg = @"The connection is already secure."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + result = NO; + return_from_block; + } + + if (![self supportsStartTLS]) + { + NSString *errMsg = @"The server does not support startTLS."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamUnsupportedAction userInfo:info]; + + result = NO; + return_from_block; + } + + // Update state + state = STATE_XMPP_STARTTLS_1; + + // Send the startTLS XML request + [self sendStartTLSRequest]; + + // We do not mark the stream as secure yet. + // We're waiting to receive the response from the + // server before we actually start the TLS handshake. + + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Registration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method checks the stream features of the connected server to determine if in-band registartion is supported. + * If we are not connected to a server, this method simply returns NO. +**/ +- (BOOL)supportsInBandRegistration +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool { + + // The root element can be properly queried for authentication mechanisms anytime after the + // stream:features are received, and TLS has been setup (if required) + if (state >= STATE_XMPP_POST_NEGOTIATION) + { + NSXMLElement *features = [rootElement elementForName:@"stream:features"]; + NSXMLElement *reg = [features elementForName:@"register" xmlns:@"http://jabber.org/features/iq-register"]; + + result = (reg != nil); + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +/** + * This method attempts to register a new user on the server using the given elements. + * The result of this action will be returned via the delegate methods. + * + * If the XMPPStream is not connected, or the server doesn't support in-band registration, this method does nothing. +**/ +- (BOOL)registerWithElements:(NSArray *)elements error:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = YES; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_CONNECTED) + { + NSString *errMsg = @"Please wait until the stream is connected."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + result = NO; + return_from_block; + } + + if (![self supportsInBandRegistration]) + { + NSString *errMsg = @"The server does not support in band registration."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamUnsupportedAction userInfo:info]; + + result = NO; + return_from_block; + } + + NSXMLElement *queryElement = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:register"]; + + for(NSXMLElement *element in elements) + { + [queryElement addChild:element]; + } + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set"]; + [iq addChild:queryElement]; + + NSString *outgoingStr = [iq compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + + // Update state + state = STATE_XMPP_REGISTERING; + + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; + +} + +/** + * This method attempts to register a new user on the server using the given username and password. + * The result of this action will be returned via the delegate methods. + * + * If the XMPPStream is not connected, or the server doesn't support in-band registration, this method does nothing. +**/ +- (BOOL)registerWithPassword:(NSString *)password error:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = YES; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (myJID_setByClient == nil) + { + NSString *errMsg = @"You must set myJID before calling registerWithPassword:error:."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidProperty userInfo:info]; + + result = NO; + return_from_block; + } + + NSString *username = [myJID_setByClient user]; + + NSMutableArray *elements = [NSMutableArray array]; + [elements addObject:[NSXMLElement elementWithName:@"username" stringValue:username]]; + [elements addObject:[NSXMLElement elementWithName:@"password" stringValue:password]]; + + [self registerWithElements:elements error:errPtr]; + + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Authentication +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSArray *)supportedAuthenticationMechanisms +{ + __block NSMutableArray *result = [[NSMutableArray alloc] init]; + + dispatch_block_t block = ^{ @autoreleasepool { + + // The root element can be properly queried for authentication mechanisms anytime after the + // stream:features are received, and TLS has been setup (if required). + + if (state >= STATE_XMPP_POST_NEGOTIATION) + { + NSXMLElement *features = [rootElement elementForName:@"stream:features"]; + NSXMLElement *mech = [features elementForName:@"mechanisms" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + + NSArray *mechanisms = [mech elementsForName:@"mechanism"]; + + for (NSXMLElement *mechanism in mechanisms) + { + [result addObject:[mechanism stringValue]]; + } + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +/** + * This method checks the stream features of the connected server to determine + * if the given authentication mechanism is supported. + * + * If we are not connected to a server, this method simply returns NO. +**/ +- (BOOL)supportsAuthenticationMechanism:(NSString *)mechanismType +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool { + + // The root element can be properly queried for authentication mechanisms anytime after the + // stream:features are received, and TLS has been setup (if required). + + if (state >= STATE_XMPP_POST_NEGOTIATION) + { + NSXMLElement *features = [rootElement elementForName:@"stream:features"]; + NSXMLElement *mech = [features elementForName:@"mechanisms" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"]; + + NSArray *mechanisms = [mech elementsForName:@"mechanism"]; + + for (NSXMLElement *mechanism in mechanisms) + { + if ([[mechanism stringValue] isEqualToString:mechanismType]) + { + result = YES; + break; + } + } + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (BOOL)authenticate:(id )inAuth error:(NSError **)errPtr +{ + XMPPLogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_CONNECTED) + { + NSString *errMsg = @"Please wait until the stream is connected."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + result = NO; + return_from_block; + } + + if (myJID_setByClient == nil) + { + NSString *errMsg = @"You must set myJID before calling authenticate:error:."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidProperty userInfo:info]; + + result = NO; + return_from_block; + } + + // Change state. + // We do this now because when we invoke the start method below, + // it may in turn invoke our sendAuthElement method, which expects us to be in STATE_XMPP_AUTH. + state = STATE_XMPP_AUTH; + + if ([inAuth start:&err]) + { + auth = inAuth; + result = YES; + } + else + { + // Unable to start authentication for some reason. + // Revert back to connected state. + state = STATE_XMPP_CONNECTED; + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +/** + * This method applies to standard password authentication schemes only. + * This is NOT the primary authentication method. + * + * @see authenticate:error: + * + * This method exists for backwards compatibility, and may disappear in future versions. +**/ +- (BOOL)authenticateWithPassword:(NSString *)inPassword error:(NSError **)errPtr +{ + XMPPLogTrace(); + + // The given password parameter could be mutable + NSString *password = [inPassword copy]; + + + __block BOOL result = YES; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_CONNECTED) + { + NSString *errMsg = @"Please wait until the stream is connected."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidState userInfo:info]; + + result = NO; + return_from_block; + } + + if (myJID_setByClient == nil) + { + NSString *errMsg = @"You must set myJID before calling authenticate:error:."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamInvalidProperty userInfo:info]; + + result = NO; + return_from_block; + } + + // Choose the best authentication method. + // + // P.S. - This method is deprecated. + + id someAuth = nil; + + if ([self supportsSCRAMSHA1Authentication]) + { + someAuth = [[XMPPSCRAMSHA1Authentication alloc] initWithStream:self password:password]; + result = [self authenticate:someAuth error:&err]; + } + else if ([self supportsDigestMD5Authentication]) + { + someAuth = [[XMPPDigestMD5Authentication alloc] initWithStream:self password:password]; + result = [self authenticate:someAuth error:&err]; + } + else if ([self supportsPlainAuthentication]) + { + someAuth = [[XMPPPlainAuthentication alloc] initWithStream:self password:password]; + result = [self authenticate:someAuth error:&err]; + } + else if ([self supportsDeprecatedDigestAuthentication]) + { + someAuth = [[XMPPDeprecatedDigestAuthentication alloc] initWithStream:self password:password]; + result = [self authenticate:someAuth error:&err]; + } + else if ([self supportsDeprecatedPlainAuthentication]) + { + someAuth = [[XMPPDeprecatedDigestAuthentication alloc] initWithStream:self password:password]; + result = [self authenticate:someAuth error:&err]; + } + else + { + NSString *errMsg = @"No suitable authentication method found"; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + err = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamUnsupportedAction userInfo:info]; + + result = NO; + } + }}; + + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +- (BOOL)isAuthenticating +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool { + result = (state == STATE_XMPP_AUTH); + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (BOOL)isAuthenticated +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (flags & kIsAuthenticated) ? YES : NO; + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +- (void)setIsAuthenticated:(BOOL)flag +{ + dispatch_block_t block = ^{ + if(flag) + { + flags |= kIsAuthenticated; + authenticationDate = [NSDate date]; + } + else + { + flags &= ~kIsAuthenticated; + authenticationDate = nil; + } + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (NSDate *)authenticationDate +{ + __block NSDate *result = nil; + + dispatch_block_t block = ^{ + if(flags & kIsAuthenticated) + { + result = authenticationDate; + } + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Compression +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSArray *)supportedCompressionMethods +{ + __block NSMutableArray *result = [[NSMutableArray alloc] init]; + + dispatch_block_t block = ^{ @autoreleasepool { + + // The root element can be properly queried for compression methods anytime after the + // stream:features are received, and TLS has been setup (if required). + + if (state >= STATE_XMPP_POST_NEGOTIATION) + { + NSXMLElement *features = [rootElement elementForName:@"stream:features"]; + NSXMLElement *compression = [features elementForName:@"compression" xmlns:@"http://jabber.org/features/compress"]; + + NSArray *methods = [compression elementsForName:@"method"]; + + for (NSXMLElement *method in methods) + { + [result addObject:[method stringValue]]; + } + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +/** + * This method checks the stream features of the connected server to determine + * if the given compression method is supported. + * + * If we are not connected to a server, this method simply returns NO. +**/ +- (BOOL)supportsCompressionMethod:(NSString *)compressionMethod +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool { + + // The root element can be properly queried for compression methods anytime after the + // stream:features are received, and TLS has been setup (if required). + + if (state >= STATE_XMPP_POST_NEGOTIATION) + { + NSXMLElement *features = [rootElement elementForName:@"stream:features"]; + NSXMLElement *compression = [features elementForName:@"compression" xmlns:@"http://jabber.org/features/compress"]; + + NSArray *methods = [compression elementsForName:@"method"]; + + for (NSXMLElement *method in methods) + { + if ([[method stringValue] isEqualToString:compressionMethod]) + { + result = YES; + break; + } + } + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark General Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method will return the root element of the document. + * This element contains the opening and tags received from the server + * when the XML stream was opened. + * + * Note: The rootElement is empty, and does not contain all the XML elements the stream has received during it's + * connection. This is done for performance reasons and for the obvious benefit of being more memory efficient. +**/ +- (NSXMLElement *)rootElement +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return rootElement; + } + else + { + __block NSXMLElement *result = nil; + + dispatch_sync(xmppQueue, ^{ + result = [rootElement copy]; + }); + + return result; + } +} + +/** + * Returns the version attribute from the servers's element. + * This should be at least 1.0 to be RFC 3920 compliant. + * If no version number was set, the server is not RFC compliant, and 0 is returned. +**/ +- (float)serverXmppStreamVersionNumber +{ + if (dispatch_get_specific(xmppQueueTag)) + { + return [rootElement attributeFloatValueForName:@"version" withDefaultValue:0.0F]; + } + else + { + __block float result; + + dispatch_sync(xmppQueue, ^{ + result = [rootElement attributeFloatValueForName:@"version" withDefaultValue:0.0F]; + }); + + return result; + } +} + +- (void)sendIQ:(XMPPIQ *)iq withTag:(long)tag +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + // We're getting ready to send an IQ. + // Notify delegates to allow them to optionally alter/filter the outgoing IQ. + + SEL selector = @selector(xmppStream:willSendIQ:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // None of the delegates implement the method. + // Use a shortcut. + + [self continueSendIQ:iq withTag:tag]; + } + else + { + // Notify all interested delegates. + // This must be done serially to allow them to alter the element in a thread-safe manner. + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + dispatch_async(willSendIqQueue, ^{ @autoreleasepool { + + // Allow delegates to modify and/or filter outgoing element + + __block XMPPIQ *modifiedIQ = iq; + + id del; + dispatch_queue_t dq; + + while (modifiedIQ && [delegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:selector]) + { + #if DEBUG + { + char methodReturnType[32]; + + Method method = class_getInstanceMethod([del class], selector); + method_getReturnType(method, methodReturnType, sizeof(methodReturnType)); + + if (strcmp(methodReturnType, @encode(XMPPIQ*)) != 0) + { + NSAssert(NO, @"Method xmppStream:willSendIQ: is no longer void (see XMPPStream.h). " + @"Culprit = %@", NSStringFromClass([del class])); + } + } + #endif + + dispatch_sync(dq, ^{ @autoreleasepool { + + modifiedIQ = [del xmppStream:self willSendIQ:modifiedIQ]; + + }}); + } + + if (modifiedIQ) + { + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) { + [self continueSendIQ:modifiedIQ withTag:tag]; + } else { + [self failToSendIQ:modifiedIQ]; + } + }}); + } + }}); + } +} + +- (void)sendMessage:(XMPPMessage *)message withTag:(long)tag +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + // We're getting ready to send a message. + // Notify delegates to allow them to optionally alter/filter the outgoing message. + + SEL selector = @selector(xmppStream:willSendMessage:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // None of the delegates implement the method. + // Use a shortcut. + + [self continueSendMessage:message withTag:tag]; + } + else + { + // Notify all interested delegates. + // This must be done serially to allow them to alter the element in a thread-safe manner. + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + dispatch_async(willSendMessageQueue, ^{ @autoreleasepool { + + // Allow delegates to modify outgoing element + + __block XMPPMessage *modifiedMessage = message; + + id del; + dispatch_queue_t dq; + + while (modifiedMessage && [delegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:selector]) + { + #if DEBUG + { + char methodReturnType[32]; + + Method method = class_getInstanceMethod([del class], selector); + method_getReturnType(method, methodReturnType, sizeof(methodReturnType)); + + if (strcmp(methodReturnType, @encode(XMPPMessage*)) != 0) + { + NSAssert(NO, @"Method xmppStream:willSendMessage: is no longer void (see XMPPStream.h). " + @"Culprit = %@", NSStringFromClass([del class])); + } + } + #endif + + dispatch_sync(dq, ^{ @autoreleasepool { + + modifiedMessage = [del xmppStream:self willSendMessage:modifiedMessage]; + + }}); + } + + if (modifiedMessage) + { + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) { + [self continueSendMessage:modifiedMessage withTag:tag]; + } + else { + [self failToSendMessage:modifiedMessage]; + } + }}); + } + }}); + } +} + +- (void)sendPresence:(XMPPPresence *)presence withTag:(long)tag +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + // We're getting ready to send a presence element. + // Notify delegates to allow them to optionally alter/filter the outgoing presence. + + SEL selector = @selector(xmppStream:willSendPresence:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // None of the delegates implement the method. + // Use a shortcut. + + [self continueSendPresence:presence withTag:tag]; + } + else + { + // Notify all interested delegates. + // This must be done serially to allow them to alter the element in a thread-safe manner. + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + dispatch_async(willSendPresenceQueue, ^{ @autoreleasepool { + + // Allow delegates to modify outgoing element + + __block XMPPPresence *modifiedPresence = presence; + + id del; + dispatch_queue_t dq; + + while (modifiedPresence && [delegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:selector]) + { + #if DEBUG + { + char methodReturnType[32]; + + Method method = class_getInstanceMethod([del class], selector); + method_getReturnType(method, methodReturnType, sizeof(methodReturnType)); + + if (strcmp(methodReturnType, @encode(XMPPPresence*)) != 0) + { + NSAssert(NO, @"Method xmppStream:willSendPresence: is no longer void (see XMPPStream.h). " + @"Culprit = %@", NSStringFromClass([del class])); + } + } + #endif + + dispatch_sync(dq, ^{ @autoreleasepool { + + modifiedPresence = [del xmppStream:self willSendPresence:modifiedPresence]; + + }}); + } + + if (modifiedPresence) + { + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) { + [self continueSendPresence:modifiedPresence withTag:tag]; + } else { + [self failToSendPresence:modifiedPresence]; + } + }}); + } + }}); + } +} + +- (void)continueSendIQ:(XMPPIQ *)iq withTag:(long)tag +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + NSString *outgoingStr = [iq compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:tag]; + + [multicastDelegate xmppStream:self didSendIQ:iq]; +} + +- (void)continueSendMessage:(XMPPMessage *)message withTag:(long)tag +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + NSString *outgoingStr = [message compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:tag]; + + [multicastDelegate xmppStream:self didSendMessage:message]; +} + +- (void)continueSendPresence:(XMPPPresence *)presence withTag:(long)tag +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + NSString *outgoingStr = [presence compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:tag]; + + // Update myPresence if this is a normal presence element. + // In other words, ignore presence subscription stuff, MUC room stuff, etc. + // + // We use the built-in [presence type] which guarantees lowercase strings, + // and will return @"available" if there was no set type (as available is implicit). + + NSString *type = [presence type]; + if ([type isEqualToString:@"available"] || [type isEqualToString:@"unavailable"]) + { + if ([presence toStr] == nil && myPresence != presence) + { + myPresence = presence; + } + } + + [multicastDelegate xmppStream:self didSendPresence:presence]; +} + +- (void)continueSendElement:(NSXMLElement *)element withTag:(long)tag +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + NSString *outgoingStr = [element compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:tag]; + + if ([customElementNames countForObject:[element name]]) + { + [multicastDelegate xmppStream:self didSendCustomElement:element]; + } +} + +/** + * Private method. + * Presencts a common method for the various public sendElement methods. +**/ +- (void)sendElement:(NSXMLElement *)element withTag:(long)tag +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + + if ([element isKindOfClass:[XMPPIQ class]]) + { + [self sendIQ:(XMPPIQ *)element withTag:tag]; + } + else if ([element isKindOfClass:[XMPPMessage class]]) + { + [self sendMessage:(XMPPMessage *)element withTag:tag]; + } + else if ([element isKindOfClass:[XMPPPresence class]]) + { + [self sendPresence:(XMPPPresence *)element withTag:tag]; + } + else + { + NSString *elementName = [element name]; + + if ([elementName isEqualToString:@"iq"]) + { + [self sendIQ:[XMPPIQ iqFromElement:element] withTag:tag]; + } + else if ([elementName isEqualToString:@"message"]) + { + [self sendMessage:[XMPPMessage messageFromElement:element] withTag:tag]; + } + else if ([elementName isEqualToString:@"presence"]) + { + [self sendPresence:[XMPPPresence presenceFromElement:element] withTag:tag]; + } + else + { + [self continueSendElement:element withTag:tag]; + } + } +} + +/** + * This method handles sending an XML stanza. + * If the XMPPStream is not connected, this method does nothing. +**/ +- (void)sendElement:(NSXMLElement *)element +{ + if (element == nil) return; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) + { + [self sendElement:element withTag:TAG_XMPP_WRITE_STREAM]; + } + else + { + [self failToSendElement:element]; + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +/** + * This method handles sending an XML stanza. + * If the XMPPStream is not connected, this method does nothing. + * + * After the element has been successfully sent, + * the xmppStream:didSendElementWithTag: delegate method is called. +**/ +- (void)sendElement:(NSXMLElement *)element andGetReceipt:(XMPPElementReceipt **)receiptPtr +{ + if (element == nil) return; + + if (receiptPtr == nil) + { + [self sendElement:element]; + } + else + { + __block XMPPElementReceipt *receipt = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) + { + receipt = [[XMPPElementReceipt alloc] init]; + [receipts addObject:receipt]; + + [self sendElement:element withTag:TAG_XMPP_WRITE_RECEIPT]; + } + else + { + [self failToSendElement:element]; + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); + + *receiptPtr = receipt; + } +} + +- (void)failToSendElement:(NSXMLElement *)element +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + if ([element isKindOfClass:[XMPPIQ class]]) + { + [self failToSendIQ:(XMPPIQ *)element]; + } + else if ([element isKindOfClass:[XMPPMessage class]]) + { + [self failToSendMessage:(XMPPMessage *)element]; + } + else if ([element isKindOfClass:[XMPPPresence class]]) + { + [self failToSendPresence:(XMPPPresence *)element]; + } + else + { + NSString *elementName = [element name]; + + if ([elementName isEqualToString:@"iq"]) + { + [self failToSendIQ:[XMPPIQ iqFromElement:element]]; + } + else if ([elementName isEqualToString:@"message"]) + { + [self failToSendMessage:[XMPPMessage messageFromElement:element]]; + } + else if ([elementName isEqualToString:@"presence"]) + { + [self failToSendPresence:[XMPPPresence presenceFromElement:element]]; + } + } +} + +- (void)failToSendIQ:(XMPPIQ *)iq +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + NSError *error = [NSError errorWithDomain:XMPPStreamErrorDomain + code:XMPPStreamInvalidState + userInfo:nil]; + + [multicastDelegate xmppStream:self didFailToSendIQ:iq error:error]; +} + +- (void)failToSendMessage:(XMPPMessage *)message +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + NSError *error = [NSError errorWithDomain:XMPPStreamErrorDomain + code:XMPPStreamInvalidState + userInfo:nil]; + + [multicastDelegate xmppStream:self didFailToSendMessage:message error:error]; +} + +- (void)failToSendPresence:(XMPPPresence *)presence +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + NSError *error = [NSError errorWithDomain:XMPPStreamErrorDomain + code:XMPPStreamInvalidState + userInfo:nil]; + + [multicastDelegate xmppStream:self didFailToSendPresence:presence error:error]; +} + +/** + * Retrieves the current presence and resends it in once atomic operation. + * Useful for various components that need to update injected information in the presence stanza. +**/ +- (void)resendMyPresence +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (myPresence && [[myPresence type] isEqualToString:@"available"]) + { + [self sendElement:myPresence]; + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +/** + * This method is for use by xmpp authentication mechanism classes. + * They should send elements using this method instead of the public sendElement methods, + * as those methods don't send the elements while authentication is in progress. + * + * @see XMPPSASLAuthentication +**/ +- (void)sendAuthElement:(NSXMLElement *)element +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (state == STATE_XMPP_AUTH) + { + NSString *outgoingStr = [element compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + } + else + { + XMPPLogWarn(@"Unable to send element while not in STATE_XMPP_AUTH: %@", [element compactXMLString]); + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +/** + * This method is for use by xmpp custom binding classes. + * They should send elements using this method instead of the public sendElement methods, + * as those methods don't send the elements while authentication/binding is in progress. + * + * @see XMPPCustomBinding +**/ +- (void)sendBindElement:(NSXMLElement *)element +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (state == STATE_XMPP_BINDING) + { + NSString *outgoingStr = [element compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + } + else + { + XMPPLogWarn(@"Unable to send element while not in STATE_XMPP_BINDING: %@", [element compactXMLString]); + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (void)receiveIQ:(XMPPIQ *)iq +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + // We're getting ready to receive an IQ. + // Notify delegates to allow them to optionally alter/filter the incoming IQ element. + + SEL selector = @selector(xmppStream:willReceiveIQ:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // None of the delegates implement the method. + // Use a shortcut. + + if (willReceiveStanzaQueue) + { + // But still go through the stanzaQueue in order to guarantee in-order-delivery of all received stanzas. + + dispatch_async(willReceiveStanzaQueue, ^{ + dispatch_async(xmppQueue, ^{ @autoreleasepool { + if (state == STATE_XMPP_CONNECTED) { + [self continueReceiveIQ:iq]; + } + }}); + }); + } + else + { + [self continueReceiveIQ:iq]; + } + } + else + { + // Notify all interested delegates. + // This must be done serially to allow them to alter the element in a thread-safe manner. + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + if (willReceiveStanzaQueue == NULL) + willReceiveStanzaQueue = dispatch_queue_create("xmpp.willReceiveStanza", DISPATCH_QUEUE_SERIAL); + + dispatch_async(willReceiveStanzaQueue, ^{ @autoreleasepool { + + // Allow delegates to modify and/or filter incoming element + + __block XMPPIQ *modifiedIQ = iq; + + id del; + dispatch_queue_t dq; + + while (modifiedIQ && [delegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:selector]) + { + dispatch_sync(dq, ^{ @autoreleasepool { + + modifiedIQ = [del xmppStream:self willReceiveIQ:modifiedIQ]; + + }}); + } + + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) + { + if (modifiedIQ) + [self continueReceiveIQ:modifiedIQ]; + else + [multicastDelegate xmppStreamDidFilterStanza:self]; + } + }}); + }}); + } +} + +- (void)receiveMessage:(XMPPMessage *)message +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + // We're getting ready to receive a message. + // Notify delegates to allow them to optionally alter/filter the incoming message. + + SEL selector = @selector(xmppStream:willReceiveMessage:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // None of the delegates implement the method. + // Use a shortcut. + + if (willReceiveStanzaQueue) + { + // But still go through the stanzaQueue in order to guarantee in-order-delivery of all received stanzas. + + dispatch_async(willReceiveStanzaQueue, ^{ + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) { + [self continueReceiveMessage:message]; + } + }}); + }); + } + else + { + [self continueReceiveMessage:message]; + } + } + else + { + // Notify all interested delegates. + // This must be done serially to allow them to alter the element in a thread-safe manner. + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + if (willReceiveStanzaQueue == NULL) + willReceiveStanzaQueue = dispatch_queue_create("xmpp.willReceiveStanza", DISPATCH_QUEUE_SERIAL); + + dispatch_async(willReceiveStanzaQueue, ^{ @autoreleasepool { + + // Allow delegates to modify incoming element + + __block XMPPMessage *modifiedMessage = message; + + id del; + dispatch_queue_t dq; + + while (modifiedMessage && [delegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:selector]) + { + dispatch_sync(dq, ^{ @autoreleasepool { + + modifiedMessage = [del xmppStream:self willReceiveMessage:modifiedMessage]; + + }}); + } + + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) + { + if (modifiedMessage) + [self continueReceiveMessage:modifiedMessage]; + else + [multicastDelegate xmppStreamDidFilterStanza:self]; + } + }}); + }}); + } +} + +- (void)receivePresence:(XMPPPresence *)presence +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(state == STATE_XMPP_CONNECTED, @"Invoked with incorrect state"); + + // We're getting ready to receive a presence element. + // Notify delegates to allow them to optionally alter/filter the incoming presence. + + SEL selector = @selector(xmppStream:willReceivePresence:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // None of the delegates implement the method. + // Use a shortcut. + + if (willReceiveStanzaQueue) + { + // But still go through the stanzaQueue in order to guarantee in-order-delivery of all received stanzas. + + dispatch_async(willReceiveStanzaQueue, ^{ + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) { + [self continueReceivePresence:presence]; + } + }}); + }); + } + else + { + [self continueReceivePresence:presence]; + } + } + else + { + // Notify all interested delegates. + // This must be done serially to allow them to alter the element in a thread-safe manner. + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + if (willReceiveStanzaQueue == NULL) + willReceiveStanzaQueue = dispatch_queue_create("xmpp.willReceiveStanza", DISPATCH_QUEUE_SERIAL); + + dispatch_async(willReceiveStanzaQueue, ^{ @autoreleasepool { + + // Allow delegates to modify outgoing element + + __block XMPPPresence *modifiedPresence = presence; + + id del; + dispatch_queue_t dq; + + while (modifiedPresence && [delegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:selector]) + { + dispatch_sync(dq, ^{ @autoreleasepool { + + modifiedPresence = [del xmppStream:self willReceivePresence:modifiedPresence]; + + }}); + } + + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + if (state == STATE_XMPP_CONNECTED) + { + if (modifiedPresence) + [self continueReceivePresence:presence]; + else + [multicastDelegate xmppStreamDidFilterStanza:self]; + } + }}); + }}); + } +} + +- (void)continueReceiveIQ:(XMPPIQ *)iq +{ + if ([iq requiresResponse]) + { + // As per the XMPP specificiation, if the IQ requires a response, + // and we don't have any delegates or modules that can properly respond to the IQ, + // we MUST send back and error IQ. + // + // So we notifiy all interested delegates and modules about the received IQ, + // keeping track of whether or not any of them have handled it. + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + id del; + dispatch_queue_t dq; + + SEL selector = @selector(xmppStream:didReceiveIQ:); + + dispatch_semaphore_t delSemaphore = dispatch_semaphore_create(0); + dispatch_group_t delGroup = dispatch_group_create(); + + while ([delegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:selector]) + { + dispatch_group_async(delGroup, dq, ^{ @autoreleasepool { + + if ([del xmppStream:self didReceiveIQ:iq]) + { + dispatch_semaphore_signal(delSemaphore); + } + + }}); + } + + dispatch_async(didReceiveIqQueue, ^{ @autoreleasepool { + + dispatch_group_wait(delGroup, DISPATCH_TIME_FOREVER); + + // Did any of the delegates handle the IQ? (handle == will response) + + BOOL handled = (dispatch_semaphore_wait(delSemaphore, DISPATCH_TIME_NOW) == 0); + + // An entity that receives an IQ request of type "get" or "set" MUST reply + // with an IQ response of type "result" or "error". + // + // The response MUST preserve the 'id' attribute of the request. + + if (!handled) + { + // Return error message: + // + // + // + // + // + // + // + + NSXMLElement *reason = [NSXMLElement elementWithName:@"feature-not-implemented" + xmlns:@"urn:ietf:params:xml:ns:xmpp-stanzas"]; + + NSXMLElement *error = [NSXMLElement elementWithName:@"error"]; + [error addAttributeWithName:@"type" stringValue:@"cancel"]; + [error addAttributeWithName:@"code" stringValue:@"501"]; + [error addChild:reason]; + + XMPPIQ *iqResponse = [XMPPIQ iqWithType:@"error" + to:[iq from] + elementID:[iq elementID] + child:error]; + + NSXMLElement *iqChild = [iq childElement]; + if (iqChild) + { + NSXMLNode *iqChildCopy = [iqChild copy]; + [iqResponse insertChild:iqChildCopy atIndex:0]; + } + + // Purposefully go through the sendElement: method + // so that it gets dispatched onto the xmppQueue, + // and so that modules may get notified of the outgoing error message. + + [self sendElement:iqResponse]; + } + + #if !OS_OBJECT_USE_OBJC + dispatch_release(delSemaphore); + dispatch_release(delGroup); + #endif + + }}); + } + else + { + // The IQ doesn't require a response. + // So we can just fire the delegate method and ignore the responses. + + [multicastDelegate xmppStream:self didReceiveIQ:iq]; + } +} + +- (void)continueReceiveMessage:(XMPPMessage *)message +{ + [multicastDelegate xmppStream:self didReceiveMessage:message]; +} + +- (void)continueReceivePresence:(XMPPPresence *)presence +{ + [multicastDelegate xmppStream:self didReceivePresence:presence]; +} + +/** + * This method allows you to inject an element into the stream as if it was received on the socket. + * This is an advanced technique, but makes for some interesting possibilities. +**/ +- (void)injectElement:(NSXMLElement *)element +{ + if (element == nil) return; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (state != STATE_XMPP_CONNECTED) + { + return_from_block; + } + + if ([element isKindOfClass:[XMPPIQ class]]) + { + [self receiveIQ:(XMPPIQ *)element]; + } + else if ([element isKindOfClass:[XMPPMessage class]]) + { + [self receiveMessage:(XMPPMessage *)element]; + } + else if ([element isKindOfClass:[XMPPPresence class]]) + { + [self receivePresence:(XMPPPresence *)element]; + } + else + { + NSString *elementName = [element name]; + + if ([elementName isEqualToString:@"iq"]) + { + [self receiveIQ:[XMPPIQ iqFromElement:element]]; + } + else if ([elementName isEqualToString:@"message"]) + { + [self receiveMessage:[XMPPMessage messageFromElement:element]]; + } + else if ([elementName isEqualToString:@"presence"]) + { + [self receivePresence:[XMPPPresence presenceFromElement:element]]; + } + else if ([customElementNames countForObject:elementName]) + { + [multicastDelegate xmppStream:self didReceiveCustomElement:element]; + } + else + { + [multicastDelegate xmppStream:self didReceiveError:element]; + } + } + }}; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (void)registerCustomElementNames:(NSSet *)names +{ + dispatch_block_t block = ^{ + + if (customElementNames == nil) + customElementNames = [[NSCountedSet alloc] init]; + + for (NSString *name in names) + { + [customElementNames addObject:name]; + } + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); +} + +- (void)unregisterCustomElementNames:(NSSet *)names +{ + dispatch_block_t block = ^{ + + for (NSString *name in names) + { + [customElementNames removeObject:name]; + } + }; + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Stream Negotiation +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method is called to start the initial negotiation process. +**/ +- (void)startNegotiation +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + NSAssert(![self didStartNegotiation], @"Invoked after initial negotiation has started"); + + XMPPLogTrace(); + + // Initialize the XML stream + [self sendOpeningNegotiation]; + + // Inform delegate that the TCP connection is open, and the stream handshake has begun + [multicastDelegate xmppStreamDidStartNegotiation:self]; + + // And start reading in the server's XML stream + [asyncSocket readDataWithTimeout:TIMEOUT_XMPP_READ_START tag:TAG_XMPP_READ_START]; +} + +/** + * This method handles sending the opening element which is needed in several situations. +**/ +- (void)sendOpeningNegotiation +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + if (![self didStartNegotiation]) + { + // TCP connection was just opened - We need to include the opening XML stanza + NSString *s1 = @""; + + NSData *outgoingData = [s1 dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", s1); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_START]; + + [self setDidStartNegotiation:YES]; + } + + if (parser == nil) + { + XMPPLogVerbose(@"%@: Initializing parser...", THIS_FILE); + + // Need to create the parser. + parser = [[XMPPParser alloc] initWithDelegate:self delegateQueue:xmppQueue]; + } + else + { + XMPPLogVerbose(@"%@: Resetting parser...", THIS_FILE); + + // We're restarting our negotiation, so we need to reset the parser. + parser = [[XMPPParser alloc] initWithDelegate:self delegateQueue:xmppQueue]; + } + + NSString *xmlns = @"jabber:client"; + NSString *xmlns_stream = @"http://etherx.jabber.org/streams"; + + NSString *temp, *s2; + if ([self isP2P]) + { + if (myJID_setByClient && remoteJID) + { + temp = @""; + s2 = [NSString stringWithFormat:temp, xmlns, xmlns_stream, [myJID_setByClient bare], [remoteJID bare]]; + } + else if (myJID_setByClient) + { + temp = @""; + s2 = [NSString stringWithFormat:temp, xmlns, xmlns_stream, [myJID_setByClient bare]]; + } + else if (remoteJID) + { + temp = @""; + s2 = [NSString stringWithFormat:temp, xmlns, xmlns_stream, [remoteJID bare]]; + } + else + { + temp = @""; + s2 = [NSString stringWithFormat:temp, xmlns, xmlns_stream]; + } + } + else + { + if (myJID_setByClient) + { + temp = @""; + s2 = [NSString stringWithFormat:temp, xmlns, xmlns_stream, [myJID_setByClient domain]]; + } + else if ([hostName length] > 0) + { + temp = @""; + s2 = [NSString stringWithFormat:temp, xmlns, xmlns_stream, hostName]; + } + else + { + temp = @""; + s2 = [NSString stringWithFormat:temp, xmlns, xmlns_stream]; + } + } + + NSData *outgoingData = [s2 dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", s2); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_START]; + + // Update status + state = STATE_XMPP_OPENING; +} + +/** + * This method handles starting TLS negotiation on the socket, using the proper settings. +**/ +- (void)startTLS +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + // Update state (part 2 - prompting delegates) + state = STATE_XMPP_STARTTLS_2; + + // Create a mutable dictionary for security settings + NSMutableDictionary *settings = [NSMutableDictionary dictionaryWithCapacity:5]; + + SEL selector = @selector(xmppStream:willSecureWithSettings:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // None of the delegates implement the method. + // Use a shortcut. + + [self continueStartTLS:settings]; + } + else + { + // Query all interested delegates. + // This must be done serially to maintain thread safety. + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(concurrentQueue, ^{ @autoreleasepool { + + // Prompt the delegate(s) to populate the security settings + + id delegate; + dispatch_queue_t delegateQueue; + + while ([delegateEnumerator getNextDelegate:&delegate delegateQueue:&delegateQueue forSelector:selector]) + { + dispatch_sync(delegateQueue, ^{ @autoreleasepool { + + [delegate xmppStream:self willSecureWithSettings:settings]; + + }}); + } + + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + [self continueStartTLS:settings]; + + }}); + + }}); + } +} + +- (void)continueStartTLS:(NSMutableDictionary *)settings +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace2(@"%@: %@ %@", THIS_FILE, THIS_METHOD, settings); + + if (state == STATE_XMPP_STARTTLS_2) + { + // If the delegates didn't respond + if ([settings count] == 0) + { + // Use the default settings, and set the peer name + + NSString *expectedCertName = hostName; + if (expectedCertName == nil) + { + expectedCertName = [myJID_setByClient domain]; + } + + if ([expectedCertName length] > 0) + { + settings[(NSString *) kCFStreamSSLPeerName] = expectedCertName; + } + } + + [asyncSocket startTLS:settings]; + [self setIsSecure:YES]; + + // Note: We don't need to wait for asyncSocket to complete TLS negotiation. + // We can just continue reading/writing to the socket, and it will handle queueing everything for us! + + if ([self didStartNegotiation]) + { + // Now we start our negotiation over again... + [self sendOpeningNegotiation]; + + // We paused reading from the socket. + // We're ready to continue now. + [asyncSocket readDataWithTimeout:TIMEOUT_XMPP_READ_STREAM tag:TAG_XMPP_READ_STREAM]; + } + else + { + // First time starting negotiation + [self startNegotiation]; + } + } +} + +/** + * This method is called anytime we receive the server's stream features. + * This method looks at the stream features, and handles any requirements so communication can continue. +**/ +- (void)handleStreamFeatures +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + // Extract the stream features + NSXMLElement *features = [rootElement elementForName:@"stream:features"]; + + // Check to see if TLS is required + // Don't forget about that NSXMLElement bug you reported to apple (xmlns is required or element won't be found) + NSXMLElement *f_starttls = [features elementForName:@"starttls" xmlns:@"urn:ietf:params:xml:ns:xmpp-tls"]; + + if (f_starttls) + { + if ([f_starttls elementForName:@"required"] || [self startTLSPolicy] >= XMPPStreamStartTLSPolicyPreferred) + { + // TLS is required for this connection + + // Update state + state = STATE_XMPP_STARTTLS_1; + + // Send the startTLS XML request + [self sendStartTLSRequest]; + + // We do not mark the stream as secure yet. + // We're waiting to receive the response from the + // server before we actually start the TLS handshake. + + // We're already listening for the response... + return; + } + } + else if (![self isSecure] && [self startTLSPolicy] == XMPPStreamStartTLSPolicyRequired) + { + // We must abort the connection as the server doesn't support our requirements. + + NSString *errMsg = @"The server does not support startTLS. And the startTLSPolicy is Required."; + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + otherError = [NSError errorWithDomain:XMPPStreamErrorDomain code:XMPPStreamUnsupportedAction userInfo:info]; + + // Close the TCP connection. + [self disconnect]; + + // The socketDidDisconnect:withError: method will handle everything else + return; + } + + // Check to see if resource binding is required + // Don't forget about that NSXMLElement bug you reported to apple (xmlns is required or element won't be found) + NSXMLElement *f_bind = [features elementForName:@"bind" xmlns:@"urn:ietf:params:xml:ns:xmpp-bind"]; + + if (f_bind) + { + // Start the binding process + [self startBinding]; + + // We're already listening for the response... + return; + } + + // It looks like all has gone well, and the connection should be ready to use now + state = STATE_XMPP_CONNECTED; + + if (![self isAuthenticated]) + { + [self setupKeepAliveTimer]; + + // Notify delegates + [multicastDelegate xmppStreamDidConnect:self]; + } +} + +- (void)handleStartTLSResponse:(NSXMLElement *)response +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + // We're expecting a proceed response + // If we get anything else we can safely assume it's the equivalent of a failure response + if ( ![[response name] isEqualToString:@"proceed"]) + { + // We can close our TCP connection now + [self disconnect]; + + // The socketDidDisconnect:withError: method will handle everything else + return; + } + + // Start TLS negotiation + [self startTLS]; +} + +/** + * After the registerUser:withPassword: method is invoked, a registration message is sent to the server. + * We're waiting for the result from this registration request. +**/ +- (void)handleRegistration:(NSXMLElement *)response +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + if ([[response attributeStringValueForName:@"type"] isEqualToString:@"error"]) + { + // Revert back to connected state (from authenticating state) + state = STATE_XMPP_CONNECTED; + + [multicastDelegate xmppStream:self didNotRegister:response]; + } + else + { + // Revert back to connected state (from authenticating state) + state = STATE_XMPP_CONNECTED; + + [multicastDelegate xmppStreamDidRegister:self]; + } +} + +/** + * After the authenticate:error: or authenticateWithPassword:error: methods are invoked, some kind of + * authentication message is sent to the server. + * This method forwards the response to the authentication module, and handles the resulting authentication state. +**/ +- (void)handleAuth:(NSXMLElement *)authResponse +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + XMPPHandleAuthResponse result = [auth handleAuth:authResponse]; + + if (result == XMPP_AUTH_SUCCESS) + { + // We are successfully authenticated (via sasl:digest-md5) + [self setIsAuthenticated:YES]; + + BOOL shouldRenegotiate = YES; + if ([auth respondsToSelector:@selector(shouldResendOpeningNegotiationAfterSuccessfulAuthentication)]) + { + shouldRenegotiate = [auth shouldResendOpeningNegotiationAfterSuccessfulAuthentication]; + } + + if (shouldRenegotiate) + { + // Now we start our negotiation over again... + [self sendOpeningNegotiation]; + + if (![self isSecure]) + { + // Normally we requeue our read operation in xmppParserDidParseData:. + // But we just reset the parser, so that code path isn't going to happen. + // So start read request here. + // The state is STATE_XMPP_OPENING, set via sendOpeningNegotiation method. + + [asyncSocket readDataWithTimeout:TIMEOUT_XMPP_READ_START tag:TAG_XMPP_READ_START]; + } + } + else + { + // Revert back to connected state (from authenticating state) + state = STATE_XMPP_CONNECTED; + + [multicastDelegate xmppStreamDidAuthenticate:self]; + } + + // Done with auth + auth = nil; + + } + else if (result == XMPP_AUTH_FAIL) + { + // Revert back to connected state (from authenticating state) + state = STATE_XMPP_CONNECTED; + + // Notify delegate + [multicastDelegate xmppStream:self didNotAuthenticate:authResponse]; + + // Done with auth + auth = nil; + + } + else if (result == XMPP_AUTH_CONTINUE) + { + // Authentication continues. + // State doesn't change. + } + else + { + XMPPLogError(@"Authentication class (%@) returned invalid response code (%i)", + NSStringFromClass([auth class]), (int)result); + + NSAssert(NO, @"Authentication class (%@) returned invalid response code (%i)", + NSStringFromClass([auth class]), (int)result); + } +} + +- (void)startBinding +{ + XMPPLogTrace(); + + state = STATE_XMPP_BINDING; + + SEL selector = @selector(xmppStreamWillBind:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + [self startStandardBinding]; + } + else + { + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(concurrentQueue, ^{ @autoreleasepool { + + __block id delegateCustomBinding = nil; + + id delegate; + dispatch_queue_t dq; + + while ([delegateEnumerator getNextDelegate:&delegate delegateQueue:&dq forSelector:selector]) + { + dispatch_sync(dq, ^{ @autoreleasepool { + + delegateCustomBinding = [delegate xmppStreamWillBind:self]; + }}); + + if (delegateCustomBinding) { + break; + } + } + + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + if (delegateCustomBinding) + [self startCustomBinding:delegateCustomBinding]; + else + [self startStandardBinding]; + }}); + }}); + } +} + +- (void)startCustomBinding:(id )delegateCustomBinding +{ + XMPPLogTrace(); + + customBinding = delegateCustomBinding; + + NSError *bindError = nil; + XMPPBindResult result = [customBinding start:&bindError]; + + if (result == XMPP_BIND_CONTINUE) + { + // Expected result + // Wait for reply from server, and forward to customBinding module. + } + else + { + if (result == XMPP_BIND_SUCCESS) + { + // It appears binding isn't needed (perhaps handled via auth) + + BOOL skipStartSessionOverride = NO; + if ([customBinding respondsToSelector:@selector(shouldSkipStartSessionAfterSuccessfulBinding)]) { + skipStartSessionOverride = [customBinding shouldSkipStartSessionAfterSuccessfulBinding]; + } + + [self continuePostBinding:skipStartSessionOverride]; + } + else if (result == XMPP_BIND_FAIL_FALLBACK) + { + // Custom binding isn't available for whatever reason, + // but the module has requested we fallback to standard binding. + + [self startStandardBinding]; + } + else if (result == XMPP_BIND_FAIL_ABORT) + { + // Custom binding failed, + // and the module requested we abort. + + otherError = bindError; + [asyncSocket disconnect]; + } + + customBinding = nil; + } +} + +- (void)handleCustomBinding:(NSXMLElement *)response +{ + XMPPLogTrace(); + + NSError *bindError = nil; + XMPPBindResult result = [customBinding handleBind:response withError:&bindError]; + + if (result == XMPP_BIND_CONTINUE) + { + // Binding still in progress + } + else + { + if (result == XMPP_BIND_SUCCESS) + { + // Binding complete. Continue. + + BOOL skipStartSessionOverride = NO; + if ([customBinding respondsToSelector:@selector(shouldSkipStartSessionAfterSuccessfulBinding)]) { + skipStartSessionOverride = [customBinding shouldSkipStartSessionAfterSuccessfulBinding]; + } + + [self continuePostBinding:skipStartSessionOverride]; + } + else if (result == XMPP_BIND_FAIL_FALLBACK) + { + // Custom binding failed for whatever reason, + // but the module has requested we fallback to standard binding. + + [self startStandardBinding]; + } + else if (result == XMPP_BIND_FAIL_ABORT) + { + // Custom binding failed, + // and the module requested we abort. + + otherError = bindError; + [asyncSocket disconnect]; + } + + customBinding = nil; + } +} + +- (void)startStandardBinding +{ + XMPPLogTrace(); + + NSString *requestedResource = [myJID_setByClient resource]; + + if ([requestedResource length] > 0) + { + // Ask the server to bind the user specified resource + + NSXMLElement *resource = [NSXMLElement elementWithName:@"resource"]; + [resource setStringValue:requestedResource]; + + NSXMLElement *bind = [NSXMLElement elementWithName:@"bind" xmlns:@"urn:ietf:params:xml:ns:xmpp-bind"]; + [bind addChild:resource]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" elementID:[self generateUUID]]; + [iq addChild:bind]; + + NSString *outgoingStr = [iq compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + + [idTracker addElement:iq + target:nil + selector:NULL + timeout:XMPPIDTrackerTimeoutNone]; + } + else + { + // The user didn't specify a resource, so we ask the server to bind one for us + + NSXMLElement *bind = [NSXMLElement elementWithName:@"bind" xmlns:@"urn:ietf:params:xml:ns:xmpp-bind"]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" elementID:[self generateUUID]]; + [iq addChild:bind]; + + NSString *outgoingStr = [iq compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + + [idTracker addElement:iq + target:nil + selector:NULL + timeout:XMPPIDTrackerTimeoutNone]; + } +} + +- (void)handleStandardBinding:(NSXMLElement *)response +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + NSXMLElement *r_bind = [response elementForName:@"bind" xmlns:@"urn:ietf:params:xml:ns:xmpp-bind"]; + NSXMLElement *r_jid = [r_bind elementForName:@"jid"]; + + if (r_jid) + { + // We're properly binded to a resource now + // Extract and save our resource (it may not be what we originally requested) + NSString *fullJIDStr = [r_jid stringValue]; + + [self setMyJID_setByServer:[XMPPJID jidWithString:fullJIDStr]]; + + // On to the next step + BOOL skipStartSessionOverride = NO; + [self continuePostBinding:skipStartSessionOverride]; + } + else + { + // It appears the server didn't allow our resource choice + // First check if we want to try an alternative resource + + NSXMLElement *r_error = [response elementForName:@"error"]; + NSXMLElement *r_conflict = [r_error elementForName:@"conflict" xmlns:@"urn:ietf:params:xml:ns:xmpp-stanzas"]; + + if (r_conflict) + { + SEL selector = @selector(xmppStream:alternativeResourceForConflictingResource:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // None of the delegates implement the method. + // Use a shortcut. + + [self continueHandleStandardBinding:nil]; + } + else + { + // Query all interested delegates. + // This must be done serially to maintain thread safety. + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(concurrentQueue, ^{ @autoreleasepool { + + // Query delegates for alternative resource + + NSString *currentResource = [[self myJID] resource]; + __block NSString *alternativeResource = nil; + + id delegate; + dispatch_queue_t dq; + + while ([delegateEnumerator getNextDelegate:&delegate delegateQueue:&dq forSelector:selector]) + { + dispatch_sync(dq, ^{ @autoreleasepool { + + NSString *delegateAlternativeResource = + [delegate xmppStream:self alternativeResourceForConflictingResource:currentResource]; + + if (delegateAlternativeResource) + { + alternativeResource = delegateAlternativeResource; + } + }}); + + if (alternativeResource) { + break; + } + } + + dispatch_async(xmppQueue, ^{ @autoreleasepool { + + [self continueHandleStandardBinding:alternativeResource]; + + }}); + + }}); + } + } + else + { + // Appears to be a conflicting resource, but server didn't specify conflict + [self continueHandleStandardBinding:nil]; + } + } +} + +- (void)continueHandleStandardBinding:(NSString *)alternativeResource +{ + XMPPLogTrace(); + + if ([alternativeResource length] > 0) + { + // Update myJID + + [self setMyJID_setByClient:[myJID_setByClient jidWithNewResource:alternativeResource]]; + + NSXMLElement *resource = [NSXMLElement elementWithName:@"resource"]; + [resource setStringValue:alternativeResource]; + + NSXMLElement *bind = [NSXMLElement elementWithName:@"bind" xmlns:@"urn:ietf:params:xml:ns:xmpp-bind"]; + [bind addChild:resource]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set"]; + [iq addChild:bind]; + + NSString *outgoingStr = [iq compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + + [idTracker addElement:iq + target:nil + selector:NULL + timeout:XMPPIDTrackerTimeoutNone]; + + // The state remains in STATE_XMPP_BINDING + } + else + { + // We'll simply let the server choose then + + NSXMLElement *bind = [NSXMLElement elementWithName:@"bind" xmlns:@"urn:ietf:params:xml:ns:xmpp-bind"]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set"]; + [iq addChild:bind]; + + NSString *outgoingStr = [iq compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + + [idTracker addElement:iq + target:nil + selector:NULL + timeout:XMPPIDTrackerTimeoutNone]; + + // The state remains in STATE_XMPP_BINDING + } +} + +- (void)continuePostBinding:(BOOL)skipStartSessionOverride +{ + XMPPLogTrace(); + + // And we may now have to do one last thing before we're ready - start an IM session + NSXMLElement *features = [rootElement elementForName:@"stream:features"]; + + // Check to see if a session is required + // Don't forget about that NSXMLElement bug you reported to apple (xmlns is required or element won't be found) + NSXMLElement *f_session = [features elementForName:@"session" xmlns:@"urn:ietf:params:xml:ns:xmpp-session"]; + + if (f_session && !skipStartSession && !skipStartSessionOverride) + { + NSXMLElement *session = [NSXMLElement elementWithName:@"session"]; + [session setXmlns:@"urn:ietf:params:xml:ns:xmpp-session"]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" elementID:[self generateUUID]]; + [iq addChild:session]; + + NSString *outgoingStr = [iq compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + + [idTracker addElement:iq + target:nil + selector:NULL + timeout:XMPPIDTrackerTimeoutNone]; + + // Update state + state = STATE_XMPP_START_SESSION; + } + else + { + // Revert back to connected state (from binding state) + state = STATE_XMPP_CONNECTED; + + [multicastDelegate xmppStreamDidAuthenticate:self]; + } +} + +- (void)handleStartSessionResponse:(NSXMLElement *)response +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + if ([[response attributeStringValueForName:@"type"] isEqualToString:@"result"]) + { + // Revert back to connected state (from start session state) + state = STATE_XMPP_CONNECTED; + + [multicastDelegate xmppStreamDidAuthenticate:self]; + } + else + { + // Revert back to connected state (from start session state) + state = STATE_XMPP_CONNECTED; + + [multicastDelegate xmppStream:self didNotAuthenticate:response]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPSRVResolver Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)tryNextSrvResult +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + NSError *connectError = nil; + BOOL success = NO; + + while (srvResultsIndex < [srvResults count]) + { + XMPPSRVRecord *srvRecord = srvResults[srvResultsIndex]; + NSString *srvHost = srvRecord.target; + UInt16 srvPort = srvRecord.port; + + success = [self connectToHost:srvHost onPort:srvPort withTimeout:XMPPStreamTimeoutNone error:&connectError]; + + if (success) + { + break; + } + else + { + srvResultsIndex++; + } + } + + if (!success) + { + // SRV resolution of the JID domain failed. + // As per the RFC: + // + // "If the SRV lookup fails, the fallback is a normal IPv4/IPv6 address record resolution + // to determine the IP address, using the "xmpp-client" port 5222, registered with the IANA." + // + // In other words, just try connecting to the domain specified in the JID. + + success = [self connectToHost:[myJID_setByClient domain] onPort:5222 withTimeout:XMPPStreamTimeoutNone error:&connectError]; + } + + if (!success) + { + [self endConnectTimeout]; + + state = STATE_XMPP_DISCONNECTED; + + [multicastDelegate xmppStreamDidDisconnect:self withError:connectError]; + } +} + +- (void)xmppSRVResolver:(XMPPSRVResolver *)sender didResolveRecords:(NSArray *)records +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + if (sender != srvResolver) return; + + XMPPLogTrace(); + + srvResults = [records copy]; + srvResultsIndex = 0; + + state = STATE_XMPP_CONNECTING; + + [self tryNextSrvResult]; +} + +- (void)xmppSRVResolver:(XMPPSRVResolver *)sender didNotResolveDueToError:(NSError *)error +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + if (sender != srvResolver) return; + + XMPPLogTrace(); + + state = STATE_XMPP_CONNECTING; + + [self tryNextSrvResult]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark AsyncSocket Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Called when a socket connects and is ready for reading and writing. "host" will be an IP address, not a DNS name. +**/ +- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port +{ + // This method is invoked on the xmppQueue. + // + // The TCP connection is now established. + + XMPPLogTrace(); + + [self endConnectTimeout]; + + #if TARGET_OS_IPHONE + { + if (self.enableBackgroundingOnSocket) + { + __block BOOL result; + + [asyncSocket performBlock:^{ + result = [asyncSocket enableBackgroundingOnSocket]; + }]; + + if (result) + XMPPLogVerbose(@"%@: Enabled backgrounding on socket", THIS_FILE); + else + XMPPLogError(@"%@: Error enabling backgrounding on socket!", THIS_FILE); + } + } + #endif + + [multicastDelegate xmppStream:self socketDidConnect:sock]; + + srvResolver = nil; + srvResults = nil; + + // Are we using old-style SSL? (Not the upgrade to TLS technique specified in the XMPP RFC) + if ([self isSecure]) + { + // The connection must be secured immediately (just like with HTTPS) + [self startTLS]; + } + else + { + [self startNegotiation]; + } +} + +- (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust + completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler +{ + XMPPLogTrace(); + + SEL selector = @selector(xmppStream:didReceiveTrust:completionHandler:); + + if ([multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + [multicastDelegate xmppStream:self didReceiveTrust:trust completionHandler:completionHandler]; + } + else + { + XMPPLogWarn(@"%@: Stream secured with (GCDAsyncSocketManuallyEvaluateTrust == YES)," + @" but there are no delegates that implement xmppStream:didReceiveTrust:completionHandler:." + @" This is likely a mistake.", THIS_FILE); + + // The delegate method should likely have code similar to this, + // but will presumably perform some extra security code stuff. + // For example, allowing a specific self-signed certificate that is known to the app. + + dispatch_queue_t bgQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(bgQueue, ^{ + + SecTrustResultType result = kSecTrustResultDeny; + OSStatus status = SecTrustEvaluate(trust, &result); + + if (status == noErr && (result == kSecTrustResultProceed || result == kSecTrustResultUnspecified)) { + completionHandler(YES); + } + else { + completionHandler(NO); + } + }); + } +} + +- (void)socketDidSecure:(GCDAsyncSocket *)sock +{ + // This method is invoked on the xmppQueue. + + XMPPLogTrace(); + + [multicastDelegate xmppStreamDidSecure:self]; +} + +/** + * Called when a socket has completed reading the requested data. Not called if there is an error. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag +{ + // This method is invoked on the xmppQueue. + + XMPPLogTrace(); + + lastSendReceiveTime = [NSDate timeIntervalSinceReferenceDate]; + numberOfBytesReceived += [data length]; + + XMPPLogRecvPre(@"RECV: %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); + + // Asynchronously parse the xml data + [parser parseData:data]; + + if ([self isSecure]) + { + // Continue reading for XML elements + if (state == STATE_XMPP_OPENING) + { + [asyncSocket readDataWithTimeout:TIMEOUT_XMPP_READ_START tag:TAG_XMPP_READ_START]; + } + else + { + [asyncSocket readDataWithTimeout:TIMEOUT_XMPP_READ_STREAM tag:TAG_XMPP_READ_STREAM]; + } + } + else + { + // Don't queue up a read on the socket as we may need to upgrade to TLS. + // We'll read more data after we've parsed the current chunk of data. + } +} + +/** + * Called after data with the given tag has been successfully sent. +**/ +- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag +{ + // This method is invoked on the xmppQueue. + + XMPPLogTrace(); + + lastSendReceiveTime = [NSDate timeIntervalSinceReferenceDate]; + + if (tag == TAG_XMPP_WRITE_RECEIPT) + { + if ([receipts count] == 0) + { + XMPPLogWarn(@"%@: Found TAG_XMPP_WRITE_RECEIPT with no pending receipts!", THIS_FILE); + return; + } + + XMPPElementReceipt *receipt = receipts[0]; + [receipt signalSuccess]; + [receipts removeObjectAtIndex:0]; + } + else if (tag == TAG_XMPP_WRITE_STOP) + { + [multicastDelegate xmppStreamDidSendClosingStreamStanza:self]; + } +} + +/** + * Called when a socket disconnects with or without error. +**/ +- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err +{ + // This method is invoked on the xmppQueue. + + XMPPLogTrace(); + + [self endConnectTimeout]; + + if (srvResults && (++srvResultsIndex < [srvResults count])) + { + [self tryNextSrvResult]; + } + else + { + // Update state + state = STATE_XMPP_DISCONNECTED; + + // Release the parser (to free underlying resources) + [parser setDelegate:nil delegateQueue:NULL]; + parser = nil; + + // Clear any saved authentication information + auth = nil; + + authenticationDate = nil; + + // Clear stored elements + myJID_setByServer = nil; + myPresence = nil; + rootElement = nil; + + // Stop the keep alive timer + if (keepAliveTimer) + { + dispatch_source_cancel(keepAliveTimer); + keepAliveTimer = NULL; + } + + // Clear srv results + srvResolver = nil; + srvResults = nil; + + // Stop tracking IDs + [idTracker removeAllIDs]; + + // Clear any pending receipts + for (XMPPElementReceipt *receipt in receipts) + { + [receipt signalFailure]; + } + [receipts removeAllObjects]; + + // Clear flags + flags = 0; + + // Notify delegate + + if (parserError || otherError) + { + NSError *error = parserError ? : otherError; + + [multicastDelegate xmppStreamDidDisconnect:self withError:error]; + + parserError = nil; + otherError = nil; + } + else + { + [multicastDelegate xmppStreamDidDisconnect:self withError:err]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPParser Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Called when the xmpp parser has read in the entire root element. +**/ +- (void)xmppParser:(XMPPParser *)sender didReadRoot:(NSXMLElement *)root +{ + // This method is invoked on the xmppQueue. + + if (sender != parser) return; + + XMPPLogTrace(); + XMPPLogRecvPost(@"RECV: %@", [root compactXMLString]); + + // At this point we've sent our XML stream header, and we've received the response XML stream header. + // We save the root element of our stream for future reference. + + rootElement = root; + + if ([self isP2P]) + { + // XEP-0174 specifies that SHOULD be sent by the receiver. + // In other words, if we're the recipient we will now send our features. + // But if we're the initiator, we can't depend on receiving their features. + + // Either way, we're connected at this point. + state = STATE_XMPP_CONNECTED; + + if ([self isP2PRecipient]) + { + // Extract the remoteJID: + // + // + + NSString *from = [[rootElement attributeForName:@"from"] stringValue]; + remoteJID = [XMPPJID jidWithString:from]; + + // Send our stream features. + // To do so we need to ask the delegate to fill it out for us. + + NSXMLElement *streamFeatures = [NSXMLElement elementWithName:@"stream:features"]; + + [multicastDelegate xmppStream:self willSendP2PFeatures:streamFeatures]; + + NSString *outgoingStr = [streamFeatures compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + } + + // Make sure the delegate didn't disconnect us in the xmppStream:willSendP2PFeatures: method. + + if ([self isConnected]) + { + [multicastDelegate xmppStreamDidConnect:self]; + } + } + else + { + // Check for RFC compliance + if ([self serverXmppStreamVersionNumber] >= 1.0) + { + // Update state - we're now onto stream negotiations + state = STATE_XMPP_NEGOTIATING; + + // Note: We're waiting for the now + } + else + { + // The server isn't RFC comliant, and won't be sending any stream features. + + // We would still like to know what authentication features it supports though, + // so we'll use the jabber:iq:auth namespace, which was used prior to the RFC spec. + + // Update state - we're onto psuedo negotiation + state = STATE_XMPP_NEGOTIATING; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:auth"]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" elementID:[self generateUUID]]; + [iq addChild:query]; + + NSString *outgoingStr = [iq compactXMLString]; + NSData *outgoingData = [outgoingStr dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogSend(@"SEND: %@", outgoingStr); + numberOfBytesSent += [outgoingData length]; + + [asyncSocket writeData:outgoingData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + + // Now wait for the response IQ + } + } +} + +- (void)xmppParser:(XMPPParser *)sender didReadElement:(NSXMLElement *)element +{ + // This method is invoked on the xmppQueue. + + if (sender != parser) return; + + XMPPLogTrace(); + XMPPLogRecvPost(@"RECV: %@", [element compactXMLString]); + + NSString *elementName = [element name]; + + if ([elementName isEqualToString:@"stream:error"] || [elementName isEqualToString:@"error"]) + { + [multicastDelegate xmppStream:self didReceiveError:element]; + + return; + } + + if (state == STATE_XMPP_NEGOTIATING) + { + // We've just read in the stream features + // We consider this part of the root element, so we'll add it (replacing any previously sent features) + [rootElement setChildren:@[element]]; + + // Call a method to handle any requirements set forth in the features + [self handleStreamFeatures]; + } + else if (state == STATE_XMPP_STARTTLS_1) + { + // The response from our starttls message + [self handleStartTLSResponse:element]; + } + else if (state == STATE_XMPP_REGISTERING) + { + // The iq response from our registration request + [self handleRegistration:element]; + } + else if (state == STATE_XMPP_AUTH) + { + // Some response to the authentication process + [self handleAuth:element]; + } + else if (state == STATE_XMPP_BINDING) + { + if (customBinding) + { + [self handleCustomBinding:element]; + } + else + { + BOOL invalid = NO; + if (validatesResponses) + { + XMPPIQ *iq = [XMPPIQ iqFromElement:element]; + if (![idTracker invokeForElement:iq withObject:nil]) + { + invalid = YES; + } + } + if (!invalid) + { + // The response from our binding request + [self handleStandardBinding:element]; + } + } + } + else if (state == STATE_XMPP_START_SESSION) + { + BOOL invalid = NO; + if (validatesResponses) + { + XMPPIQ *iq = [XMPPIQ iqFromElement:element]; + if (![idTracker invokeForElement:iq withObject:nil]) + { + invalid = YES; + } + } + if (!invalid) + { + // The response from our start session request + [self handleStartSessionResponse:element]; + } + } + else + { + if ([elementName isEqualToString:@"iq"]) + { + [self receiveIQ:[XMPPIQ iqFromElement:element]]; + } + else if ([elementName isEqualToString:@"message"]) + { + [self receiveMessage:[XMPPMessage messageFromElement:element]]; + } + else if ([elementName isEqualToString:@"presence"]) + { + [self receivePresence:[XMPPPresence presenceFromElement:element]]; + } + else if ([self isP2P] && + ([elementName isEqualToString:@"stream:features"] || [elementName isEqualToString:@"features"])) + { + [multicastDelegate xmppStream:self didReceiveP2PFeatures:element]; + } + else if ([customElementNames countForObject:elementName]) + { + [multicastDelegate xmppStream:self didReceiveCustomElement:element]; + } + else + { + [multicastDelegate xmppStream:self didReceiveError:element]; + } + } +} + +- (void)xmppParserDidParseData:(XMPPParser *)sender +{ + // This method is invoked on the xmppQueue. + + if (sender != parser) return; + + XMPPLogTrace(); + + if (![self isSecure]) + { + // Continue reading for XML elements + if (state == STATE_XMPP_OPENING) + { + [asyncSocket readDataWithTimeout:TIMEOUT_XMPP_READ_START tag:TAG_XMPP_READ_START]; + } + else if (state != STATE_XMPP_STARTTLS_2) // Don't queue read operation prior to [asyncSocket startTLS:] + { + [asyncSocket readDataWithTimeout:TIMEOUT_XMPP_READ_STREAM tag:TAG_XMPP_READ_STREAM]; + } + } +} + +- (void)xmppParserDidEnd:(XMPPParser *)sender +{ + // This method is invoked on the xmppQueue. + + if (sender != parser) return; + + XMPPLogTrace(); + + [asyncSocket disconnect]; +} + +- (void)xmppParser:(XMPPParser *)sender didFail:(NSError *)error +{ + // This method is invoked on the xmppQueue. + + if (sender != parser) return; + + XMPPLogTrace(); + + parserError = error; + [asyncSocket disconnect]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Keep Alive +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)setupKeepAliveTimer +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + if (keepAliveTimer) + { + dispatch_source_cancel(keepAliveTimer); + keepAliveTimer = NULL; + } + + if (state == STATE_XMPP_CONNECTED) + { + if (keepAliveInterval > 0) + { + keepAliveTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, xmppQueue); + + dispatch_source_set_event_handler(keepAliveTimer, ^{ @autoreleasepool { + + [self keepAlive]; + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theKeepAliveTimer = keepAliveTimer; + + dispatch_source_set_cancel_handler(keepAliveTimer, ^{ + XMPPLogVerbose(@"dispatch_release(keepAliveTimer)"); + dispatch_release(theKeepAliveTimer); + }); + #endif + + // Everytime we send or receive data, we update our lastSendReceiveTime. + // We set our timer to fire several times per keepAliveInterval. + // This allows us to maintain a single timer, + // and an acceptable timer resolution (assuming larger keepAliveIntervals). + + uint64_t interval = ((keepAliveInterval / 4.0) * NSEC_PER_SEC); + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, interval); + + dispatch_source_set_timer(keepAliveTimer, tt, interval, 1.0); + dispatch_resume(keepAliveTimer); + } + } +} + +- (void)keepAlive +{ + NSAssert(dispatch_get_specific(xmppQueueTag), @"Invoked on incorrect queue"); + + if (state == STATE_XMPP_CONNECTED) + { + NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate]; + NSTimeInterval elapsed = (now - lastSendReceiveTime); + + if (elapsed < 0 || elapsed >= keepAliveInterval) + { + numberOfBytesSent += [keepAliveData length]; + + [asyncSocket writeData:keepAliveData + withTimeout:TIMEOUT_XMPP_WRITE + tag:TAG_XMPP_WRITE_STREAM]; + + // Force update the lastSendReceiveTime here just to be safe. + // + // In case the TCP socket comes to a crawl with a giant element in the queue, + // which would prevent the socket:didWriteDataWithTag: method from being called for some time. + + lastSendReceiveTime = [NSDate timeIntervalSinceReferenceDate]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Stanza Validation +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isValidResponseElementFrom:(XMPPJID *)from forRequestElementTo:(XMPPJID *)to +{ + BOOL valid = YES; + + if(to) + { + if(![to isEqualToJID:from]) + { + valid = NO; + } + } +/** + * Replies for Stanza's that had no TO will be accepted if the FROM is: + * + * No from. + * from = the bare account JID. + * from = the full account JID (legal in 3920, but not 6120). + * from = the server's domain. +**/ + else if(!to && from) + { + if(![from isEqualToJID:self.myJID options:XMPPJIDCompareBare] + && ![from isEqualToJID:self.myJID options:XMPPJIDCompareFull] + && ![from isEqualToJID:[self.myJID domainJID] options:XMPPJIDCompareFull]) + { + valid = NO; + } + } + + return valid; +} + +- (BOOL)isValidResponseElement:(XMPPElement *)response forRequestElement:(XMPPElement *)request +{ + if(!response || !request) return NO; + + return [self isValidResponseElementFrom:[response from] forRequestElementTo:[request to]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Module Plug-In System +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)registerModule:(XMPPModule *)module +{ + if (module == nil) return; + + // Asynchronous operation + + dispatch_block_t block = ^{ @autoreleasepool { + + // Register module + + [registeredModules addObject:module]; + + // Add auto delegates (if there are any) + + NSString *className = NSStringFromClass([module class]); + GCDMulticastDelegate *autoDelegates = autoDelegateDict[className]; + + GCDMulticastDelegateEnumerator *autoDelegatesEnumerator = [autoDelegates delegateEnumerator]; + id delegate; + dispatch_queue_t delegateQueue; + + while ([autoDelegatesEnumerator getNextDelegate:&delegate delegateQueue:&delegateQueue]) + { + [module addDelegate:delegate delegateQueue:delegateQueue]; + } + + // Notify our own delegate(s) + + [multicastDelegate xmppStream:self didRegisterModule:module]; + + }}; + + // Asynchronous operation + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (void)unregisterModule:(XMPPModule *)module +{ + if (module == nil) return; + + // Synchronous operation + + dispatch_block_t block = ^{ @autoreleasepool { + + // Notify our own delegate(s) + + [multicastDelegate xmppStream:self willUnregisterModule:module]; + + // Remove auto delegates (if there are any) + + NSString *className = NSStringFromClass([module class]); + GCDMulticastDelegate *autoDelegates = autoDelegateDict[className]; + + GCDMulticastDelegateEnumerator *autoDelegatesEnumerator = [autoDelegates delegateEnumerator]; + id delegate; + dispatch_queue_t delegateQueue; + + while ([autoDelegatesEnumerator getNextDelegate:&delegate delegateQueue:&delegateQueue]) + { + // The module itself has dispatch_sync'd in order to invoke its deactivate method, + // which has in turn invoked this method. If we call back into the module, + // and have it dispatch_sync again, we're going to get a deadlock. + // So we must remove the delegate(s) asynchronously. + + [module removeDelegate:delegate delegateQueue:delegateQueue synchronously:NO]; + } + + // Unregister modules + + [registeredModules removeObject:module]; + + }}; + + // Synchronous operation + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); +} + +- (void)autoAddDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue toModulesOfClass:(Class)aClass +{ + if (delegate == nil) return; + if (aClass == nil) return; + + // Asynchronous operation + + dispatch_block_t block = ^{ @autoreleasepool { + + NSString *className = NSStringFromClass(aClass); + + // Add the delegate to all currently registered modules of the given class. + + for (XMPPModule *module in registeredModules) + { + if ([module isKindOfClass:aClass]) + { + [module addDelegate:delegate delegateQueue:delegateQueue]; + } + } + + // Add the delegate to list of auto delegates for the given class. + // It will be added as a delegate to future registered modules of the given class. + + id delegates = autoDelegateDict[className]; + if (delegates == nil) + { + delegates = [[GCDMulticastDelegate alloc] init]; + + autoDelegateDict[className] = delegates; + } + + [delegates addDelegate:delegate delegateQueue:delegateQueue]; + + }}; + + // Asynchronous operation + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_async(xmppQueue, block); +} + +- (void)removeAutoDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue fromModulesOfClass:(Class)aClass +{ + if (delegate == nil) return; + // delegateQueue may be NULL + // aClass may be NULL + + // Synchronous operation + + dispatch_block_t block = ^{ @autoreleasepool { + + if (aClass == NULL) + { + // Remove the delegate from all currently registered modules of ANY class. + + for (XMPPModule *module in registeredModules) + { + [module removeDelegate:delegate delegateQueue:delegateQueue]; + } + + // Remove the delegate from list of auto delegates for all classes, + // so that it will not be auto added as a delegate to future registered modules. + + for (GCDMulticastDelegate *delegates in [autoDelegateDict objectEnumerator]) + { + [delegates removeDelegate:delegate delegateQueue:delegateQueue]; + } + } + else + { + NSString *className = NSStringFromClass(aClass); + + // Remove the delegate from all currently registered modules of the given class. + + for (XMPPModule *module in registeredModules) + { + if ([module isKindOfClass:aClass]) + { + [module removeDelegate:delegate delegateQueue:delegateQueue]; + } + } + + // Remove the delegate from list of auto delegates for the given class, + // so that it will not be added as a delegate to future registered modules of the given class. + + GCDMulticastDelegate *delegates = autoDelegateDict[className]; + [delegates removeDelegate:delegate delegateQueue:delegateQueue]; + + if ([delegates count] == 0) + { + [autoDelegateDict removeObjectForKey:className]; + } + } + + }}; + + // Synchronous operation + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); +} + +- (void)enumerateModulesWithBlock:(void (^)(XMPPModule *module, NSUInteger idx, BOOL *stop))enumBlock +{ + if (enumBlock == NULL) return; + + dispatch_block_t block = ^{ @autoreleasepool { + + NSUInteger i = 0; + BOOL stop = NO; + + for (XMPPModule *module in registeredModules) + { + enumBlock(module, i, &stop); + + if (stop) + break; + else + i++; + } + }}; + + // Synchronous operation + + if (dispatch_get_specific(xmppQueueTag)) + block(); + else + dispatch_sync(xmppQueue, block); +} + +- (void)enumerateModulesOfClass:(Class)aClass withBlock:(void (^)(XMPPModule *module, NSUInteger idx, BOOL *stop))block +{ + [self enumerateModulesWithBlock:^(XMPPModule *module, NSUInteger idx, BOOL *stop) + { + if([module isKindOfClass:aClass]) + { + block(module,idx,stop); + } + }]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (NSString *)generateUUID +{ + NSString *result = nil; + + CFUUIDRef uuid = CFUUIDCreate(NULL); + if (uuid) + { + result = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, uuid); + CFRelease(uuid); + } + + return result; +} + +- (NSString *)generateUUID +{ + return [[self class] generateUUID]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPElementReceipt + +static const uint32_t receipt_unknown = 0 << 0; +static const uint32_t receipt_failure = 1 << 0; +static const uint32_t receipt_success = 1 << 1; + + +- (id)init +{ + if ((self = [super init])) + { + atomicFlags = receipt_unknown; + semaphore = dispatch_semaphore_create(0); + } + return self; +} + +- (void)signalSuccess +{ + uint32_t mask = receipt_success; + OSAtomicOr32Barrier(mask, &atomicFlags); + + dispatch_semaphore_signal(semaphore); +} + +- (void)signalFailure +{ + uint32_t mask = receipt_failure; + OSAtomicOr32Barrier(mask, &atomicFlags); + + dispatch_semaphore_signal(semaphore); +} + +- (BOOL)wait:(NSTimeInterval)timeout_seconds +{ + uint32_t mask = 0; + uint32_t flags = OSAtomicOr32Barrier(mask, &atomicFlags); + + if (flags != receipt_unknown) return (flags == receipt_success); + + dispatch_time_t timeout_nanos; + + if (isless(timeout_seconds, 0.0)) + timeout_nanos = DISPATCH_TIME_FOREVER; + else + timeout_nanos = dispatch_time(DISPATCH_TIME_NOW, (timeout_seconds * NSEC_PER_SEC)); + + // dispatch_semaphore_wait + // + // Decrement the counting semaphore. If the resulting value is less than zero, + // this function waits in FIFO order for a signal to occur before returning. + // + // Returns zero on success, or non-zero if the timeout occurred. + // + // Note: If the timeout occurs, the semaphore value is incremented (without signaling). + + long result = dispatch_semaphore_wait(semaphore, timeout_nanos); + + if (result == 0) + { + flags = OSAtomicOr32Barrier(mask, &atomicFlags); + + return (flags == receipt_success); + } + else + { + // Timed out waiting... + return NO; + } +} + +- (void)dealloc +{ + #if !OS_OBJECT_USE_OBJC + dispatch_release(semaphore); + #endif +} + +@end diff --git a/Example/PNXMPPFramework.xcodeproj/project.pbxproj b/Example/PNXMPPFramework.xcodeproj/project.pbxproj index 32f662e..586e13c 100644 --- a/Example/PNXMPPFramework.xcodeproj/project.pbxproj +++ b/Example/PNXMPPFramework.xcodeproj/project.pbxproj @@ -467,7 +467,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.1; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -500,7 +500,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.1; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; diff --git a/Example/Podfile.lock b/Example/Podfile.lock index e9e0ea4..3fc1cbe 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -6,14 +6,11 @@ PODS: - CocoaAsyncSocket/RunLoop - CocoaAsyncSocket/GCD (7.4.3) - CocoaAsyncSocket/RunLoop (7.4.3) - - CocoaLumberjack (2.2.0): - - CocoaLumberjack/Default (= 2.2.0) - - CocoaLumberjack/Extensions (= 2.2.0) - - CocoaLumberjack/Core (2.2.0) - - CocoaLumberjack/Default (2.2.0): + - CocoaLumberjack (1.9.2): + - CocoaLumberjack/Extensions (= 1.9.2) + - CocoaLumberjack/Core (1.9.2) + - CocoaLumberjack/Extensions (1.9.2): - CocoaLumberjack/Core - - CocoaLumberjack/Extensions (2.2.0): - - CocoaLumberjack/Default - FBSnapshotTestCase (2.0.7): - FBSnapshotTestCase/SwiftSupport (= 2.0.7) - FBSnapshotTestCase/Core (2.0.7) @@ -25,9 +22,145 @@ PODS: - KissXML/Standard (5.0.3): - KissXML/Core - PNXMPPFramework (0.1.0): + - PNXMPPFramework/Authentication (= 0.1.0) + - PNXMPPFramework/BandwidthMonitor (= 0.1.0) + - PNXMPPFramework/Categories (= 0.1.0) + - PNXMPPFramework/Core (= 0.1.0) + - PNXMPPFramework/CoreDataStorage (= 0.1.0) + - PNXMPPFramework/GoogleSharedStatus (= 0.1.0) + - PNXMPPFramework/ProcessOne (= 0.1.0) + - PNXMPPFramework/Reconnect (= 0.1.0) + - PNXMPPFramework/Roster (= 0.1.0) + - PNXMPPFramework/SystemInputActivityMonitor (= 0.1.0) + - PNXMPPFramework/Utilities (= 0.1.0) + - PNXMPPFramework/XEP-0009 (= 0.1.0) + - PNXMPPFramework/XEP-0012 (= 0.1.0) + - PNXMPPFramework/XEP-0016 (= 0.1.0) + - PNXMPPFramework/XEP-0045 (= 0.1.0) + - PNXMPPFramework/XEP-0054 (= 0.1.0) + - PNXMPPFramework/XEP-0059 (= 0.1.0) + - PNXMPPFramework/XEP-0060 (= 0.1.0) + - PNXMPPFramework/XEP-0065 (= 0.1.0) + - PNXMPPFramework/XEP-0066 (= 0.1.0) + - PNXMPPFramework/XEP-0082 (= 0.1.0) + - PNXMPPFramework/XEP-0085 (= 0.1.0) + - PNXMPPFramework/XEP-0092 (= 0.1.0) + - PNXMPPFramework/XEP-0100 (= 0.1.0) + - PNXMPPFramework/XEP-0106 (= 0.1.0) + - PNXMPPFramework/XEP-0115 (= 0.1.0) + - PNXMPPFramework/XEP-0136 (= 0.1.0) + - PNXMPPFramework/XEP-0153 (= 0.1.0) + - PNXMPPFramework/XEP-0172 (= 0.1.0) + - PNXMPPFramework/XEP-0184 (= 0.1.0) + - PNXMPPFramework/XEP-0191 (= 0.1.0) + - PNXMPPFramework/XEP-0198 (= 0.1.0) + - PNXMPPFramework/XEP-0199 (= 0.1.0) + - PNXMPPFramework/XEP-0202 (= 0.1.0) + - PNXMPPFramework/XEP-0203 (= 0.1.0) + - PNXMPPFramework/XEP-0223 (= 0.1.0) + - PNXMPPFramework/XEP-0224 (= 0.1.0) + - PNXMPPFramework/XEP-0280 (= 0.1.0) + - PNXMPPFramework/XEP-0297 (= 0.1.0) + - PNXMPPFramework/XEP-0308 (= 0.1.0) + - PNXMPPFramework/XEP-0333 (= 0.1.0) + - PNXMPPFramework/XEP-0335 (= 0.1.0) + - PNXMPPFramework/Authentication (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/BandwidthMonitor (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/Categories (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/Core (0.1.0): - CocoaAsyncSocket - - CocoaLumberjack + - CocoaLumberjack (~> 1.9) - KissXML + - PNXMPPFramework/CoreDataStorage (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/GoogleSharedStatus (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/ProcessOne (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/Reconnect (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/Roster (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/CoreDataStorage + - PNXMPPFramework/SystemInputActivityMonitor (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/Utilities (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0009 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0012 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0016 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0045 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/CoreDataStorage + - PNXMPPFramework/XEP-0203 + - PNXMPPFramework/XEP-0054 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/Roster + - PNXMPPFramework/XEP-0059 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0060 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0065 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0066 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0082 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0085 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0092 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0100 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0106 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0115 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/CoreDataStorage + - PNXMPPFramework/XEP-0136 (0.1.0): + - PNXMPPFramework/CoreDataStorage + - PNXMPPFramework/XEP-0085 + - PNXMPPFramework/XEP-0203 + - PNXMPPFramework/XEP-0153 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0054 + - PNXMPPFramework/XEP-0172 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0184 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0191 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0198 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0199 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0202 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0082 + - PNXMPPFramework/XEP-0203 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0082 + - PNXMPPFramework/XEP-0223 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0224 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0280 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0297 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0203 + - PNXMPPFramework/XEP-0308 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0333 (0.1.0): + - PNXMPPFramework/Core + - PNXMPPFramework/XEP-0335 (0.1.0): + - PNXMPPFramework/Core DEPENDENCIES: - FBSnapshotTestCase @@ -39,10 +172,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: CocoaAsyncSocket: a18c75dca4b08723628a0bacca6e94803d90be91 - CocoaLumberjack: 17fe8581f84914d5d7e6360f7c70022b173c3ae0 + CocoaLumberjack: 628fca2e88ef06f7cf6817309aa405f325d9a6fa FBSnapshotTestCase: 7e85180d0d141a0cf472352edda7e80d7eaeb547 KissXML: d19dd6dc65e0dc721ba92b3077b8ebdd240f1c1e - PNXMPPFramework: 85a177de196fd742392f6ed0053c9cd2dd160f06 + PNXMPPFramework: 69989a4b1f7470763acd3e9a3d982ddfa5c161df PODFILE CHECKSUM: c24dacdc80a49fe0e0fea049a6d762eb76667498 diff --git a/Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.h b/Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.h deleted file mode 100644 index 0b568fb..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.h +++ /dev/null @@ -1,81 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -/** - * Welcome to CocoaLumberjack! - * - * The project page has a wealth of documentation if you have any questions. - * https://github.com/CocoaLumberjack/CocoaLumberjack - * - * If you're new to the project you may wish to read "Getting Started" at: - * Documentation/GettingStarted.md - * - * Otherwise, here is a quick refresher. - * There are three steps to using the macros: - * - * Step 1: - * Import the header in your implementation or prefix file: - * - * #import - * - * Step 2: - * Define your logging level in your implementation file: - * - * // Log levels: off, error, warn, info, verbose - * static const DDLogLevel ddLogLevel = DDLogLevelVerbose; - * - * Step 2 [3rd party frameworks]: - * - * Define your LOG_LEVEL_DEF to a different variable/function than ddLogLevel: - * - * // #undef LOG_LEVEL_DEF // Undefine first only if needed - * #define LOG_LEVEL_DEF myLibLogLevel - * - * Define your logging level in your implementation file: - * - * // Log levels: off, error, warn, info, verbose - * static const DDLogLevel myLibLogLevel = DDLogLevelVerbose; - * - * Step 3: - * Replace your NSLog statements with DDLog statements according to the severity of the message. - * - * NSLog(@"Fatal error, no dohickey found!"); -> DDLogError(@"Fatal error, no dohickey found!"); - * - * DDLog works exactly the same as NSLog. - * This means you can pass it multiple variables just like NSLog. - **/ - -#import - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif - -// Core -#import "DDLog.h" - -// Main macros -#import "DDLogMacros.h" -#import "DDAssertMacros.h" - -// Capture ASL -#import "DDASLLogCapture.h" - -// Loggers -#import "DDTTYLogger.h" -#import "DDASLLogger.h" -#import "DDFileLogger.h" - diff --git a/Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.swift b/Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.swift deleted file mode 100644 index 5f022ce..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.swift +++ /dev/null @@ -1,91 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2014-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -import Foundation - -extension DDLogFlag { - public static func fromLogLevel(logLevel: DDLogLevel) -> DDLogFlag { - return DDLogFlag(rawValue: logLevel.rawValue) - } - - public init(_ logLevel: DDLogLevel) { - self = DDLogFlag(rawValue: logLevel.rawValue) - } - - ///returns the log level, or the lowest equivalant. - public func toLogLevel() -> DDLogLevel { - if let ourValid = DDLogLevel(rawValue: self.rawValue) { - return ourValid - } else { - let logFlag:DDLogFlag = self - - if logFlag.contains(.Verbose) { - return .Verbose - } else if logFlag.contains(.Debug) { - return .Debug - } else if logFlag.contains(.Info) { - return .Info - } else if logFlag.contains(.Warning) { - return .Warning - } else if logFlag.contains(.Error) { - return .Error - } else { - return .Off - } - } - } -} - -public var defaultDebugLevel = DDLogLevel.Verbose - -public func resetDefaultDebugLevel() { - defaultDebugLevel = DDLogLevel.Verbose -} - -public func SwiftLogMacro(isAsynchronous: Bool, level: DDLogLevel, flag flg: DDLogFlag, context: Int = 0, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, tag: AnyObject? = nil, @autoclosure string: () -> String) { - if level.rawValue & flg.rawValue != 0 { - // Tell the DDLogMessage constructor to copy the C strings that get passed to it. - // Using string interpolation to prevent integer overflow warning when using StaticString.stringValue - let logMessage = DDLogMessage(message: string(), level: level, flag: flg, context: context, file: "\(file)", function: "\(function)", line: line, tag: tag, options: [.CopyFile, .CopyFunction], timestamp: nil) - DDLog.log(isAsynchronous, message: logMessage) - } -} - -public func DDLogDebug(@autoclosure logText: () -> String, level: DDLogLevel = defaultDebugLevel, context: Int = 0, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, tag: AnyObject? = nil, asynchronous async: Bool = true) { - SwiftLogMacro(async, level: level, flag: .Debug, context: context, file: file, function: function, line: line, tag: tag, string: logText) -} - -public func DDLogInfo(@autoclosure logText: () -> String, level: DDLogLevel = defaultDebugLevel, context: Int = 0, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, tag: AnyObject? = nil, asynchronous async: Bool = true) { - SwiftLogMacro(async, level: level, flag: .Info, context: context, file: file, function: function, line: line, tag: tag, string: logText) -} - -public func DDLogWarn(@autoclosure logText: () -> String, level: DDLogLevel = defaultDebugLevel, context: Int = 0, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, tag: AnyObject? = nil, asynchronous async: Bool = true) { - SwiftLogMacro(async, level: level, flag: .Warning, context: context, file: file, function: function, line: line, tag: tag, string: logText) -} - -public func DDLogVerbose(@autoclosure logText: () -> String, level: DDLogLevel = defaultDebugLevel, context: Int = 0, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, tag: AnyObject? = nil, asynchronous async: Bool = true) { - SwiftLogMacro(async, level: level, flag: .Verbose, context: context, file: file, function: function, line: line, tag: tag, string: logText) -} - -public func DDLogError(@autoclosure logText: () -> String, level: DDLogLevel = defaultDebugLevel, context: Int = 0, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, tag: AnyObject? = nil, asynchronous async: Bool = false) { - SwiftLogMacro(async, level: level, flag: .Error, context: context, file: file, function: function, line: line, tag: tag, string: logText) -} - -/// Analogous to the C preprocessor macro `THIS_FILE`. -public func CurrentFileName(fileName: StaticString = __FILE__) -> String { - // Using string interpolation to prevent integer overflow warning when using StaticString.stringValue - // This double-casting to NSString is necessary as changes to how Swift handles NSPathUtilities requres the string to be an NSString - return (("\(fileName)" as NSString).lastPathComponent as NSString).stringByDeletingPathExtension -} diff --git a/Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.h b/Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.h deleted file mode 100644 index f7fa79f..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.h +++ /dev/null @@ -1,48 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import "DDASLLogger.h" - -@protocol DDLogger; - -/** - * This class provides the ability to capture the ASL (Apple System Logs) - */ -@interface DDASLLogCapture : NSObject - -/** - * Start capturing logs - */ -+ (void)start; - -/** - * Stop capturing logs - */ -+ (void)stop; - -/** - * Returns the current capture level. - * @note Default log level: DDLogLevelVerbose (i.e. capture all ASL messages). - */ -+ (DDLogLevel)captureLevel; - -/** - * Set the capture level - * - * @param level new level - */ -+ (void)setCaptureLevel:(DDLogLevel)level; - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.m b/Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.m deleted file mode 100644 index 98d5342..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.m +++ /dev/null @@ -1,230 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import "DDASLLogCapture.h" - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif - -#import "DDLog.h" - -#include -#include -#include -#include - -static BOOL _cancel = YES; -static DDLogLevel _captureLevel = DDLogLevelVerbose; - -#ifdef __IPHONE_8_0 - #define DDASL_IOS_PIVOT_VERSION __IPHONE_8_0 -#endif -#ifdef __MAC_10_10 - #define DDASL_OSX_PIVOT_VERSION __MAC_10_10 -#endif - -@implementation DDASLLogCapture - -static aslmsg (*dd_asl_next)(aslresponse obj); -static void (*dd_asl_release)(aslresponse obj); - -+ (void)initialize -{ - #if (defined(DDASL_IOS_PIVOT_VERSION) && __IPHONE_OS_VERSION_MAX_ALLOWED >= DDASL_IOS_PIVOT_VERSION) || (defined(DDASL_OSX_PIVOT_VERSION) && __MAC_OS_X_VERSION_MAX_ALLOWED >= DDASL_OSX_PIVOT_VERSION) - #if __IPHONE_OS_VERSION_MIN_REQUIRED < DDASL_IOS_PIVOT_VERSION || __MAC_OS_X_VERSION_MIN_REQUIRED < DDASL_OSX_PIVOT_VERSION - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wdeprecated-declarations" - // Building on falsely advertised SDK, targeting deprecated API - dd_asl_next = &aslresponse_next; - dd_asl_release = &aslresponse_free; - #pragma GCC diagnostic pop - #else - // Building on lastest, correct SDK, targeting latest API - dd_asl_next = &asl_next; - dd_asl_release = &asl_release; - #endif - #else - // Building on old SDKs, targeting deprecated API - dd_asl_next = &aslresponse_next; - dd_asl_release = &aslresponse_free; - #endif -} - -+ (void)start { - // Ignore subsequent calls - if (!_cancel) { - return; - } - - _cancel = NO; - - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) { - [self captureAslLogs]; - }); -} - -+ (void)stop { - _cancel = YES; -} - -+ (DDLogLevel)captureLevel { - return _captureLevel; -} - -+ (void)setCaptureLevel:(DDLogLevel)level { - _captureLevel = level; -} - -#pragma mark - Private methods - -+ (void)configureAslQuery:(aslmsg)query { - const char param[] = "7"; // ASL_LEVEL_DEBUG, which is everything. We'll rely on regular DDlog log level to filter - - asl_set_query(query, ASL_KEY_LEVEL, param, ASL_QUERY_OP_LESS_EQUAL | ASL_QUERY_OP_NUMERIC); - - // Don't retrieve logs from our own DDASLLogger - asl_set_query(query, kDDASLKeyDDLog, kDDASLDDLogValue, ASL_QUERY_OP_NOT_EQUAL); - -#if !TARGET_OS_IPHONE || TARGET_SIMULATOR - int processId = [[NSProcessInfo processInfo] processIdentifier]; - char pid[16]; - sprintf(pid, "%d", processId); - asl_set_query(query, ASL_KEY_PID, pid, ASL_QUERY_OP_EQUAL | ASL_QUERY_OP_NUMERIC); -#endif -} - -+ (void)aslMessageReceived:(aslmsg)msg { - const char* messageCString = asl_get( msg, ASL_KEY_MSG ); - if ( messageCString == NULL ) - return; - - int flag; - BOOL async; - - const char* levelCString = asl_get(msg, ASL_KEY_LEVEL); - switch (levelCString? atoi(levelCString) : 0) { - // By default all NSLog's with a ASL_LEVEL_WARNING level - case ASL_LEVEL_EMERG : - case ASL_LEVEL_ALERT : - case ASL_LEVEL_CRIT : flag = DDLogFlagError; async = NO; break; - case ASL_LEVEL_ERR : flag = DDLogFlagWarning; async = YES; break; - case ASL_LEVEL_WARNING : flag = DDLogFlagInfo; async = YES; break; - case ASL_LEVEL_NOTICE : flag = DDLogFlagDebug; async = YES; break; - case ASL_LEVEL_INFO : - case ASL_LEVEL_DEBUG : - default : flag = DDLogFlagVerbose; async = YES; break; - } - - if (!(_captureLevel & flag)) { - return; - } - - // NSString * sender = [NSString stringWithCString:asl_get(msg, ASL_KEY_SENDER) encoding:NSUTF8StringEncoding]; - NSString *message = @(messageCString); - - const char* secondsCString = asl_get( msg, ASL_KEY_TIME ); - const char* nanoCString = asl_get( msg, ASL_KEY_TIME_NSEC ); - NSTimeInterval seconds = secondsCString ? strtod(secondsCString, NULL) : [NSDate timeIntervalSinceReferenceDate] - NSTimeIntervalSince1970; - double nanoSeconds = nanoCString? strtod(nanoCString, NULL) : 0; - NSTimeInterval totalSeconds = seconds + (nanoSeconds / 1e9); - - NSDate *timeStamp = [NSDate dateWithTimeIntervalSince1970:totalSeconds]; - - DDLogMessage *logMessage = [[DDLogMessage alloc]initWithMessage:message - level:_captureLevel - flag:flag - context:0 - file:@"DDASLLogCapture" - function:0 - line:0 - tag:nil - options:0 - timestamp:timeStamp]; - - [DDLog log:async message:logMessage]; -} - -+ (void)captureAslLogs { - @autoreleasepool - { - /* - We use ASL_KEY_MSG_ID to see each message once, but there's no - obvious way to get the "next" ID. To bootstrap the process, we'll - search by timestamp until we've seen a message. - */ - - struct timeval timeval = { - .tv_sec = 0 - }; - gettimeofday(&timeval, NULL); - unsigned long long startTime = timeval.tv_sec; - __block unsigned long long lastSeenID = 0; - - /* - syslogd posts kNotifyASLDBUpdate (com.apple.system.logger.message) - through the notify API when it saves messages to the ASL database. - There is some coalescing - currently it is sent at most twice per - second - but there is no documented guarantee about this. In any - case, there may be multiple messages per notification. - - Notify notifications don't carry any payload, so we need to search - for the messages. - */ - int notifyToken = 0; // Can be used to unregister with notify_cancel(). - notify_register_dispatch(kNotifyASLDBUpdate, ¬ifyToken, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(int token) - { - // At least one message has been posted; build a search query. - @autoreleasepool - { - aslmsg query = asl_new(ASL_TYPE_QUERY); - char stringValue[64]; - - if (lastSeenID > 0) { - snprintf(stringValue, sizeof stringValue, "%llu", lastSeenID); - asl_set_query(query, ASL_KEY_MSG_ID, stringValue, ASL_QUERY_OP_GREATER | ASL_QUERY_OP_NUMERIC); - } else { - snprintf(stringValue, sizeof stringValue, "%llu", startTime); - asl_set_query(query, ASL_KEY_TIME, stringValue, ASL_QUERY_OP_GREATER_EQUAL | ASL_QUERY_OP_NUMERIC); - } - - [self configureAslQuery:query]; - - // Iterate over new messages. - aslmsg msg; - aslresponse response = asl_search(NULL, query); - - while ((msg = dd_asl_next(response))) - { - [self aslMessageReceived:msg]; - - // Keep track of which messages we've seen. - lastSeenID = atoll(asl_get(msg, ASL_KEY_MSG_ID)); - } - dd_asl_release(response); - asl_free(query); - - if (_cancel) { - notify_cancel(token); - return; - } - - } - }); - } -} - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/DDASLLogger.h b/Example/Pods/CocoaLumberjack/Classes/DDASLLogger.h deleted file mode 100644 index 24cc1c3..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDASLLogger.h +++ /dev/null @@ -1,58 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif - -#import "DDLog.h" - -// Custom key set on messages sent to ASL -extern const char* const kDDASLKeyDDLog; - -// Value set for kDDASLKeyDDLog -extern const char* const kDDASLDDLogValue; - -/** - * This class provides a logger for the Apple System Log facility. - * - * As described in the "Getting Started" page, - * the traditional NSLog() function directs its output to two places: - * - * - Apple System Log - * - StdErr (if stderr is a TTY) so log statements show up in Xcode console - * - * To duplicate NSLog() functionality you can simply add this logger and a tty logger. - * However, if you instead choose to use file logging (for faster performance), - * you may choose to use a file logger and a tty logger. - **/ -@interface DDASLLogger : DDAbstractLogger - -/** - * Singleton method - * - * @return the shared instance - */ -+ (instancetype)sharedInstance; - -// Inherited from DDAbstractLogger - -// - (id )logFormatter; -// - (void)setLogFormatter:(id )formatter; - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/DDASLLogger.m b/Example/Pods/CocoaLumberjack/Classes/DDASLLogger.m deleted file mode 100644 index 90061c8..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDASLLogger.m +++ /dev/null @@ -1,121 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import "DDASLLogger.h" -#import - -#if !__has_feature(objc_arc) -#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). -#endif - -const char* const kDDASLKeyDDLog = "DDLog"; - -const char* const kDDASLDDLogValue = "1"; - -static DDASLLogger *sharedInstance; - -@interface DDASLLogger () { - aslclient _client; -} - -@end - - -@implementation DDASLLogger - -+ (instancetype)sharedInstance { - static dispatch_once_t DDASLLoggerOnceToken; - - dispatch_once(&DDASLLoggerOnceToken, ^{ - sharedInstance = [[[self class] alloc] init]; - }); - - return sharedInstance; -} - -- (instancetype)init { - if (sharedInstance != nil) { - return nil; - } - - if ((self = [super init])) { - // A default asl client is provided for the main thread, - // but background threads need to create their own client. - - _client = asl_open(NULL, "com.apple.console", 0); - } - - return self; -} - -- (void)logMessage:(DDLogMessage *)logMessage { - // Skip captured log messages - if ([logMessage->_fileName isEqualToString:@"DDASLLogCapture"]) { - return; - } - - NSString * message = _logFormatter ? [_logFormatter formatLogMessage:logMessage] : logMessage->_message; - - if (logMessage) { - const char *msg = [message UTF8String]; - - size_t aslLogLevel; - switch (logMessage->_flag) { - // Note: By default ASL will filter anything above level 5 (Notice). - // So our mappings shouldn't go above that level. - case DDLogFlagError : aslLogLevel = ASL_LEVEL_CRIT; break; - case DDLogFlagWarning : aslLogLevel = ASL_LEVEL_ERR; break; - case DDLogFlagInfo : aslLogLevel = ASL_LEVEL_WARNING; break; // Regular NSLog's level - case DDLogFlagDebug : - case DDLogFlagVerbose : - default : aslLogLevel = ASL_LEVEL_NOTICE; break; - } - - static char const *const level_strings[] = { "0", "1", "2", "3", "4", "5", "6", "7" }; - - // NSLog uses the current euid to set the ASL_KEY_READ_UID. - uid_t const readUID = geteuid(); - - char readUIDString[16]; -#ifndef NS_BLOCK_ASSERTIONS - int l = snprintf(readUIDString, sizeof(readUIDString), "%d", readUID); -#else - snprintf(readUIDString, sizeof(readUIDString), "%d", readUID); -#endif - - NSAssert(l < sizeof(readUIDString), - @"Formatted euid is too long."); - NSAssert(aslLogLevel < (sizeof(level_strings) / sizeof(level_strings[0])), - @"Unhandled ASL log level."); - - aslmsg m = asl_new(ASL_TYPE_MSG); - if (m != NULL) { - if (asl_set(m, ASL_KEY_LEVEL, level_strings[aslLogLevel]) == 0 && - asl_set(m, ASL_KEY_MSG, msg) == 0 && - asl_set(m, ASL_KEY_READ_UID, readUIDString) == 0 && - asl_set(m, kDDASLKeyDDLog, kDDASLDDLogValue) == 0) { - asl_send(_client, m); - } - asl_free(m); - } - //TODO handle asl_* failures non-silently? - } -} - -- (NSString *)loggerName { - return @"cocoa.lumberjack.aslLogger"; -} - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.m b/Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.m deleted file mode 100644 index c8782de..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.m +++ /dev/null @@ -1,660 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import "DDAbstractDatabaseLogger.h" -#import - - -#if !__has_feature(objc_arc) -#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). -#endif - -@interface DDAbstractDatabaseLogger () - -- (void)destroySaveTimer; -- (void)destroyDeleteTimer; - -@end - -#pragma mark - - -@implementation DDAbstractDatabaseLogger - -- (instancetype)init { - if ((self = [super init])) { - _saveThreshold = 500; - _saveInterval = 60; // 60 seconds - _maxAge = (60 * 60 * 24 * 7); // 7 days - _deleteInterval = (60 * 5); // 5 minutes - } - - return self; -} - -- (void)dealloc { - [self destroySaveTimer]; - [self destroyDeleteTimer]; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Override Me -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -- (BOOL)db_log:(DDLogMessage *)logMessage { - // Override me and add your implementation. - // - // Return YES if an item was added to the buffer. - // Return NO if the logMessage was ignored. - - return NO; -} - -- (void)db_save { - // Override me and add your implementation. -} - -- (void)db_delete { - // Override me and add your implementation. -} - -- (void)db_saveAndDelete { - // Override me and add your implementation. -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Private API -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -- (void)performSaveAndSuspendSaveTimer { - if (_unsavedCount > 0) { - if (_deleteOnEverySave) { - [self db_saveAndDelete]; - } else { - [self db_save]; - } - } - - _unsavedCount = 0; - _unsavedTime = 0; - - if (_saveTimer && !_saveTimerSuspended) { - dispatch_suspend(_saveTimer); - _saveTimerSuspended = YES; - } -} - -- (void)performDelete { - if (_maxAge > 0.0) { - [self db_delete]; - - _lastDeleteTime = dispatch_time(DISPATCH_TIME_NOW, 0); - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Timers -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -- (void)destroySaveTimer { - if (_saveTimer) { - dispatch_source_cancel(_saveTimer); - - if (_saveTimerSuspended) { - // Must resume a timer before releasing it (or it will crash) - dispatch_resume(_saveTimer); - _saveTimerSuspended = NO; - } - - #if !OS_OBJECT_USE_OBJC - dispatch_release(_saveTimer); - #endif - _saveTimer = NULL; - } -} - -- (void)updateAndResumeSaveTimer { - if ((_saveTimer != NULL) && (_saveInterval > 0.0) && (_unsavedTime > 0.0)) { - uint64_t interval = (uint64_t)(_saveInterval * NSEC_PER_SEC); - dispatch_time_t startTime = dispatch_time(_unsavedTime, interval); - - dispatch_source_set_timer(_saveTimer, startTime, interval, 1.0); - - if (_saveTimerSuspended) { - dispatch_resume(_saveTimer); - _saveTimerSuspended = NO; - } - } -} - -- (void)createSuspendedSaveTimer { - if ((_saveTimer == NULL) && (_saveInterval > 0.0)) { - _saveTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.loggerQueue); - - dispatch_source_set_event_handler(_saveTimer, ^{ @autoreleasepool { - [self performSaveAndSuspendSaveTimer]; - } }); - - _saveTimerSuspended = YES; - } -} - -- (void)destroyDeleteTimer { - if (_deleteTimer) { - dispatch_source_cancel(_deleteTimer); - #if !OS_OBJECT_USE_OBJC - dispatch_release(_deleteTimer); - #endif - _deleteTimer = NULL; - } -} - -- (void)updateDeleteTimer { - if ((_deleteTimer != NULL) && (_deleteInterval > 0.0) && (_maxAge > 0.0)) { - uint64_t interval = (uint64_t)(_deleteInterval * NSEC_PER_SEC); - dispatch_time_t startTime; - - if (_lastDeleteTime > 0) { - startTime = dispatch_time(_lastDeleteTime, interval); - } else { - startTime = dispatch_time(DISPATCH_TIME_NOW, interval); - } - - dispatch_source_set_timer(_deleteTimer, startTime, interval, 1.0); - } -} - -- (void)createAndStartDeleteTimer { - if ((_deleteTimer == NULL) && (_deleteInterval > 0.0) && (_maxAge > 0.0)) { - _deleteTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.loggerQueue); - - if (_deleteTimer != NULL) { - dispatch_source_set_event_handler(_deleteTimer, ^{ @autoreleasepool { - [self performDelete]; - } }); - - [self updateDeleteTimer]; - - if (_deleteTimer != NULL) { - dispatch_resume(_deleteTimer); - } - } - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Configuration -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -- (NSUInteger)saveThreshold { - // The design of this method is taken from the DDAbstractLogger implementation. - // For extensive documentation please refer to the DDAbstractLogger implementation. - - // Note: The internal implementation MUST access the colorsEnabled variable directly, - // This method is designed explicitly for external access. - // - // Using "self." syntax to go through this method will cause immediate deadlock. - // This is the intended result. Fix it by accessing the ivar directly. - // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. - - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); - - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - - __block NSUInteger result; - - dispatch_sync(globalLoggingQueue, ^{ - dispatch_sync(self.loggerQueue, ^{ - result = _saveThreshold; - }); - }); - - return result; -} - -- (void)setSaveThreshold:(NSUInteger)threshold { - dispatch_block_t block = ^{ - @autoreleasepool { - if (_saveThreshold != threshold) { - _saveThreshold = threshold; - - // Since the saveThreshold has changed, - // we check to see if the current unsavedCount has surpassed the new threshold. - // - // If it has, we immediately save the log. - - if ((_unsavedCount >= _saveThreshold) && (_saveThreshold > 0)) { - [self performSaveAndSuspendSaveTimer]; - } - } - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (NSTimeInterval)saveInterval { - // The design of this method is taken from the DDAbstractLogger implementation. - // For extensive documentation please refer to the DDAbstractLogger implementation. - - // Note: The internal implementation MUST access the colorsEnabled variable directly, - // This method is designed explicitly for external access. - // - // Using "self." syntax to go through this method will cause immediate deadlock. - // This is the intended result. Fix it by accessing the ivar directly. - // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. - - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); - - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - - __block NSTimeInterval result; - - dispatch_sync(globalLoggingQueue, ^{ - dispatch_sync(self.loggerQueue, ^{ - result = _saveInterval; - }); - }); - - return result; -} - -- (void)setSaveInterval:(NSTimeInterval)interval { - dispatch_block_t block = ^{ - @autoreleasepool { - // C99 recommended floating point comparison macro - // Read: isLessThanOrGreaterThan(floatA, floatB) - - if (/* saveInterval != interval */ islessgreater(_saveInterval, interval)) { - _saveInterval = interval; - - // There are several cases we need to handle here. - // - // 1. If the saveInterval was previously enabled and it just got disabled, - // then we need to stop the saveTimer. (And we might as well release it.) - // - // 2. If the saveInterval was previously disabled and it just got enabled, - // then we need to setup the saveTimer. (Plus we might need to do an immediate save.) - // - // 3. If the saveInterval increased, then we need to reset the timer so that it fires at the later date. - // - // 4. If the saveInterval decreased, then we need to reset the timer so that it fires at an earlier date. - // (Plus we might need to do an immediate save.) - - if (_saveInterval > 0.0) { - if (_saveTimer == NULL) { - // Handles #2 - // - // Since the saveTimer uses the unsavedTime to calculate it's first fireDate, - // if a save is needed the timer will fire immediately. - - [self createSuspendedSaveTimer]; - [self updateAndResumeSaveTimer]; - } else { - // Handles #3 - // Handles #4 - // - // Since the saveTimer uses the unsavedTime to calculate it's first fireDate, - // if a save is needed the timer will fire immediately. - - [self updateAndResumeSaveTimer]; - } - } else if (_saveTimer) { - // Handles #1 - - [self destroySaveTimer]; - } - } - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (NSTimeInterval)maxAge { - // The design of this method is taken from the DDAbstractLogger implementation. - // For extensive documentation please refer to the DDAbstractLogger implementation. - - // Note: The internal implementation MUST access the colorsEnabled variable directly, - // This method is designed explicitly for external access. - // - // Using "self." syntax to go through this method will cause immediate deadlock. - // This is the intended result. Fix it by accessing the ivar directly. - // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. - - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); - - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - - __block NSTimeInterval result; - - dispatch_sync(globalLoggingQueue, ^{ - dispatch_sync(self.loggerQueue, ^{ - result = _maxAge; - }); - }); - - return result; -} - -- (void)setMaxAge:(NSTimeInterval)interval { - dispatch_block_t block = ^{ - @autoreleasepool { - // C99 recommended floating point comparison macro - // Read: isLessThanOrGreaterThan(floatA, floatB) - - if (/* maxAge != interval */ islessgreater(_maxAge, interval)) { - NSTimeInterval oldMaxAge = _maxAge; - NSTimeInterval newMaxAge = interval; - - _maxAge = interval; - - // There are several cases we need to handle here. - // - // 1. If the maxAge was previously enabled and it just got disabled, - // then we need to stop the deleteTimer. (And we might as well release it.) - // - // 2. If the maxAge was previously disabled and it just got enabled, - // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) - // - // 3. If the maxAge was increased, - // then we don't need to do anything. - // - // 4. If the maxAge was decreased, - // then we should do an immediate delete. - - BOOL shouldDeleteNow = NO; - - if (oldMaxAge > 0.0) { - if (newMaxAge <= 0.0) { - // Handles #1 - - [self destroyDeleteTimer]; - } else if (oldMaxAge > newMaxAge) { - // Handles #4 - shouldDeleteNow = YES; - } - } else if (newMaxAge > 0.0) { - // Handles #2 - shouldDeleteNow = YES; - } - - if (shouldDeleteNow) { - [self performDelete]; - - if (_deleteTimer) { - [self updateDeleteTimer]; - } else { - [self createAndStartDeleteTimer]; - } - } - } - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (NSTimeInterval)deleteInterval { - // The design of this method is taken from the DDAbstractLogger implementation. - // For extensive documentation please refer to the DDAbstractLogger implementation. - - // Note: The internal implementation MUST access the colorsEnabled variable directly, - // This method is designed explicitly for external access. - // - // Using "self." syntax to go through this method will cause immediate deadlock. - // This is the intended result. Fix it by accessing the ivar directly. - // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. - - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); - - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - - __block NSTimeInterval result; - - dispatch_sync(globalLoggingQueue, ^{ - dispatch_sync(self.loggerQueue, ^{ - result = _deleteInterval; - }); - }); - - return result; -} - -- (void)setDeleteInterval:(NSTimeInterval)interval { - dispatch_block_t block = ^{ - @autoreleasepool { - // C99 recommended floating point comparison macro - // Read: isLessThanOrGreaterThan(floatA, floatB) - - if (/* deleteInterval != interval */ islessgreater(_deleteInterval, interval)) { - _deleteInterval = interval; - - // There are several cases we need to handle here. - // - // 1. If the deleteInterval was previously enabled and it just got disabled, - // then we need to stop the deleteTimer. (And we might as well release it.) - // - // 2. If the deleteInterval was previously disabled and it just got enabled, - // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) - // - // 3. If the deleteInterval increased, then we need to reset the timer so that it fires at the later date. - // - // 4. If the deleteInterval decreased, then we need to reset the timer so that it fires at an earlier date. - // (Plus we might need to do an immediate delete.) - - if (_deleteInterval > 0.0) { - if (_deleteTimer == NULL) { - // Handles #2 - // - // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, - // if a delete is needed the timer will fire immediately. - - [self createAndStartDeleteTimer]; - } else { - // Handles #3 - // Handles #4 - // - // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, - // if a save is needed the timer will fire immediately. - - [self updateDeleteTimer]; - } - } else if (_deleteTimer) { - // Handles #1 - - [self destroyDeleteTimer]; - } - } - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (BOOL)deleteOnEverySave { - // The design of this method is taken from the DDAbstractLogger implementation. - // For extensive documentation please refer to the DDAbstractLogger implementation. - - // Note: The internal implementation MUST access the colorsEnabled variable directly, - // This method is designed explicitly for external access. - // - // Using "self." syntax to go through this method will cause immediate deadlock. - // This is the intended result. Fix it by accessing the ivar directly. - // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. - - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); - - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - - __block BOOL result; - - dispatch_sync(globalLoggingQueue, ^{ - dispatch_sync(self.loggerQueue, ^{ - result = _deleteOnEverySave; - }); - }); - - return result; -} - -- (void)setDeleteOnEverySave:(BOOL)flag { - dispatch_block_t block = ^{ - _deleteOnEverySave = flag; - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Public API -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -- (void)savePendingLogEntries { - dispatch_block_t block = ^{ - @autoreleasepool { - [self performSaveAndSuspendSaveTimer]; - } - }; - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_async(self.loggerQueue, block); - } -} - -- (void)deleteOldLogEntries { - dispatch_block_t block = ^{ - @autoreleasepool { - [self performDelete]; - } - }; - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_async(self.loggerQueue, block); - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark DDLogger -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -- (void)didAddLogger { - // If you override me be sure to invoke [super didAddLogger]; - - [self createSuspendedSaveTimer]; - - [self createAndStartDeleteTimer]; -} - -- (void)willRemoveLogger { - // If you override me be sure to invoke [super willRemoveLogger]; - - [self performSaveAndSuspendSaveTimer]; - - [self destroySaveTimer]; - [self destroyDeleteTimer]; -} - -- (void)logMessage:(DDLogMessage *)logMessage { - if ([self db_log:logMessage]) { - BOOL firstUnsavedEntry = (++_unsavedCount == 1); - - if ((_unsavedCount >= _saveThreshold) && (_saveThreshold > 0)) { - [self performSaveAndSuspendSaveTimer]; - } else if (firstUnsavedEntry) { - _unsavedTime = dispatch_time(DISPATCH_TIME_NOW, 0); - [self updateAndResumeSaveTimer]; - } - } -} - -- (void)flush { - // This method is invoked by DDLog's flushLog method. - // - // It is called automatically when the application quits, - // or if the developer invokes DDLog's flushLog method prior to crashing or something. - - [self performSaveAndSuspendSaveTimer]; -} - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/DDAssertMacros.h b/Example/Pods/CocoaLumberjack/Classes/DDAssertMacros.h deleted file mode 100644 index 870d31f..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDAssertMacros.h +++ /dev/null @@ -1,26 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -/** - * NSAsset replacement that will output a log message even when assertions are disabled. - **/ -#define DDAssert(condition, frmt, ...) \ - if (!(condition)) { \ - NSString *description = [NSString stringWithFormat:frmt, ## __VA_ARGS__]; \ - DDLogError(@"%@", description); \ - NSAssert(NO, description); \ - } -#define DDAssertCondition(condition) DDAssert(condition, @"Condition not satisfied: %s", #condition) - diff --git a/Example/Pods/CocoaLumberjack/Classes/DDLegacyMacros.h b/Example/Pods/CocoaLumberjack/Classes/DDLegacyMacros.h deleted file mode 100644 index e0671b9..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDLegacyMacros.h +++ /dev/null @@ -1,75 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -/** - * Legacy macros used for 1.9.x backwards compatibility. - * - * Imported by default when importing a DDLog.h directly and DD_LEGACY_MACROS is not defined and set to 0. - **/ -#if DD_LEGACY_MACROS - -#warning CocoaLumberjack 1.9.x legacy macros enabled. \ -Disable legacy macros by importing CocoaLumberjack.h or DDLogMacros.h instead of DDLog.h or add `#define DD_LEGACY_MACROS 0` before importing DDLog.h. - -#ifndef LOG_LEVEL_DEF - #define LOG_LEVEL_DEF ddLogLevel -#endif - -#define LOG_FLAG_ERROR DDLogFlagError -#define LOG_FLAG_WARN DDLogFlagWarning -#define LOG_FLAG_INFO DDLogFlagInfo -#define LOG_FLAG_DEBUG DDLogFlagDebug -#define LOG_FLAG_VERBOSE DDLogFlagVerbose - -#define LOG_LEVEL_OFF DDLogLevelOff -#define LOG_LEVEL_ERROR DDLogLevelError -#define LOG_LEVEL_WARN DDLogLevelWarning -#define LOG_LEVEL_INFO DDLogLevelInfo -#define LOG_LEVEL_DEBUG DDLogLevelDebug -#define LOG_LEVEL_VERBOSE DDLogLevelVerbose -#define LOG_LEVEL_ALL DDLogLevelAll - -#define LOG_ASYNC_ENABLED YES - -#define LOG_ASYNC_ERROR ( NO && LOG_ASYNC_ENABLED) -#define LOG_ASYNC_WARN (YES && LOG_ASYNC_ENABLED) -#define LOG_ASYNC_INFO (YES && LOG_ASYNC_ENABLED) -#define LOG_ASYNC_DEBUG (YES && LOG_ASYNC_ENABLED) -#define LOG_ASYNC_VERBOSE (YES && LOG_ASYNC_ENABLED) - -#define LOG_MACRO(isAsynchronous, lvl, flg, ctx, atag, fnct, frmt, ...) \ - [DDLog log : isAsynchronous \ - level : lvl \ - flag : flg \ - context : ctx \ - file : __FILE__ \ - function : fnct \ - line : __LINE__ \ - tag : atag \ - format : (frmt), ## __VA_ARGS__] - -#define LOG_MAYBE(async, lvl, flg, ctx, fnct, frmt, ...) \ - do { if(lvl & flg) LOG_MACRO(async, lvl, flg, ctx, nil, fnct, frmt, ##__VA_ARGS__); } while(0) - -#define LOG_OBJC_MAYBE(async, lvl, flg, ctx, frmt, ...) \ - LOG_MAYBE(async, lvl, flg, ctx, __PRETTY_FUNCTION__, frmt, ## __VA_ARGS__) - -#define DDLogError(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_ERROR, LOG_LEVEL_DEF, LOG_FLAG_ERROR, 0, frmt, ##__VA_ARGS__) -#define DDLogWarn(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_WARN, LOG_LEVEL_DEF, LOG_FLAG_WARN, 0, frmt, ##__VA_ARGS__) -#define DDLogInfo(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_INFO, LOG_LEVEL_DEF, LOG_FLAG_INFO, 0, frmt, ##__VA_ARGS__) -#define DDLogDebug(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_DEBUG, LOG_LEVEL_DEF, LOG_FLAG_DEBUG, 0, frmt, ##__VA_ARGS__) -#define DDLogVerbose(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_VERBOSE, LOG_LEVEL_DEF, LOG_FLAG_VERBOSE, 0, frmt, ##__VA_ARGS__) - -#endif diff --git a/Example/Pods/CocoaLumberjack/Classes/DDLog+LOGV.h b/Example/Pods/CocoaLumberjack/Classes/DDLog+LOGV.h deleted file mode 100644 index cf4bfc3..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDLog+LOGV.h +++ /dev/null @@ -1,83 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif - -#import "DDLog.h" - -/** - * The constant/variable/method responsible for controlling the current log level. - **/ -#ifndef LOG_LEVEL_DEF - #define LOG_LEVEL_DEF ddLogLevel -#endif - -/** - * Whether async should be used by log messages, excluding error messages that are always sent sync. - **/ -#ifndef LOG_ASYNC_ENABLED - #define LOG_ASYNC_ENABLED YES -#endif - -/** - * This is the single macro that all other macros below compile into. - * This big multiline macro makes all the other macros easier to read. - **/ -#define LOGV_MACRO(isAsynchronous, lvl, flg, ctx, atag, fnct, frmt, avalist) \ - [DDLog log : isAsynchronous \ - level : lvl \ - flag : flg \ - context : ctx \ - file : __FILE__ \ - function : fnct \ - line : __LINE__ \ - tag : atag \ - format : frmt \ - args : avalist] - -/** - * Define version of the macro that only execute if the log level is above the threshold. - * The compiled versions essentially look like this: - * - * if (logFlagForThisLogMsg & ddLogLevel) { execute log message } - * - * When LOG_LEVEL_DEF is defined as ddLogLevel. - * - * As shown further below, Lumberjack actually uses a bitmask as opposed to primitive log levels. - * This allows for a great amount of flexibility and some pretty advanced fine grained logging techniques. - * - * Note that when compiler optimizations are enabled (as they are for your release builds), - * the log messages above your logging threshold will automatically be compiled out. - * - * (If the compiler sees LOG_LEVEL_DEF/ddLogLevel declared as a constant, the compiler simply checks to see - * if the 'if' statement would execute, and if not it strips it from the binary.) - * - * We also define shorthand versions for asynchronous and synchronous logging. - **/ -#define LOGV_MAYBE(async, lvl, flg, ctx, tag, fnct, frmt, avalist) \ - do { if(lvl & flg) LOGV_MACRO(async, lvl, flg, ctx, tag, fnct, frmt, avalist); } while(0) - -/** - * Ready to use log macros with no context or tag. - **/ -#define DDLogVError(frmt, avalist) LOGV_MAYBE(NO, LOG_LEVEL_DEF, DDLogFlagError, 0, nil, __PRETTY_FUNCTION__, frmt, avalist) -#define DDLogVWarn(frmt, avalist) LOGV_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagWarning, 0, nil, __PRETTY_FUNCTION__, frmt, avalist) -#define DDLogVInfo(frmt, avalist) LOGV_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagInfo, 0, nil, __PRETTY_FUNCTION__, frmt, avalist) -#define DDLogVDebug(frmt, avalist) LOGV_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagDebug, 0, nil, __PRETTY_FUNCTION__, frmt, avalist) -#define DDLogVVerbose(frmt, avalist) LOGV_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagVerbose, 0, nil, __PRETTY_FUNCTION__, frmt, avalist) - diff --git a/Example/Pods/CocoaLumberjack/Classes/DDLog.h b/Example/Pods/CocoaLumberjack/Classes/DDLog.h deleted file mode 100644 index b7f1074..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDLog.h +++ /dev/null @@ -1,743 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import - -// Enable 1.9.x legacy macros if imported directly -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 1 -#endif -// DD_LEGACY_MACROS is checked in the file itself -#import "DDLegacyMacros.h" - -#if OS_OBJECT_USE_OBJC - #define DISPATCH_QUEUE_REFERENCE_TYPE strong -#else - #define DISPATCH_QUEUE_REFERENCE_TYPE assign -#endif - -@class DDLogMessage; -@protocol DDLogger; -@protocol DDLogFormatter; - -/** - * Define the standard options. - * - * We default to only 4 levels because it makes it easier for beginners - * to make the transition to a logging framework. - * - * More advanced users may choose to completely customize the levels (and level names) to suite their needs. - * For more information on this see the "Custom Log Levels" page: - * Documentation/CustomLogLevels.md - * - * Advanced users may also notice that we're using a bitmask. - * This is to allow for custom fine grained logging: - * Documentation/FineGrainedLogging.md - * - * -- Flags -- - * - * Typically you will use the LOG_LEVELS (see below), but the flags may be used directly in certain situations. - * For example, say you have a lot of warning log messages, and you wanted to disable them. - * However, you still needed to see your error and info log messages. - * You could accomplish that with the following: - * - * static const DDLogLevel ddLogLevel = DDLogFlagError | DDLogFlagInfo; - * - * When LOG_LEVEL_DEF is defined as ddLogLevel. - * - * Flags may also be consulted when writing custom log formatters, - * as the DDLogMessage class captures the individual flag that caused the log message to fire. - * - * -- Levels -- - * - * Log levels are simply the proper bitmask of the flags. - * - * -- Booleans -- - * - * The booleans may be used when your logging code involves more than one line. - * For example: - * - * if (LOG_VERBOSE) { - * for (id sprocket in sprockets) - * DDLogVerbose(@"sprocket: %@", [sprocket description]) - * } - * - * -- Async -- - * - * Defines the default asynchronous options. - * The default philosophy for asynchronous logging is very simple: - * - * Log messages with errors should be executed synchronously. - * After all, an error just occurred. The application could be unstable. - * - * All other log messages, such as debug output, are executed asynchronously. - * After all, if it wasn't an error, then it was just informational output, - * or something the application was easily able to recover from. - * - * -- Changes -- - * - * You are strongly discouraged from modifying this file. - * If you do, you make it more difficult on yourself to merge future bug fixes and improvements from the project. - * Instead, create your own MyLogging.h or ApplicationNameLogging.h or CompanyLogging.h - * - * For an example of customizing your logging experience, see the "Custom Log Levels" page: - * Documentation/CustomLogLevels.md - **/ - -/** - * Flags accompany each log. They are used together with levels to filter out logs. - */ -typedef NS_OPTIONS(NSUInteger, DDLogFlag){ - /** - * 0...00000 DDLogFlagError - */ - DDLogFlagError = (1 << 0), - - /** - * 0...00001 DDLogFlagWarning - */ - DDLogFlagWarning = (1 << 1), - - /** - * 0...00010 DDLogFlagInfo - */ - DDLogFlagInfo = (1 << 2), - - /** - * 0...00100 DDLogFlagDebug - */ - DDLogFlagDebug = (1 << 3), - - /** - * 0...01000 DDLogFlagVerbose - */ - DDLogFlagVerbose = (1 << 4) -}; - -/** - * Log levels are used to filter out logs. Used together with flags. - */ -typedef NS_ENUM(NSUInteger, DDLogLevel){ - /** - * No logs - */ - DDLogLevelOff = 0, - - /** - * Error logs only - */ - DDLogLevelError = (DDLogFlagError), - - /** - * Error and warning logs - */ - DDLogLevelWarning = (DDLogLevelError | DDLogFlagWarning), - - /** - * Error, warning and info logs - */ - DDLogLevelInfo = (DDLogLevelWarning | DDLogFlagInfo), - - /** - * Error, warning, info and debug logs - */ - DDLogLevelDebug = (DDLogLevelInfo | DDLogFlagDebug), - - /** - * Error, warning, info, debug and verbose logs - */ - DDLogLevelVerbose = (DDLogLevelDebug | DDLogFlagVerbose), - - /** - * All logs (1...11111) - */ - DDLogLevelAll = NSUIntegerMax -}; - -/** - * Extracts just the file name, no path or extension - * - * @param filePath input file path - * @param copy YES if we want the result to be copied - * - * @return the file name - */ -NSString * DDExtractFileNameWithoutExtension(const char *filePath, BOOL copy); - -/** - * The THIS_FILE macro gives you an NSString of the file name. - * For simplicity and clarity, the file name does not include the full path or file extension. - * - * For example: DDLogWarn(@"%@: Unable to find thingy", THIS_FILE) -> @"MyViewController: Unable to find thingy" - **/ -#define THIS_FILE (DDExtractFileNameWithoutExtension(__FILE__, NO)) - -/** - * The THIS_METHOD macro gives you the name of the current objective-c method. - * - * For example: DDLogWarn(@"%@ - Requires non-nil strings", THIS_METHOD) -> @"setMake:model: requires non-nil strings" - * - * Note: This does NOT work in straight C functions (non objective-c). - * Instead you should use the predefined __FUNCTION__ macro. - **/ -#define THIS_METHOD NSStringFromSelector(_cmd) - - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * The main class, exposes all logging mechanisms, loggers, ... - * For most of the users, this class is hidden behind the logging functions like `DDLogInfo` - */ -@interface DDLog : NSObject - -/** - * Provides access to the underlying logging queue. - * This may be helpful to Logger classes for things like thread synchronization. - **/ -+ (dispatch_queue_t)loggingQueue; - -/** - * Logging Primitive. - * - * This method is used by the macros or logging functions. - * It is suggested you stick with the macros as they're easier to use. - * - * @param asynchronous YES if the logging is done async, NO if you want to force sync - * @param level the log level - * @param flag the log flag - * @param context the context (if any is defined) - * @param file the current file - * @param function the current function - * @param line the current code line - * @param tag potential tag - * @param format the log format - */ -+ (void)log:(BOOL)asynchronous - level:(DDLogLevel)level - flag:(DDLogFlag)flag - context:(NSInteger)context - file:(const char *)file - function:(const char *)function - line:(NSUInteger)line - tag:(id)tag - format:(NSString *)format, ... NS_FORMAT_FUNCTION(9,10); - -/** - * Logging Primitive. - * - * This method can be used if you have a prepared va_list. - * Similar to `log:level:flag:context:file:function:line:tag:format:...` - * - * @param asynchronous YES if the logging is done async, NO if you want to force sync - * @param level the log level - * @param flag the log flag - * @param context the context (if any is defined) - * @param file the current file - * @param function the current function - * @param line the current code line - * @param tag potential tag - * @param format the log format - * @param argList the arguments list as a va_list - */ -+ (void)log:(BOOL)asynchronous - level:(DDLogLevel)level - flag:(DDLogFlag)flag - context:(NSInteger)context - file:(const char *)file - function:(const char *)function - line:(NSUInteger)line - tag:(id)tag - format:(NSString *)format - args:(va_list)argList; - -/** - * Logging Primitive. - * - * @param asynchronous YES if the logging is done async, NO if you want to force sync - * @param message the message - * @param level the log level - * @param flag the log flag - * @param context the context (if any is defined) - * @param file the current file - * @param function the current function - * @param line the current code line - * @param tag potential tag - */ -+ (void)log:(BOOL)asynchronous - message:(NSString *)message - level:(DDLogLevel)level - flag:(DDLogFlag)flag - context:(NSInteger)context - file:(const char *)file - function:(const char *)function - line:(NSUInteger)line - tag:(id)tag; - -/** - * Logging Primitive. - * - * This method can be used if you manualy prepared DDLogMessage. - * - * @param asynchronous YES if the logging is done async, NO if you want to force sync - * @param logMessage the log message stored in a `DDLogMessage` model object - */ -+ (void)log:(BOOL)asynchronous - message:(DDLogMessage *)logMessage; - -/** - * Since logging can be asynchronous, there may be times when you want to flush the logs. - * The framework invokes this automatically when the application quits. - **/ -+ (void)flushLog; - -/** - * Loggers - * - * In order for your log statements to go somewhere, you should create and add a logger. - * - * You can add multiple loggers in order to direct your log statements to multiple places. - * And each logger can be configured separately. - * So you could have, for example, verbose logging to the console, but a concise log file with only warnings & errors. - **/ - -/** - * Adds the logger to the system. - * - * This is equivalent to invoking `[DDLog addLogger:logger withLogLevel:DDLogLevelAll]`. - **/ -+ (void)addLogger:(id )logger; - -/** - * Adds the logger to the system. - * - * The level that you provide here is a preemptive filter (for performance). - * That is, the level specified here will be used to filter out logMessages so that - * the logger is never even invoked for the messages. - * - * More information: - * When you issue a log statement, the logging framework iterates over each logger, - * and checks to see if it should forward the logMessage to the logger. - * This check is done using the level parameter passed to this method. - * - * For example: - * - * `[DDLog addLogger:consoleLogger withLogLevel:DDLogLevelVerbose];` - * `[DDLog addLogger:fileLogger withLogLevel:DDLogLevelWarning];` - * - * `DDLogError(@"oh no");` => gets forwarded to consoleLogger & fileLogger - * `DDLogInfo(@"hi");` => gets forwarded to consoleLogger only - * - * It is important to remember that Lumberjack uses a BITMASK. - * Many developers & third party frameworks may define extra log levels & flags. - * For example: - * - * `#define SOME_FRAMEWORK_LOG_FLAG_TRACE (1 << 6) // 0...1000000` - * - * So if you specify `DDLogLevelVerbose` to this method, you won't see the framework's trace messages. - * - * `(SOME_FRAMEWORK_LOG_FLAG_TRACE & DDLogLevelVerbose) => (01000000 & 00011111) => NO` - * - * Consider passing `DDLogLevelAll` to this method, which has all bits set. - * You can also use the exclusive-or bitwise operator to get a bitmask that has all flags set, - * except the ones you explicitly don't want. For example, if you wanted everything except verbose & debug: - * - * `((DDLogLevelAll ^ DDLogLevelVerbose) | DDLogLevelInfo)` - **/ -+ (void)addLogger:(id )logger withLevel:(DDLogLevel)level; - -/** - * Remove the logger from the system - */ -+ (void)removeLogger:(id )logger; - -/** - * Remove all the current loggers - */ -+ (void)removeAllLoggers; - -/** - * Return all the current loggers - */ -+ (NSArray *)allLoggers; - -/** - * Registered Dynamic Logging - * - * These methods allow you to obtain a list of classes that are using registered dynamic logging, - * and also provides methods to get and set their log level during run time. - **/ - -/** - * Returns an array with the classes that are using registered dynamic logging - */ -+ (NSArray *)registeredClasses; - -/** - * Returns an array with the classes names that are using registered dynamic logging - */ -+ (NSArray *)registeredClassNames; - -/** - * Returns the current log level for a certain class - * - * @param aClass `Class` param - */ -+ (DDLogLevel)levelForClass:(Class)aClass; - -/** - * Returns the current log level for a certain class - * - * @param aClassName string param - */ -+ (DDLogLevel)levelForClassWithName:(NSString *)aClassName; - -/** - * Set the log level for a certain class - * - * @param level the new level - * @param aClass `Class` param - */ -+ (void)setLevel:(DDLogLevel)level forClass:(Class)aClass; - -/** - * Set the log level for a certain class - * - * @param level the new level - * @param aClassName string param - */ -+ (void)setLevel:(DDLogLevel)level forClassWithName:(NSString *)aClassName; - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * This protocol describes a basic logger behavior. - * Basically, it can log messages, store a logFormatter plus a bunch of optional behaviors. - * (i.e. flush, get its loggerQueue, get its name, ... - */ -@protocol DDLogger - -/** - * The log message method - * - * @param logMessage the message (model) - */ -- (void)logMessage:(DDLogMessage *)logMessage; - -/** - * Formatters may optionally be added to any logger. - * - * If no formatter is set, the logger simply logs the message as it is given in logMessage, - * or it may use its own built in formatting style. - **/ -@property (nonatomic, strong) id logFormatter; - -@optional - -/** - * Since logging is asynchronous, adding and removing loggers is also asynchronous. - * In other words, the loggers are added and removed at appropriate times with regards to log messages. - * - * - Loggers will not receive log messages that were executed prior to when they were added. - * - Loggers will not receive log messages that were executed after they were removed. - * - * These methods are executed in the logging thread/queue. - * This is the same thread/queue that will execute every logMessage: invocation. - * Loggers may use these methods for thread synchronization or other setup/teardown tasks. - **/ -- (void)didAddLogger; - -/** - * See the above description for `didAddLoger` - */ -- (void)willRemoveLogger; - -/** - * Some loggers may buffer IO for optimization purposes. - * For example, a database logger may only save occasionaly as the disk IO is slow. - * In such loggers, this method should be implemented to flush any pending IO. - * - * This allows invocations of DDLog's flushLog method to be propogated to loggers that need it. - * - * Note that DDLog's flushLog method is invoked automatically when the application quits, - * and it may be also invoked manually by the developer prior to application crashes, or other such reasons. - **/ -- (void)flush; - -/** - * Each logger is executed concurrently with respect to the other loggers. - * Thus, a dedicated dispatch queue is used for each logger. - * Logger implementations may optionally choose to provide their own dispatch queue. - **/ -@property (nonatomic, DISPATCH_QUEUE_REFERENCE_TYPE, readonly) dispatch_queue_t loggerQueue; - -/** - * If the logger implementation does not choose to provide its own queue, - * one will automatically be created for it. - * The created queue will receive its name from this method. - * This may be helpful for debugging or profiling reasons. - **/ -@property (nonatomic, readonly) NSString *loggerName; - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * This protocol describes the behavior of a log formatter - */ -@protocol DDLogFormatter -@required - -/** - * Formatters may optionally be added to any logger. - * This allows for increased flexibility in the logging environment. - * For example, log messages for log files may be formatted differently than log messages for the console. - * - * For more information about formatters, see the "Custom Formatters" page: - * Documentation/CustomFormatters.md - * - * The formatter may also optionally filter the log message by returning nil, - * in which case the logger will not log the message. - **/ -- (NSString *)formatLogMessage:(DDLogMessage *)logMessage; - -@optional - -/** - * A single formatter instance can be added to multiple loggers. - * These methods provides hooks to notify the formatter of when it's added/removed. - * - * This is primarily for thread-safety. - * If a formatter is explicitly not thread-safe, it may wish to throw an exception if added to multiple loggers. - * Or if a formatter has potentially thread-unsafe code (e.g. NSDateFormatter), - * it could possibly use these hooks to switch to thread-safe versions of the code. - **/ -- (void)didAddToLogger:(id )logger; - -/** - * See the above description for `didAddToLogger:` - */ -- (void)willRemoveFromLogger:(id )logger; - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * This protocol describes a dynamic logging component - */ -@protocol DDRegisteredDynamicLogging - -/** - * Implement these methods to allow a file's log level to be managed from a central location. - * - * This is useful if you'd like to be able to change log levels for various parts - * of your code from within the running application. - * - * Imagine pulling up the settings for your application, - * and being able to configure the logging level on a per file basis. - * - * The implementation can be very straight-forward: - * - * ``` - * + (int)ddLogLevel - * { - * return ddLogLevel; - * } - * - * + (void)ddSetLogLevel:(DDLogLevel)level - * { - * ddLogLevel = level; - * } - * ``` - **/ -+ (DDLogLevel)ddLogLevel; - -/** - * See the above description for `ddLogLevel` - */ -+ (void)ddSetLogLevel:(DDLogLevel)level; - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -#ifndef NS_DESIGNATED_INITIALIZER - #define NS_DESIGNATED_INITIALIZER -#endif - -/** - * Log message options, allow copying certain log elements - */ -typedef NS_OPTIONS(NSInteger, DDLogMessageOptions){ - /** - * Use this to use a copy of the file path - */ - DDLogMessageCopyFile = 1 << 0, - /** - * Use this to use a copy of the function name - */ - DDLogMessageCopyFunction = 1 << 1 -}; - -/** - * The `DDLogMessage` class encapsulates information about the log message. - * If you write custom loggers or formatters, you will be dealing with objects of this class. - **/ -@interface DDLogMessage : NSObject -{ - // Direct accessors to be used only for performance - @public - NSString *_message; - DDLogLevel _level; - DDLogFlag _flag; - NSInteger _context; - NSString *_file; - NSString *_fileName; - NSString *_function; - NSUInteger _line; - id _tag; - DDLogMessageOptions _options; - NSDate *_timestamp; - NSString *_threadID; - NSString *_threadName; - NSString *_queueLabel; -} - -/** - * Default `init` is not available - */ -- (instancetype)init NS_UNAVAILABLE; - -/** - * Standard init method for a log message object. - * Used by the logging primitives. (And the macros use the logging primitives.) - * - * If you find need to manually create logMessage objects, there is one thing you should be aware of: - * - * If no flags are passed, the method expects the file and function parameters to be string literals. - * That is, it expects the given strings to exist for the duration of the object's lifetime, - * and it expects the given strings to be immutable. - * In other words, it does not copy these strings, it simply points to them. - * This is due to the fact that __FILE__ and __FUNCTION__ are usually used to specify these parameters, - * so it makes sense to optimize and skip the unnecessary allocations. - * However, if you need them to be copied you may use the options parameter to specify this. - * - * @param message the message - * @param level the log level - * @param flag the log flag - * @param context the context (if any is defined) - * @param file the current file - * @param function the current function - * @param line the current code line - * @param tag potential tag - * @param options a bitmask which supports DDLogMessageCopyFile and DDLogMessageCopyFunction. - * @param timestamp the log timestamp - * - * @return a new instance of a log message model object - */ -- (instancetype)initWithMessage:(NSString *)message - level:(DDLogLevel)level - flag:(DDLogFlag)flag - context:(NSInteger)context - file:(NSString *)file - function:(NSString *)function - line:(NSUInteger)line - tag:(id)tag - options:(DDLogMessageOptions)options - timestamp:(NSDate *)timestamp NS_DESIGNATED_INITIALIZER; - -/** - * Read-only properties - **/ - -/** - * The log message - */ -@property (readonly, nonatomic) NSString *message; -@property (readonly, nonatomic) DDLogLevel level; -@property (readonly, nonatomic) DDLogFlag flag; -@property (readonly, nonatomic) NSInteger context; -@property (readonly, nonatomic) NSString *file; -@property (readonly, nonatomic) NSString *fileName; -@property (readonly, nonatomic) NSString *function; -@property (readonly, nonatomic) NSUInteger line; -@property (readonly, nonatomic) id tag; -@property (readonly, nonatomic) DDLogMessageOptions options; -@property (readonly, nonatomic) NSDate *timestamp; -@property (readonly, nonatomic) NSString *threadID; // ID as it appears in NSLog calculated from the machThreadID -@property (readonly, nonatomic) NSString *threadName; -@property (readonly, nonatomic) NSString *queueLabel; - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * The `DDLogger` protocol specifies that an optional formatter can be added to a logger. - * Most (but not all) loggers will want to support formatters. - * - * However, writting getters and setters in a thread safe manner, - * while still maintaining maximum speed for the logging process, is a difficult task. - * - * To do it right, the implementation of the getter/setter has strict requiremenets: - * - Must NOT require the `logMessage:` method to acquire a lock. - * - Must NOT require the `logMessage:` method to access an atomic property (also a lock of sorts). - * - * To simplify things, an abstract logger is provided that implements the getter and setter. - * - * Logger implementations may simply extend this class, - * and they can ACCESS THE FORMATTER VARIABLE DIRECTLY from within their `logMessage:` method! - **/ -@interface DDAbstractLogger : NSObject -{ - // Direct accessors to be used only for performance - @public - id _logFormatter; - dispatch_queue_t _loggerQueue; -} - -@property (nonatomic, strong) id logFormatter; -@property (nonatomic, DISPATCH_QUEUE_REFERENCE_TYPE) dispatch_queue_t loggerQueue; - -// For thread-safety assertions - -/** - * Return YES if the current logger uses a global queue for logging - */ -@property (nonatomic, readonly, getter=isOnGlobalLoggingQueue) BOOL onGlobalLoggingQueue; - -/** - * Return YES if the current logger uses the internal designated queue for logging - */ -@property (nonatomic, readonly, getter=isOnInternalLoggerQueue) BOOL onInternalLoggerQueue; - -@end - diff --git a/Example/Pods/CocoaLumberjack/Classes/DDLogMacros.h b/Example/Pods/CocoaLumberjack/Classes/DDLogMacros.h deleted file mode 100644 index 975d00a..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDLogMacros.h +++ /dev/null @@ -1,82 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif - -#import "DDLog.h" - -/** - * The constant/variable/method responsible for controlling the current log level. - **/ -#ifndef LOG_LEVEL_DEF - #define LOG_LEVEL_DEF ddLogLevel -#endif - -/** - * Whether async should be used by log messages, excluding error messages that are always sent sync. - **/ -#ifndef LOG_ASYNC_ENABLED - #define LOG_ASYNC_ENABLED YES -#endif - -/** - * This is the single macro that all other macros below compile into. - * This big multiline macro makes all the other macros easier to read. - **/ -#define LOG_MACRO(isAsynchronous, lvl, flg, ctx, atag, fnct, frmt, ...) \ - [DDLog log : isAsynchronous \ - level : lvl \ - flag : flg \ - context : ctx \ - file : __FILE__ \ - function : fnct \ - line : __LINE__ \ - tag : atag \ - format : (frmt), ## __VA_ARGS__] - -/** - * Define version of the macro that only execute if the log level is above the threshold. - * The compiled versions essentially look like this: - * - * if (logFlagForThisLogMsg & ddLogLevel) { execute log message } - * - * When LOG_LEVEL_DEF is defined as ddLogLevel. - * - * As shown further below, Lumberjack actually uses a bitmask as opposed to primitive log levels. - * This allows for a great amount of flexibility and some pretty advanced fine grained logging techniques. - * - * Note that when compiler optimizations are enabled (as they are for your release builds), - * the log messages above your logging threshold will automatically be compiled out. - * - * (If the compiler sees LOG_LEVEL_DEF/ddLogLevel declared as a constant, the compiler simply checks to see - * if the 'if' statement would execute, and if not it strips it from the binary.) - * - * We also define shorthand versions for asynchronous and synchronous logging. - **/ -#define LOG_MAYBE(async, lvl, flg, ctx, tag, fnct, frmt, ...) \ - do { if(lvl & flg) LOG_MACRO(async, lvl, flg, ctx, tag, fnct, frmt, ##__VA_ARGS__); } while(0) - -/** - * Ready to use log macros with no context or tag. - **/ -#define DDLogError(frmt, ...) LOG_MAYBE(NO, LOG_LEVEL_DEF, DDLogFlagError, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__) -#define DDLogWarn(frmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagWarning, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__) -#define DDLogInfo(frmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagInfo, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__) -#define DDLogDebug(frmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagDebug, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__) -#define DDLogVerbose(frmt, ...) LOG_MAYBE(LOG_ASYNC_ENABLED, LOG_LEVEL_DEF, DDLogFlagVerbose, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__) - diff --git a/Example/Pods/CocoaLumberjack/Classes/DDTTYLogger.m b/Example/Pods/CocoaLumberjack/Classes/DDTTYLogger.m deleted file mode 100644 index 41592ca..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/DDTTYLogger.m +++ /dev/null @@ -1,1481 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import "DDTTYLogger.h" - -#import -#import - -#if !__has_feature(objc_arc) -#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). -#endif - -// We probably shouldn't be using DDLog() statements within the DDLog implementation. -// But we still want to leave our log statements for any future debugging, -// and to allow other developers to trace the implementation (which is a great learning tool). -// -// So we use primitive logging macros around NSLog. -// We maintain the NS prefix on the macros to be explicit about the fact that we're using NSLog. - -#ifndef DD_NSLOG_LEVEL - #define DD_NSLOG_LEVEL 2 -#endif - -#define NSLogError(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 1) NSLog((frmt), ##__VA_ARGS__); } while(0) -#define NSLogWarn(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 2) NSLog((frmt), ##__VA_ARGS__); } while(0) -#define NSLogInfo(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 3) NSLog((frmt), ##__VA_ARGS__); } while(0) -#define NSLogDebug(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 4) NSLog((frmt), ##__VA_ARGS__); } while(0) -#define NSLogVerbose(frmt, ...) do{ if(DD_NSLOG_LEVEL >= 5) NSLog((frmt), ##__VA_ARGS__); } while(0) - -// Xcode does NOT natively support colors in the Xcode debugging console. -// You'll need to install the XcodeColors plugin to see colors in the Xcode console. -// https://github.com/robbiehanson/XcodeColors -// -// The following is documentation from the XcodeColors project: -// -// -// How to apply color formatting to your log statements: -// -// To set the foreground color: -// Insert the ESCAPE_SEQ into your string, followed by "fg124,12,255;" where r=124, g=12, b=255. -// -// To set the background color: -// Insert the ESCAPE_SEQ into your string, followed by "bg12,24,36;" where r=12, g=24, b=36. -// -// To reset the foreground color (to default value): -// Insert the ESCAPE_SEQ into your string, followed by "fg;" -// -// To reset the background color (to default value): -// Insert the ESCAPE_SEQ into your string, followed by "bg;" -// -// To reset the foreground and background color (to default values) in one operation: -// Insert the ESCAPE_SEQ into your string, followed by ";" - -#define XCODE_COLORS_ESCAPE_SEQ "\033[" - -#define XCODE_COLORS_RESET_FG XCODE_COLORS_ESCAPE_SEQ "fg;" // Clear any foreground color -#define XCODE_COLORS_RESET_BG XCODE_COLORS_ESCAPE_SEQ "bg;" // Clear any background color -#define XCODE_COLORS_RESET XCODE_COLORS_ESCAPE_SEQ ";" // Clear any foreground or background color - -// If running in a shell, not all RGB colors will be supported. -// In this case we automatically map to the closest available color. -// In order to provide this mapping, we have a hard-coded set of the standard RGB values available in the shell. -// However, not every shell is the same, and Apple likes to think different even when it comes to shell colors. -// -// Map to standard Terminal.app colors (1), or -// map to standard xterm colors (0). - -#define MAP_TO_TERMINAL_APP_COLORS 1 - - -@interface DDTTYLoggerColorProfile : NSObject { - @public - DDLogFlag mask; - NSInteger context; - - uint8_t fg_r; - uint8_t fg_g; - uint8_t fg_b; - - uint8_t bg_r; - uint8_t bg_g; - uint8_t bg_b; - - NSUInteger fgCodeIndex; - NSString *fgCodeRaw; - - NSUInteger bgCodeIndex; - NSString *bgCodeRaw; - - char fgCode[24]; - size_t fgCodeLen; - - char bgCode[24]; - size_t bgCodeLen; - - char resetCode[8]; - size_t resetCodeLen; -} - -- (instancetype)initWithForegroundColor:(DDColor *)fgColor backgroundColor:(DDColor *)bgColor flag:(DDLogFlag)mask context:(NSInteger)ctxt; - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -@interface DDTTYLogger () { - NSUInteger _calendarUnitFlags; - - NSString *_appName; - char *_app; - size_t _appLen; - - NSString *_processID; - char *_pid; - size_t _pidLen; - - BOOL _colorsEnabled; - NSMutableArray *_colorProfilesArray; - NSMutableDictionary *_colorProfilesDict; -} - -@end - - -@implementation DDTTYLogger - -static BOOL isaColorTTY; -static BOOL isaColor256TTY; -static BOOL isaXcodeColorTTY; - -static NSArray *codes_fg = nil; -static NSArray *codes_bg = nil; -static NSArray *colors = nil; - -static DDTTYLogger *sharedInstance; - -/** - * Initializes the colors array, as well as the codes_fg and codes_bg arrays, for 16 color mode. - * - * This method is used when the application is running from within a shell that only supports 16 color mode. - * This method is not invoked if the application is running within Xcode, or via normal UI app launch. - **/ -+ (void)initialize_colors_16 { - if (codes_fg || codes_bg || colors) { - return; - } - - NSMutableArray *m_codes_fg = [NSMutableArray arrayWithCapacity:16]; - NSMutableArray *m_codes_bg = [NSMutableArray arrayWithCapacity:16]; - NSMutableArray *m_colors = [NSMutableArray arrayWithCapacity:16]; - - // In a standard shell only 16 colors are supported. - // - // More information about ansi escape codes can be found online. - // http://en.wikipedia.org/wiki/ANSI_escape_code - - [m_codes_fg addObject:@"30m"]; // normal - black - [m_codes_fg addObject:@"31m"]; // normal - red - [m_codes_fg addObject:@"32m"]; // normal - green - [m_codes_fg addObject:@"33m"]; // normal - yellow - [m_codes_fg addObject:@"34m"]; // normal - blue - [m_codes_fg addObject:@"35m"]; // normal - magenta - [m_codes_fg addObject:@"36m"]; // normal - cyan - [m_codes_fg addObject:@"37m"]; // normal - gray - [m_codes_fg addObject:@"1;30m"]; // bright - darkgray - [m_codes_fg addObject:@"1;31m"]; // bright - red - [m_codes_fg addObject:@"1;32m"]; // bright - green - [m_codes_fg addObject:@"1;33m"]; // bright - yellow - [m_codes_fg addObject:@"1;34m"]; // bright - blue - [m_codes_fg addObject:@"1;35m"]; // bright - magenta - [m_codes_fg addObject:@"1;36m"]; // bright - cyan - [m_codes_fg addObject:@"1;37m"]; // bright - white - - [m_codes_bg addObject:@"40m"]; // normal - black - [m_codes_bg addObject:@"41m"]; // normal - red - [m_codes_bg addObject:@"42m"]; // normal - green - [m_codes_bg addObject:@"43m"]; // normal - yellow - [m_codes_bg addObject:@"44m"]; // normal - blue - [m_codes_bg addObject:@"45m"]; // normal - magenta - [m_codes_bg addObject:@"46m"]; // normal - cyan - [m_codes_bg addObject:@"47m"]; // normal - gray - [m_codes_bg addObject:@"1;40m"]; // bright - darkgray - [m_codes_bg addObject:@"1;41m"]; // bright - red - [m_codes_bg addObject:@"1;42m"]; // bright - green - [m_codes_bg addObject:@"1;43m"]; // bright - yellow - [m_codes_bg addObject:@"1;44m"]; // bright - blue - [m_codes_bg addObject:@"1;45m"]; // bright - magenta - [m_codes_bg addObject:@"1;46m"]; // bright - cyan - [m_codes_bg addObject:@"1;47m"]; // bright - white - -#if MAP_TO_TERMINAL_APP_COLORS - - // Standard Terminal.app colors: - // - // These are the default colors used by Apple's Terminal.app. - - [m_colors addObject:DDMakeColor( 0, 0, 0)]; // normal - black - [m_colors addObject:DDMakeColor(194, 54, 33)]; // normal - red - [m_colors addObject:DDMakeColor( 37, 188, 36)]; // normal - green - [m_colors addObject:DDMakeColor(173, 173, 39)]; // normal - yellow - [m_colors addObject:DDMakeColor( 73, 46, 225)]; // normal - blue - [m_colors addObject:DDMakeColor(211, 56, 211)]; // normal - magenta - [m_colors addObject:DDMakeColor( 51, 187, 200)]; // normal - cyan - [m_colors addObject:DDMakeColor(203, 204, 205)]; // normal - gray - [m_colors addObject:DDMakeColor(129, 131, 131)]; // bright - darkgray - [m_colors addObject:DDMakeColor(252, 57, 31)]; // bright - red - [m_colors addObject:DDMakeColor( 49, 231, 34)]; // bright - green - [m_colors addObject:DDMakeColor(234, 236, 35)]; // bright - yellow - [m_colors addObject:DDMakeColor( 88, 51, 255)]; // bright - blue - [m_colors addObject:DDMakeColor(249, 53, 248)]; // bright - magenta - [m_colors addObject:DDMakeColor( 20, 240, 240)]; // bright - cyan - [m_colors addObject:DDMakeColor(233, 235, 235)]; // bright - white - -#else /* if MAP_TO_TERMINAL_APP_COLORS */ - - // Standard xterm colors: - // - // These are the default colors used by most xterm shells. - - [m_colors addObject:DDMakeColor( 0, 0, 0)]; // normal - black - [m_colors addObject:DDMakeColor(205, 0, 0)]; // normal - red - [m_colors addObject:DDMakeColor( 0, 205, 0)]; // normal - green - [m_colors addObject:DDMakeColor(205, 205, 0)]; // normal - yellow - [m_colors addObject:DDMakeColor( 0, 0, 238)]; // normal - blue - [m_colors addObject:DDMakeColor(205, 0, 205)]; // normal - magenta - [m_colors addObject:DDMakeColor( 0, 205, 205)]; // normal - cyan - [m_colors addObject:DDMakeColor(229, 229, 229)]; // normal - gray - [m_colors addObject:DDMakeColor(127, 127, 127)]; // bright - darkgray - [m_colors addObject:DDMakeColor(255, 0, 0)]; // bright - red - [m_colors addObject:DDMakeColor( 0, 255, 0)]; // bright - green - [m_colors addObject:DDMakeColor(255, 255, 0)]; // bright - yellow - [m_colors addObject:DDMakeColor( 92, 92, 255)]; // bright - blue - [m_colors addObject:DDMakeColor(255, 0, 255)]; // bright - magenta - [m_colors addObject:DDMakeColor( 0, 255, 255)]; // bright - cyan - [m_colors addObject:DDMakeColor(255, 255, 255)]; // bright - white - -#endif /* if MAP_TO_TERMINAL_APP_COLORS */ - - codes_fg = [m_codes_fg copy]; - codes_bg = [m_codes_bg copy]; - colors = [m_colors copy]; - - NSAssert([codes_fg count] == [codes_bg count], @"Invalid colors/codes array(s)"); - NSAssert([codes_fg count] == [colors count], @"Invalid colors/codes array(s)"); -} - -/** - * Initializes the colors array, as well as the codes_fg and codes_bg arrays, for 256 color mode. - * - * This method is used when the application is running from within a shell that supports 256 color mode. - * This method is not invoked if the application is running within Xcode, or via normal UI app launch. - **/ -+ (void)initialize_colors_256 { - if (codes_fg || codes_bg || colors) { - return; - } - - NSMutableArray *m_codes_fg = [NSMutableArray arrayWithCapacity:(256 - 16)]; - NSMutableArray *m_codes_bg = [NSMutableArray arrayWithCapacity:(256 - 16)]; - NSMutableArray *m_colors = [NSMutableArray arrayWithCapacity:(256 - 16)]; - - #if MAP_TO_TERMINAL_APP_COLORS - - // Standard Terminal.app colors: - // - // These are the colors the Terminal.app uses in xterm-256color mode. - // In this mode, the terminal supports 256 different colors, specified by 256 color codes. - // - // The first 16 color codes map to the original 16 color codes supported by the earlier xterm-color mode. - // These are actually configurable, and thus we ignore them for the purposes of mapping, - // as we can't rely on them being constant. They are largely duplicated anyway. - // - // The next 216 color codes are designed to run the spectrum, with several shades of every color. - // While the color codes are standardized, the actual RGB values for each color code is not. - // Apple's Terminal.app uses different RGB values from that of a standard xterm. - // Apple's choices in colors are designed to be a little nicer on the eyes. - // - // The last 24 color codes represent a grayscale. - // - // Unfortunately, unlike the standard xterm color chart, - // Apple's RGB values cannot be calculated using a simple formula (at least not that I know of). - // Also, I don't know of any ways to programmatically query the shell for the RGB values. - // So this big giant color chart had to be made by hand. - // - // More information about ansi escape codes can be found online. - // http://en.wikipedia.org/wiki/ANSI_escape_code - - // Colors - - [m_colors addObject:DDMakeColor( 47, 49, 49)]; - [m_colors addObject:DDMakeColor( 60, 42, 144)]; - [m_colors addObject:DDMakeColor( 66, 44, 183)]; - [m_colors addObject:DDMakeColor( 73, 46, 222)]; - [m_colors addObject:DDMakeColor( 81, 50, 253)]; - [m_colors addObject:DDMakeColor( 88, 51, 255)]; - - [m_colors addObject:DDMakeColor( 42, 128, 37)]; - [m_colors addObject:DDMakeColor( 42, 127, 128)]; - [m_colors addObject:DDMakeColor( 44, 126, 169)]; - [m_colors addObject:DDMakeColor( 56, 125, 209)]; - [m_colors addObject:DDMakeColor( 59, 124, 245)]; - [m_colors addObject:DDMakeColor( 66, 123, 255)]; - - [m_colors addObject:DDMakeColor( 51, 163, 41)]; - [m_colors addObject:DDMakeColor( 39, 162, 121)]; - [m_colors addObject:DDMakeColor( 42, 161, 162)]; - [m_colors addObject:DDMakeColor( 53, 160, 202)]; - [m_colors addObject:DDMakeColor( 45, 159, 240)]; - [m_colors addObject:DDMakeColor( 58, 158, 255)]; - - [m_colors addObject:DDMakeColor( 31, 196, 37)]; - [m_colors addObject:DDMakeColor( 48, 196, 115)]; - [m_colors addObject:DDMakeColor( 39, 195, 155)]; - [m_colors addObject:DDMakeColor( 49, 195, 195)]; - [m_colors addObject:DDMakeColor( 32, 194, 235)]; - [m_colors addObject:DDMakeColor( 53, 193, 255)]; - - [m_colors addObject:DDMakeColor( 50, 229, 35)]; - [m_colors addObject:DDMakeColor( 40, 229, 109)]; - [m_colors addObject:DDMakeColor( 27, 229, 149)]; - [m_colors addObject:DDMakeColor( 49, 228, 189)]; - [m_colors addObject:DDMakeColor( 33, 228, 228)]; - [m_colors addObject:DDMakeColor( 53, 227, 255)]; - - [m_colors addObject:DDMakeColor( 27, 254, 30)]; - [m_colors addObject:DDMakeColor( 30, 254, 103)]; - [m_colors addObject:DDMakeColor( 45, 254, 143)]; - [m_colors addObject:DDMakeColor( 38, 253, 182)]; - [m_colors addObject:DDMakeColor( 38, 253, 222)]; - [m_colors addObject:DDMakeColor( 42, 253, 252)]; - - [m_colors addObject:DDMakeColor(140, 48, 40)]; - [m_colors addObject:DDMakeColor(136, 51, 136)]; - [m_colors addObject:DDMakeColor(135, 52, 177)]; - [m_colors addObject:DDMakeColor(134, 52, 217)]; - [m_colors addObject:DDMakeColor(135, 56, 248)]; - [m_colors addObject:DDMakeColor(134, 53, 255)]; - - [m_colors addObject:DDMakeColor(125, 125, 38)]; - [m_colors addObject:DDMakeColor(124, 125, 125)]; - [m_colors addObject:DDMakeColor(122, 124, 166)]; - [m_colors addObject:DDMakeColor(123, 124, 207)]; - [m_colors addObject:DDMakeColor(123, 122, 247)]; - [m_colors addObject:DDMakeColor(124, 121, 255)]; - - [m_colors addObject:DDMakeColor(119, 160, 35)]; - [m_colors addObject:DDMakeColor(117, 160, 120)]; - [m_colors addObject:DDMakeColor(117, 160, 160)]; - [m_colors addObject:DDMakeColor(115, 159, 201)]; - [m_colors addObject:DDMakeColor(116, 158, 240)]; - [m_colors addObject:DDMakeColor(117, 157, 255)]; - - [m_colors addObject:DDMakeColor(113, 195, 39)]; - [m_colors addObject:DDMakeColor(110, 194, 114)]; - [m_colors addObject:DDMakeColor(111, 194, 154)]; - [m_colors addObject:DDMakeColor(108, 194, 194)]; - [m_colors addObject:DDMakeColor(109, 193, 234)]; - [m_colors addObject:DDMakeColor(108, 192, 255)]; - - [m_colors addObject:DDMakeColor(105, 228, 30)]; - [m_colors addObject:DDMakeColor(103, 228, 109)]; - [m_colors addObject:DDMakeColor(105, 228, 148)]; - [m_colors addObject:DDMakeColor(100, 227, 188)]; - [m_colors addObject:DDMakeColor( 99, 227, 227)]; - [m_colors addObject:DDMakeColor( 99, 226, 253)]; - - [m_colors addObject:DDMakeColor( 92, 253, 34)]; - [m_colors addObject:DDMakeColor( 96, 253, 103)]; - [m_colors addObject:DDMakeColor( 97, 253, 142)]; - [m_colors addObject:DDMakeColor( 88, 253, 182)]; - [m_colors addObject:DDMakeColor( 93, 253, 221)]; - [m_colors addObject:DDMakeColor( 88, 254, 251)]; - - [m_colors addObject:DDMakeColor(177, 53, 34)]; - [m_colors addObject:DDMakeColor(174, 54, 131)]; - [m_colors addObject:DDMakeColor(172, 55, 172)]; - [m_colors addObject:DDMakeColor(171, 57, 213)]; - [m_colors addObject:DDMakeColor(170, 55, 249)]; - [m_colors addObject:DDMakeColor(170, 57, 255)]; - - [m_colors addObject:DDMakeColor(165, 123, 37)]; - [m_colors addObject:DDMakeColor(163, 123, 123)]; - [m_colors addObject:DDMakeColor(162, 123, 164)]; - [m_colors addObject:DDMakeColor(161, 122, 205)]; - [m_colors addObject:DDMakeColor(161, 121, 241)]; - [m_colors addObject:DDMakeColor(161, 121, 255)]; - - [m_colors addObject:DDMakeColor(158, 159, 33)]; - [m_colors addObject:DDMakeColor(157, 158, 118)]; - [m_colors addObject:DDMakeColor(157, 158, 159)]; - [m_colors addObject:DDMakeColor(155, 157, 199)]; - [m_colors addObject:DDMakeColor(155, 157, 239)]; - [m_colors addObject:DDMakeColor(154, 156, 255)]; - - [m_colors addObject:DDMakeColor(152, 193, 40)]; - [m_colors addObject:DDMakeColor(151, 193, 113)]; - [m_colors addObject:DDMakeColor(150, 193, 153)]; - [m_colors addObject:DDMakeColor(150, 192, 193)]; - [m_colors addObject:DDMakeColor(148, 192, 232)]; - [m_colors addObject:DDMakeColor(149, 191, 253)]; - - [m_colors addObject:DDMakeColor(146, 227, 28)]; - [m_colors addObject:DDMakeColor(144, 227, 108)]; - [m_colors addObject:DDMakeColor(144, 227, 147)]; - [m_colors addObject:DDMakeColor(144, 227, 187)]; - [m_colors addObject:DDMakeColor(142, 226, 227)]; - [m_colors addObject:DDMakeColor(142, 225, 252)]; - - [m_colors addObject:DDMakeColor(138, 253, 36)]; - [m_colors addObject:DDMakeColor(137, 253, 102)]; - [m_colors addObject:DDMakeColor(136, 253, 141)]; - [m_colors addObject:DDMakeColor(138, 254, 181)]; - [m_colors addObject:DDMakeColor(135, 255, 220)]; - [m_colors addObject:DDMakeColor(133, 255, 250)]; - - [m_colors addObject:DDMakeColor(214, 57, 30)]; - [m_colors addObject:DDMakeColor(211, 59, 126)]; - [m_colors addObject:DDMakeColor(209, 57, 168)]; - [m_colors addObject:DDMakeColor(208, 55, 208)]; - [m_colors addObject:DDMakeColor(207, 58, 247)]; - [m_colors addObject:DDMakeColor(206, 61, 255)]; - - [m_colors addObject:DDMakeColor(204, 121, 32)]; - [m_colors addObject:DDMakeColor(202, 121, 121)]; - [m_colors addObject:DDMakeColor(201, 121, 161)]; - [m_colors addObject:DDMakeColor(200, 120, 202)]; - [m_colors addObject:DDMakeColor(200, 120, 241)]; - [m_colors addObject:DDMakeColor(198, 119, 255)]; - - [m_colors addObject:DDMakeColor(198, 157, 37)]; - [m_colors addObject:DDMakeColor(196, 157, 116)]; - [m_colors addObject:DDMakeColor(195, 156, 157)]; - [m_colors addObject:DDMakeColor(195, 156, 197)]; - [m_colors addObject:DDMakeColor(194, 155, 236)]; - [m_colors addObject:DDMakeColor(193, 155, 255)]; - - [m_colors addObject:DDMakeColor(191, 192, 36)]; - [m_colors addObject:DDMakeColor(190, 191, 112)]; - [m_colors addObject:DDMakeColor(189, 191, 152)]; - [m_colors addObject:DDMakeColor(189, 191, 191)]; - [m_colors addObject:DDMakeColor(188, 190, 230)]; - [m_colors addObject:DDMakeColor(187, 190, 253)]; - - [m_colors addObject:DDMakeColor(185, 226, 28)]; - [m_colors addObject:DDMakeColor(184, 226, 106)]; - [m_colors addObject:DDMakeColor(183, 225, 146)]; - [m_colors addObject:DDMakeColor(183, 225, 186)]; - [m_colors addObject:DDMakeColor(182, 225, 225)]; - [m_colors addObject:DDMakeColor(181, 224, 252)]; - - [m_colors addObject:DDMakeColor(178, 255, 35)]; - [m_colors addObject:DDMakeColor(178, 255, 101)]; - [m_colors addObject:DDMakeColor(177, 254, 141)]; - [m_colors addObject:DDMakeColor(176, 254, 180)]; - [m_colors addObject:DDMakeColor(176, 254, 220)]; - [m_colors addObject:DDMakeColor(175, 253, 249)]; - - [m_colors addObject:DDMakeColor(247, 56, 30)]; - [m_colors addObject:DDMakeColor(245, 57, 122)]; - [m_colors addObject:DDMakeColor(243, 59, 163)]; - [m_colors addObject:DDMakeColor(244, 60, 204)]; - [m_colors addObject:DDMakeColor(242, 59, 241)]; - [m_colors addObject:DDMakeColor(240, 55, 255)]; - - [m_colors addObject:DDMakeColor(241, 119, 36)]; - [m_colors addObject:DDMakeColor(240, 120, 118)]; - [m_colors addObject:DDMakeColor(238, 119, 158)]; - [m_colors addObject:DDMakeColor(237, 119, 199)]; - [m_colors addObject:DDMakeColor(237, 118, 238)]; - [m_colors addObject:DDMakeColor(236, 118, 255)]; - - [m_colors addObject:DDMakeColor(235, 154, 36)]; - [m_colors addObject:DDMakeColor(235, 154, 114)]; - [m_colors addObject:DDMakeColor(234, 154, 154)]; - [m_colors addObject:DDMakeColor(232, 154, 194)]; - [m_colors addObject:DDMakeColor(232, 153, 234)]; - [m_colors addObject:DDMakeColor(232, 153, 255)]; - - [m_colors addObject:DDMakeColor(230, 190, 30)]; - [m_colors addObject:DDMakeColor(229, 189, 110)]; - [m_colors addObject:DDMakeColor(228, 189, 150)]; - [m_colors addObject:DDMakeColor(227, 189, 190)]; - [m_colors addObject:DDMakeColor(227, 189, 229)]; - [m_colors addObject:DDMakeColor(226, 188, 255)]; - - [m_colors addObject:DDMakeColor(224, 224, 35)]; - [m_colors addObject:DDMakeColor(223, 224, 105)]; - [m_colors addObject:DDMakeColor(222, 224, 144)]; - [m_colors addObject:DDMakeColor(222, 223, 184)]; - [m_colors addObject:DDMakeColor(222, 223, 224)]; - [m_colors addObject:DDMakeColor(220, 223, 253)]; - - [m_colors addObject:DDMakeColor(217, 253, 28)]; - [m_colors addObject:DDMakeColor(217, 253, 99)]; - [m_colors addObject:DDMakeColor(216, 252, 139)]; - [m_colors addObject:DDMakeColor(216, 252, 179)]; - [m_colors addObject:DDMakeColor(215, 252, 218)]; - [m_colors addObject:DDMakeColor(215, 251, 250)]; - - [m_colors addObject:DDMakeColor(255, 61, 30)]; - [m_colors addObject:DDMakeColor(255, 60, 118)]; - [m_colors addObject:DDMakeColor(255, 58, 159)]; - [m_colors addObject:DDMakeColor(255, 56, 199)]; - [m_colors addObject:DDMakeColor(255, 55, 238)]; - [m_colors addObject:DDMakeColor(255, 59, 255)]; - - [m_colors addObject:DDMakeColor(255, 117, 29)]; - [m_colors addObject:DDMakeColor(255, 117, 115)]; - [m_colors addObject:DDMakeColor(255, 117, 155)]; - [m_colors addObject:DDMakeColor(255, 117, 195)]; - [m_colors addObject:DDMakeColor(255, 116, 235)]; - [m_colors addObject:DDMakeColor(254, 116, 255)]; - - [m_colors addObject:DDMakeColor(255, 152, 27)]; - [m_colors addObject:DDMakeColor(255, 152, 111)]; - [m_colors addObject:DDMakeColor(254, 152, 152)]; - [m_colors addObject:DDMakeColor(255, 152, 192)]; - [m_colors addObject:DDMakeColor(254, 151, 231)]; - [m_colors addObject:DDMakeColor(253, 151, 253)]; - - [m_colors addObject:DDMakeColor(255, 187, 33)]; - [m_colors addObject:DDMakeColor(253, 187, 107)]; - [m_colors addObject:DDMakeColor(252, 187, 148)]; - [m_colors addObject:DDMakeColor(253, 187, 187)]; - [m_colors addObject:DDMakeColor(254, 187, 227)]; - [m_colors addObject:DDMakeColor(252, 186, 252)]; - - [m_colors addObject:DDMakeColor(252, 222, 34)]; - [m_colors addObject:DDMakeColor(251, 222, 103)]; - [m_colors addObject:DDMakeColor(251, 222, 143)]; - [m_colors addObject:DDMakeColor(250, 222, 182)]; - [m_colors addObject:DDMakeColor(251, 221, 222)]; - [m_colors addObject:DDMakeColor(252, 221, 252)]; - - [m_colors addObject:DDMakeColor(251, 252, 15)]; - [m_colors addObject:DDMakeColor(251, 252, 97)]; - [m_colors addObject:DDMakeColor(249, 252, 137)]; - [m_colors addObject:DDMakeColor(247, 252, 177)]; - [m_colors addObject:DDMakeColor(247, 253, 217)]; - [m_colors addObject:DDMakeColor(254, 255, 255)]; - - // Grayscale - - [m_colors addObject:DDMakeColor( 52, 53, 53)]; - [m_colors addObject:DDMakeColor( 57, 58, 59)]; - [m_colors addObject:DDMakeColor( 66, 67, 67)]; - [m_colors addObject:DDMakeColor( 75, 76, 76)]; - [m_colors addObject:DDMakeColor( 83, 85, 85)]; - [m_colors addObject:DDMakeColor( 92, 93, 94)]; - - [m_colors addObject:DDMakeColor(101, 102, 102)]; - [m_colors addObject:DDMakeColor(109, 111, 111)]; - [m_colors addObject:DDMakeColor(118, 119, 119)]; - [m_colors addObject:DDMakeColor(126, 127, 128)]; - [m_colors addObject:DDMakeColor(134, 136, 136)]; - [m_colors addObject:DDMakeColor(143, 144, 145)]; - - [m_colors addObject:DDMakeColor(151, 152, 153)]; - [m_colors addObject:DDMakeColor(159, 161, 161)]; - [m_colors addObject:DDMakeColor(167, 169, 169)]; - [m_colors addObject:DDMakeColor(176, 177, 177)]; - [m_colors addObject:DDMakeColor(184, 185, 186)]; - [m_colors addObject:DDMakeColor(192, 193, 194)]; - - [m_colors addObject:DDMakeColor(200, 201, 202)]; - [m_colors addObject:DDMakeColor(208, 209, 210)]; - [m_colors addObject:DDMakeColor(216, 218, 218)]; - [m_colors addObject:DDMakeColor(224, 226, 226)]; - [m_colors addObject:DDMakeColor(232, 234, 234)]; - [m_colors addObject:DDMakeColor(240, 242, 242)]; - - // Color codes - - int index = 16; - - while (index < 256) { - [m_codes_fg addObject:[NSString stringWithFormat:@"38;5;%dm", index]]; - [m_codes_bg addObject:[NSString stringWithFormat:@"48;5;%dm", index]]; - - index++; - } - - #else /* if MAP_TO_TERMINAL_APP_COLORS */ - - // Standard xterm colors: - // - // These are the colors xterm shells use in xterm-256color mode. - // In this mode, the shell supports 256 different colors, specified by 256 color codes. - // - // The first 16 color codes map to the original 16 color codes supported by the earlier xterm-color mode. - // These are generally configurable, and thus we ignore them for the purposes of mapping, - // as we can't rely on them being constant. They are largely duplicated anyway. - // - // The next 216 color codes are designed to run the spectrum, with several shades of every color. - // The last 24 color codes represent a grayscale. - // - // While the color codes are standardized, the actual RGB values for each color code is not. - // However most standard xterms follow a well known color chart, - // which can easily be calculated using the simple formula below. - // - // More information about ansi escape codes can be found online. - // http://en.wikipedia.org/wiki/ANSI_escape_code - - int index = 16; - - int r; // red - int g; // green - int b; // blue - - int ri; // r increment - int gi; // g increment - int bi; // b increment - - // Calculate xterm colors (using standard algorithm) - - int r = 0; - int g = 0; - int b = 0; - - for (ri = 0; ri < 6; ri++) { - r = (ri == 0) ? 0 : 95 + (40 * (ri - 1)); - - for (gi = 0; gi < 6; gi++) { - g = (gi == 0) ? 0 : 95 + (40 * (gi - 1)); - - for (bi = 0; bi < 6; bi++) { - b = (bi == 0) ? 0 : 95 + (40 * (bi - 1)); - - [m_codes_fg addObject:[NSString stringWithFormat:@"38;5;%dm", index]]; - [m_codes_bg addObject:[NSString stringWithFormat:@"48;5;%dm", index]]; - [m_colors addObject:DDMakeColor(r, g, b)]; - - index++; - } - } - } - - // Calculate xterm grayscale (using standard algorithm) - - r = 8; - g = 8; - b = 8; - - while (index < 256) { - [m_codes_fg addObject:[NSString stringWithFormat:@"38;5;%dm", index]]; - [m_codes_bg addObject:[NSString stringWithFormat:@"48;5;%dm", index]]; - [m_colors addObject:DDMakeColor(r, g, b)]; - - r += 10; - g += 10; - b += 10; - - index++; - } - - #endif /* if MAP_TO_TERMINAL_APP_COLORS */ - - codes_fg = [m_codes_fg copy]; - codes_bg = [m_codes_bg copy]; - colors = [m_colors copy]; - - NSAssert([codes_fg count] == [codes_bg count], @"Invalid colors/codes array(s)"); - NSAssert([codes_fg count] == [colors count], @"Invalid colors/codes array(s)"); -} - -+ (void)getRed:(CGFloat *)rPtr green:(CGFloat *)gPtr blue:(CGFloat *)bPtr fromColor:(DDColor *)color { - #if TARGET_OS_IPHONE - - // iOS - - BOOL done = NO; - - if ([color respondsToSelector:@selector(getRed:green:blue:alpha:)]) { - done = [color getRed:rPtr green:gPtr blue:bPtr alpha:NULL]; - } - - if (!done) { - // The method getRed:green:blue:alpha: was only available starting iOS 5. - // So in iOS 4 and earlier, we have to jump through hoops. - - CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB(); - - unsigned char pixel[4]; - CGContextRef context = CGBitmapContextCreate(&pixel, 1, 1, 8, 4, rgbColorSpace, (CGBitmapInfo)(kCGBitmapAlphaInfoMask & kCGImageAlphaNoneSkipLast)); - - CGContextSetFillColorWithColor(context, [color CGColor]); - CGContextFillRect(context, CGRectMake(0, 0, 1, 1)); - - if (rPtr) { - *rPtr = pixel[0] / 255.0f; - } - - if (gPtr) { - *gPtr = pixel[1] / 255.0f; - } - - if (bPtr) { - *bPtr = pixel[2] / 255.0f; - } - - CGContextRelease(context); - CGColorSpaceRelease(rgbColorSpace); - } - - #elif defined(DD_CLI) || !__has_include() - - // OS X without AppKit - - [color getRed:rPtr green:gPtr blue:bPtr alpha:NULL]; - - #else /* if TARGET_OS_IPHONE */ - - // OS X with AppKit - - NSColor *safeColor = [color colorUsingColorSpaceName:NSCalibratedRGBColorSpace]; - - [safeColor getRed:rPtr green:gPtr blue:bPtr alpha:NULL]; - #endif /* if TARGET_OS_IPHONE */ -} - -/** - * Maps the given color to the closest available color supported by the shell. - * The shell may support 256 colors, or only 16. - * - * This method loops through the known supported color set, and calculates the closest color. - * The array index of that color, within the colors array, is then returned. - * This array index may also be used as the index within the codes_fg and codes_bg arrays. - **/ -+ (NSUInteger)codeIndexForColor:(DDColor *)inColor { - CGFloat inR, inG, inB; - - [self getRed:&inR green:&inG blue:&inB fromColor:inColor]; - - NSUInteger bestIndex = 0; - CGFloat lowestDistance = 100.0f; - - NSUInteger i = 0; - - for (DDColor *color in colors) { - // Calculate Euclidean distance (lower value means closer to given color) - - CGFloat r, g, b; - [self getRed:&r green:&g blue:&b fromColor:color]; - - #if CGFLOAT_IS_DOUBLE - CGFloat distance = sqrt(pow(r - inR, 2.0) + pow(g - inG, 2.0) + pow(b - inB, 2.0)); - #else - CGFloat distance = sqrtf(powf(r - inR, 2.0f) + powf(g - inG, 2.0f) + powf(b - inB, 2.0f)); - #endif - - NSLogVerbose(@"DDTTYLogger: %3lu : %.3f,%.3f,%.3f & %.3f,%.3f,%.3f = %.6f", - (unsigned long)i, inR, inG, inB, r, g, b, distance); - - if (distance < lowestDistance) { - bestIndex = i; - lowestDistance = distance; - - NSLogVerbose(@"DDTTYLogger: New best index = %lu", (unsigned long)bestIndex); - } - - i++; - } - - return bestIndex; -} - -+ (instancetype)sharedInstance { - static dispatch_once_t DDTTYLoggerOnceToken; - - dispatch_once(&DDTTYLoggerOnceToken, ^{ - // Xcode does NOT natively support colors in the Xcode debugging console. - // You'll need to install the XcodeColors plugin to see colors in the Xcode console. - // - // PS - Please read the header file before diving into the source code. - - char *xcode_colors = getenv("XcodeColors"); - char *term = getenv("TERM"); - - if (xcode_colors && (strcmp(xcode_colors, "YES") == 0)) { - isaXcodeColorTTY = YES; - } else if (term) { - if (strcasestr(term, "color") != NULL) { - isaColorTTY = YES; - isaColor256TTY = (strcasestr(term, "256") != NULL); - - if (isaColor256TTY) { - [self initialize_colors_256]; - } else { - [self initialize_colors_16]; - } - } - } - - NSLogInfo(@"DDTTYLogger: isaColorTTY = %@", (isaColorTTY ? @"YES" : @"NO")); - NSLogInfo(@"DDTTYLogger: isaColor256TTY: %@", (isaColor256TTY ? @"YES" : @"NO")); - NSLogInfo(@"DDTTYLogger: isaXcodeColorTTY: %@", (isaXcodeColorTTY ? @"YES" : @"NO")); - - sharedInstance = [[[self class] alloc] init]; - }); - - return sharedInstance; -} - -- (instancetype)init { - if (sharedInstance != nil) { - return nil; - } - - if ((self = [super init])) { - _calendarUnitFlags = (NSCalendarUnitYear | - NSCalendarUnitMonth | - NSCalendarUnitDay | - NSCalendarUnitHour | - NSCalendarUnitMinute | - NSCalendarUnitSecond); - - // Initialze 'app' variable (char *) - - _appName = [[NSProcessInfo processInfo] processName]; - - _appLen = [_appName lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - - if (_appLen == 0) { - _appName = @""; - _appLen = [_appName lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - } - - _app = (char *)malloc(_appLen + 1); - - if (_app == NULL) { - return nil; - } - - BOOL processedAppName = [_appName getCString:_app maxLength:(_appLen + 1) encoding:NSUTF8StringEncoding]; - - if (NO == processedAppName) { - free(_app); - return nil; - } - - // Initialize 'pid' variable (char *) - - _processID = [NSString stringWithFormat:@"%i", (int)getpid()]; - - _pidLen = [_processID lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - _pid = (char *)malloc(_pidLen + 1); - - if (_pid == NULL) { - free(_app); - return nil; - } - - BOOL processedID = [_processID getCString:_pid maxLength:(_pidLen + 1) encoding:NSUTF8StringEncoding]; - - if (NO == processedID) { - free(_app); - free(_pid); - return nil; - } - - // Initialize color stuff - - _colorsEnabled = NO; - _colorProfilesArray = [[NSMutableArray alloc] initWithCapacity:8]; - _colorProfilesDict = [[NSMutableDictionary alloc] initWithCapacity:8]; - - _automaticallyAppendNewlineForCustomFormatters = YES; - } - - return self; -} - -- (void)loadDefaultColorProfiles { - [self setForegroundColor:DDMakeColor(214, 57, 30) backgroundColor:nil forFlag:DDLogFlagError]; - [self setForegroundColor:DDMakeColor(204, 121, 32) backgroundColor:nil forFlag:DDLogFlagWarning]; -} - -- (BOOL)colorsEnabled { - // The design of this method is taken from the DDAbstractLogger implementation. - // For extensive documentation please refer to the DDAbstractLogger implementation. - - // Note: The internal implementation MUST access the colorsEnabled variable directly, - // This method is designed explicitly for external access. - // - // Using "self." syntax to go through this method will cause immediate deadlock. - // This is the intended result. Fix it by accessing the ivar directly. - // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. - - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); - - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - - __block BOOL result; - - dispatch_sync(globalLoggingQueue, ^{ - dispatch_sync(self.loggerQueue, ^{ - result = _colorsEnabled; - }); - }); - - return result; -} - -- (void)setColorsEnabled:(BOOL)newColorsEnabled { - dispatch_block_t block = ^{ - @autoreleasepool { - _colorsEnabled = newColorsEnabled; - - if ([_colorProfilesArray count] == 0) { - [self loadDefaultColorProfiles]; - } - } - }; - - // The design of this method is taken from the DDAbstractLogger implementation. - // For extensive documentation please refer to the DDAbstractLogger implementation. - - // Note: The internal implementation MUST access the colorsEnabled variable directly, - // This method is designed explicitly for external access. - // - // Using "self." syntax to go through this method will cause immediate deadlock. - // This is the intended result. Fix it by accessing the ivar directly. - // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. - - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); - - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); -} - -- (void)setForegroundColor:(DDColor *)txtColor backgroundColor:(DDColor *)bgColor forFlag:(DDLogFlag)mask { - [self setForegroundColor:txtColor backgroundColor:bgColor forFlag:mask context:LOG_CONTEXT_ALL]; -} - -- (void)setForegroundColor:(DDColor *)txtColor backgroundColor:(DDColor *)bgColor forFlag:(DDLogFlag)mask context:(NSInteger)ctxt { - dispatch_block_t block = ^{ - @autoreleasepool { - DDTTYLoggerColorProfile *newColorProfile = - [[DDTTYLoggerColorProfile alloc] initWithForegroundColor:txtColor - backgroundColor:bgColor - flag:mask - context:ctxt]; - - NSLogInfo(@"DDTTYLogger: newColorProfile: %@", newColorProfile); - - NSUInteger i = 0; - - for (DDTTYLoggerColorProfile *colorProfile in _colorProfilesArray) { - if ((colorProfile->mask == mask) && (colorProfile->context == ctxt)) { - break; - } - - i++; - } - - if (i < [_colorProfilesArray count]) { - _colorProfilesArray[i] = newColorProfile; - } else { - [_colorProfilesArray addObject:newColorProfile]; - } - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (void)setForegroundColor:(DDColor *)txtColor backgroundColor:(DDColor *)bgColor forTag:(id )tag { - NSAssert([(id < NSObject >) tag conformsToProtocol: @protocol(NSCopying)], @"Invalid tag"); - - dispatch_block_t block = ^{ - @autoreleasepool { - DDTTYLoggerColorProfile *newColorProfile = - [[DDTTYLoggerColorProfile alloc] initWithForegroundColor:txtColor - backgroundColor:bgColor - flag:(DDLogFlag)0 - context:0]; - - NSLogInfo(@"DDTTYLogger: newColorProfile: %@", newColorProfile); - - _colorProfilesDict[tag] = newColorProfile; - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (void)clearColorsForFlag:(DDLogFlag)mask { - [self clearColorsForFlag:mask context:0]; -} - -- (void)clearColorsForFlag:(DDLogFlag)mask context:(NSInteger)context { - dispatch_block_t block = ^{ - @autoreleasepool { - NSUInteger i = 0; - - for (DDTTYLoggerColorProfile *colorProfile in _colorProfilesArray) { - if ((colorProfile->mask == mask) && (colorProfile->context == context)) { - break; - } - - i++; - } - - if (i < [_colorProfilesArray count]) { - [_colorProfilesArray removeObjectAtIndex:i]; - } - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (void)clearColorsForTag:(id )tag { - NSAssert([(id < NSObject >) tag conformsToProtocol: @protocol(NSCopying)], @"Invalid tag"); - - dispatch_block_t block = ^{ - @autoreleasepool { - [_colorProfilesDict removeObjectForKey:tag]; - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (void)clearColorsForAllFlags { - dispatch_block_t block = ^{ - @autoreleasepool { - [_colorProfilesArray removeAllObjects]; - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (void)clearColorsForAllTags { - dispatch_block_t block = ^{ - @autoreleasepool { - [_colorProfilesDict removeAllObjects]; - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (void)clearAllColors { - dispatch_block_t block = ^{ - @autoreleasepool { - [_colorProfilesArray removeAllObjects]; - [_colorProfilesDict removeAllObjects]; - } - }; - - // The design of the setter logic below is taken from the DDAbstractLogger implementation. - // For documentation please refer to the DDAbstractLogger implementation. - - if ([self isOnInternalLoggerQueue]) { - block(); - } else { - dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; - NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); - - dispatch_async(globalLoggingQueue, ^{ - dispatch_async(self.loggerQueue, block); - }); - } -} - -- (void)logMessage:(DDLogMessage *)logMessage { - NSString *logMsg = logMessage->_message; - BOOL isFormatted = NO; - - if (_logFormatter) { - logMsg = [_logFormatter formatLogMessage:logMessage]; - isFormatted = logMsg != logMessage->_message; - } - - if (logMsg) { - // Search for a color profile associated with the log message - - DDTTYLoggerColorProfile *colorProfile = nil; - - if (_colorsEnabled) { - if (logMessage->_tag) { - colorProfile = _colorProfilesDict[logMessage->_tag]; - } - - if (colorProfile == nil) { - for (DDTTYLoggerColorProfile *cp in _colorProfilesArray) { - if (logMessage->_flag & cp->mask) { - // Color profile set for this context? - if (logMessage->_context == cp->context) { - colorProfile = cp; - - // Stop searching - break; - } - - // Check if LOG_CONTEXT_ALL was specified as a default color for this flag - if (cp->context == LOG_CONTEXT_ALL) { - colorProfile = cp; - - // We don't break to keep searching for more specific color profiles for the context - } - } - } - } - } - - // Convert log message to C string. - // - // We use the stack instead of the heap for speed if possible. - // But we're extra cautious to avoid a stack overflow. - - NSUInteger msgLen = [logMsg lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - const BOOL useStack = msgLen < (1024 * 4); - - char msgStack[useStack ? (msgLen + 1) : 1]; // Analyzer doesn't like zero-size array, hence the 1 - char *msg = useStack ? msgStack : (char *)malloc(msgLen + 1); - - if (msg == NULL) { - return; - } - - BOOL logMsgEnc = [logMsg getCString:msg maxLength:(msgLen + 1) encoding:NSUTF8StringEncoding]; - - if (!logMsgEnc) { - if (!useStack && msg != NULL) { - free(msg); - } - - return; - } - - // Write the log message to STDERR - - if (isFormatted) { - // The log message has already been formatted. - int iovec_len = (_automaticallyAppendNewlineForCustomFormatters) ? 5 : 4; - struct iovec v[iovec_len]; - - if (colorProfile) { - v[0].iov_base = colorProfile->fgCode; - v[0].iov_len = colorProfile->fgCodeLen; - - v[1].iov_base = colorProfile->bgCode; - v[1].iov_len = colorProfile->bgCodeLen; - - v[iovec_len - 1].iov_base = colorProfile->resetCode; - v[iovec_len - 1].iov_len = colorProfile->resetCodeLen; - } else { - v[0].iov_base = ""; - v[0].iov_len = 0; - - v[1].iov_base = ""; - v[1].iov_len = 0; - - v[iovec_len - 1].iov_base = ""; - v[iovec_len - 1].iov_len = 0; - } - - v[2].iov_base = (char *)msg; - v[2].iov_len = msgLen; - - if (iovec_len == 5) { - v[3].iov_base = "\n"; - v[3].iov_len = (msg[msgLen] == '\n') ? 0 : 1; - } - - writev(STDERR_FILENO, v, iovec_len); - } else { - // The log message is unformatted, so apply standard NSLog style formatting. - - int len; - char ts[24] = ""; - size_t tsLen = 0; - - // Calculate timestamp. - // The technique below is faster than using NSDateFormatter. - if (logMessage->_timestamp) { - NSDateComponents *components = [[NSCalendar autoupdatingCurrentCalendar] components:_calendarUnitFlags fromDate:logMessage->_timestamp]; - - NSTimeInterval epoch = [logMessage->_timestamp timeIntervalSinceReferenceDate]; - int milliseconds = (int)((epoch - floor(epoch)) * 1000); - - len = snprintf(ts, 24, "%04ld-%02ld-%02ld %02ld:%02ld:%02ld:%03d", // yyyy-MM-dd HH:mm:ss:SSS - (long)components.year, - (long)components.month, - (long)components.day, - (long)components.hour, - (long)components.minute, - (long)components.second, milliseconds); - - tsLen = (NSUInteger)MAX(MIN(24 - 1, len), 0); - } - - // Calculate thread ID - // - // How many characters do we need for the thread id? - // logMessage->machThreadID is of type mach_port_t, which is an unsigned int. - // - // 1 hex char = 4 bits - // 8 hex chars for 32 bit, plus ending '\0' = 9 - - char tid[9]; - len = snprintf(tid, 9, "%s", [logMessage->_threadID cStringUsingEncoding:NSUTF8StringEncoding]); - - size_t tidLen = (NSUInteger)MAX(MIN(9 - 1, len), 0); - - // Here is our format: "%s %s[%i:%s] %s", timestamp, appName, processID, threadID, logMsg - - struct iovec v[13]; - - if (colorProfile) { - v[0].iov_base = colorProfile->fgCode; - v[0].iov_len = colorProfile->fgCodeLen; - - v[1].iov_base = colorProfile->bgCode; - v[1].iov_len = colorProfile->bgCodeLen; - - v[12].iov_base = colorProfile->resetCode; - v[12].iov_len = colorProfile->resetCodeLen; - } else { - v[0].iov_base = ""; - v[0].iov_len = 0; - - v[1].iov_base = ""; - v[1].iov_len = 0; - - v[12].iov_base = ""; - v[12].iov_len = 0; - } - - v[2].iov_base = ts; - v[2].iov_len = tsLen; - - v[3].iov_base = " "; - v[3].iov_len = 1; - - v[4].iov_base = _app; - v[4].iov_len = _appLen; - - v[5].iov_base = "["; - v[5].iov_len = 1; - - v[6].iov_base = _pid; - v[6].iov_len = _pidLen; - - v[7].iov_base = ":"; - v[7].iov_len = 1; - - v[8].iov_base = tid; - v[8].iov_len = MIN((size_t)8, tidLen); // snprintf doesn't return what you might think - - v[9].iov_base = "] "; - v[9].iov_len = 2; - - v[10].iov_base = (char *)msg; - v[10].iov_len = msgLen; - - v[11].iov_base = "\n"; - v[11].iov_len = (msg[msgLen] == '\n') ? 0 : 1; - - writev(STDERR_FILENO, v, 13); - } - - if (!useStack) { - free(msg); - } - } -} - -- (NSString *)loggerName { - return @"cocoa.lumberjack.ttyLogger"; -} - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -@implementation DDTTYLoggerColorProfile - -- (instancetype)initWithForegroundColor:(DDColor *)fgColor backgroundColor:(DDColor *)bgColor flag:(DDLogFlag)aMask context:(NSInteger)ctxt { - if ((self = [super init])) { - mask = aMask; - context = ctxt; - - CGFloat r, g, b; - - if (fgColor) { - [DDTTYLogger getRed:&r green:&g blue:&b fromColor:fgColor]; - - fg_r = (uint8_t)(r * 255.0f); - fg_g = (uint8_t)(g * 255.0f); - fg_b = (uint8_t)(b * 255.0f); - } - - if (bgColor) { - [DDTTYLogger getRed:&r green:&g blue:&b fromColor:bgColor]; - - bg_r = (uint8_t)(r * 255.0f); - bg_g = (uint8_t)(g * 255.0f); - bg_b = (uint8_t)(b * 255.0f); - } - - if (fgColor && isaColorTTY) { - // Map foreground color to closest available shell color - - fgCodeIndex = [DDTTYLogger codeIndexForColor:fgColor]; - fgCodeRaw = codes_fg[fgCodeIndex]; - - NSString *escapeSeq = @"\033["; - - NSUInteger len1 = [escapeSeq lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - NSUInteger len2 = [fgCodeRaw lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - - BOOL escapeSeqEnc = [escapeSeq getCString:(fgCode) maxLength:(len1 + 1) encoding:NSUTF8StringEncoding]; - BOOL fgCodeRawEsc = [fgCodeRaw getCString:(fgCode + len1) maxLength:(len2 + 1) encoding:NSUTF8StringEncoding]; - - if (!escapeSeqEnc || !fgCodeRawEsc) { - return nil; - } - - fgCodeLen = len1 + len2; - } else if (fgColor && isaXcodeColorTTY) { - // Convert foreground color to color code sequence - - const char *escapeSeq = XCODE_COLORS_ESCAPE_SEQ; - - int result = snprintf(fgCode, 24, "%sfg%u,%u,%u;", escapeSeq, fg_r, fg_g, fg_b); - fgCodeLen = (NSUInteger)MAX(MIN(result, (24 - 1)), 0); - } else { - // No foreground color or no color support - - fgCode[0] = '\0'; - fgCodeLen = 0; - } - - if (bgColor && isaColorTTY) { - // Map background color to closest available shell color - - bgCodeIndex = [DDTTYLogger codeIndexForColor:bgColor]; - bgCodeRaw = codes_bg[bgCodeIndex]; - - NSString *escapeSeq = @"\033["; - - NSUInteger len1 = [escapeSeq lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - NSUInteger len2 = [bgCodeRaw lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - - BOOL escapeSeqEnc = [escapeSeq getCString:(bgCode) maxLength:(len1 + 1) encoding:NSUTF8StringEncoding]; - BOOL bgCodeRawEsc = [bgCodeRaw getCString:(bgCode + len1) maxLength:(len2 + 1) encoding:NSUTF8StringEncoding]; - - if (!escapeSeqEnc || !bgCodeRawEsc) { - return nil; - } - - bgCodeLen = len1 + len2; - } else if (bgColor && isaXcodeColorTTY) { - // Convert background color to color code sequence - - const char *escapeSeq = XCODE_COLORS_ESCAPE_SEQ; - - int result = snprintf(bgCode, 24, "%sbg%u,%u,%u;", escapeSeq, bg_r, bg_g, bg_b); - bgCodeLen = (NSUInteger)MAX(MIN(result, (24 - 1)), 0); - } else { - // No background color or no color support - - bgCode[0] = '\0'; - bgCodeLen = 0; - } - - if (isaColorTTY) { - resetCodeLen = (NSUInteger)MAX(snprintf(resetCode, 8, "\033[0m"), 0); - } else if (isaXcodeColorTTY) { - resetCodeLen = (NSUInteger)MAX(snprintf(resetCode, 8, XCODE_COLORS_RESET), 0); - } else { - resetCode[0] = '\0'; - resetCodeLen = 0; - } - } - - return self; -} - -- (NSString *)description { - return [NSString stringWithFormat: - @"", - self, (int)mask, (long)context, fg_r, fg_g, fg_b, bg_r, bg_g, bg_b, fgCodeRaw, bgCodeRaw]; -} - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDContextFilterLogFormatter.h b/Example/Pods/CocoaLumberjack/Classes/Extensions/DDContextFilterLogFormatter.h deleted file mode 100644 index 1657f1f..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDContextFilterLogFormatter.h +++ /dev/null @@ -1,117 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif - -#import "DDLog.h" - -/** - * This class provides a log formatter that filters log statements from a logging context not on the whitelist. - * - * A log formatter can be added to any logger to format and/or filter its output. - * You can learn more about log formatters here: - * Documentation/CustomFormatters.md - * - * You can learn more about logging context's here: - * Documentation/CustomContext.md - * - * But here's a quick overview / refresher: - * - * Every log statement has a logging context. - * These come from the underlying logging macros defined in DDLog.h. - * The default logging context is zero. - * You can define multiple logging context's for use in your application. - * For example, logically separate parts of your app each have a different logging context. - * Also 3rd party frameworks that make use of Lumberjack generally use their own dedicated logging context. - **/ -@interface DDContextWhitelistFilterLogFormatter : NSObject - -/** - * Designated default initializer - */ -- (instancetype)init NS_DESIGNATED_INITIALIZER; - -/** - * Add a context to the whitelist - * - * @param loggingContext the context - */ -- (void)addToWhitelist:(NSUInteger)loggingContext; - -/** - * Remove context from whitelist - * - * @param loggingContext the context - */ -- (void)removeFromWhitelist:(NSUInteger)loggingContext; - -/** - * Return the whitelist - */ -@property (readonly, copy) NSArray *whitelist; - -/** - * Check if a context is on the whitelist - * - * @param loggingContext the context - */ -- (BOOL)isOnWhitelist:(NSUInteger)loggingContext; - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * This class provides a log formatter that filters log statements from a logging context on the blacklist. - **/ -@interface DDContextBlacklistFilterLogFormatter : NSObject - -- (instancetype)init NS_DESIGNATED_INITIALIZER; - -/** - * Add a context to the blacklist - * - * @param loggingContext the context - */ -- (void)addToBlacklist:(NSUInteger)loggingContext; - -/** - * Remove context from blacklist - * - * @param loggingContext the context - */ -- (void)removeFromBlacklist:(NSUInteger)loggingContext; - -/** - * Return the blacklist - */ -@property (readonly, copy) NSArray *blacklist; - - -/** - * Check if a context is on the blacklist - * - * @param loggingContext the context - */ -- (BOOL)isOnBlacklist:(NSUInteger)loggingContext; - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDContextFilterLogFormatter.m b/Example/Pods/CocoaLumberjack/Classes/Extensions/DDContextFilterLogFormatter.m deleted file mode 100644 index 14a6ae9..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDContextFilterLogFormatter.m +++ /dev/null @@ -1,191 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import "DDContextFilterLogFormatter.h" -#import - -#if !__has_feature(objc_arc) -#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). -#endif - -@interface DDLoggingContextSet : NSObject - -- (void)addToSet:(NSUInteger)loggingContext; -- (void)removeFromSet:(NSUInteger)loggingContext; - -@property (readonly, copy) NSArray *currentSet; - -- (BOOL)isInSet:(NSUInteger)loggingContext; - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -@interface DDContextWhitelistFilterLogFormatter () { - DDLoggingContextSet *_contextSet; -} - -@end - - -@implementation DDContextWhitelistFilterLogFormatter - -- (instancetype)init { - if ((self = [super init])) { - _contextSet = [[DDLoggingContextSet alloc] init]; - } - - return self; -} - -- (void)addToWhitelist:(NSUInteger)loggingContext { - [_contextSet addToSet:loggingContext]; -} - -- (void)removeFromWhitelist:(NSUInteger)loggingContext { - [_contextSet removeFromSet:loggingContext]; -} - -- (NSArray *)whitelist { - return [_contextSet currentSet]; -} - -- (BOOL)isOnWhitelist:(NSUInteger)loggingContext { - return [_contextSet isInSet:loggingContext]; -} - -- (NSString *)formatLogMessage:(DDLogMessage *)logMessage { - if ([self isOnWhitelist:logMessage->_context]) { - return logMessage->_message; - } else { - return nil; - } -} - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -@interface DDContextBlacklistFilterLogFormatter () { - DDLoggingContextSet *_contextSet; -} - -@end - - -@implementation DDContextBlacklistFilterLogFormatter - -- (instancetype)init { - if ((self = [super init])) { - _contextSet = [[DDLoggingContextSet alloc] init]; - } - - return self; -} - -- (void)addToBlacklist:(NSUInteger)loggingContext { - [_contextSet addToSet:loggingContext]; -} - -- (void)removeFromBlacklist:(NSUInteger)loggingContext { - [_contextSet removeFromSet:loggingContext]; -} - -- (NSArray *)blacklist { - return [_contextSet currentSet]; -} - -- (BOOL)isOnBlacklist:(NSUInteger)loggingContext { - return [_contextSet isInSet:loggingContext]; -} - -- (NSString *)formatLogMessage:(DDLogMessage *)logMessage { - if ([self isOnBlacklist:logMessage->_context]) { - return nil; - } else { - return logMessage->_message; - } -} - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - -@interface DDLoggingContextSet () { - OSSpinLock _lock; - NSMutableSet *_set; -} - -@end - - -@implementation DDLoggingContextSet - -- (instancetype)init { - if ((self = [super init])) { - _set = [[NSMutableSet alloc] init]; - } - - return self; -} - -- (void)addToSet:(NSUInteger)loggingContext { - OSSpinLockLock(&_lock); - { - [_set addObject:@(loggingContext)]; - } - OSSpinLockUnlock(&_lock); -} - -- (void)removeFromSet:(NSUInteger)loggingContext { - OSSpinLockLock(&_lock); - { - [_set removeObject:@(loggingContext)]; - } - OSSpinLockUnlock(&_lock); -} - -- (NSArray *)currentSet { - NSArray *result = nil; - - OSSpinLockLock(&_lock); - { - result = [_set allObjects]; - } - OSSpinLockUnlock(&_lock); - - return result; -} - -- (BOOL)isInSet:(NSUInteger)loggingContext { - BOOL result = NO; - - OSSpinLockLock(&_lock); - { - result = [_set containsObject:@(loggingContext)]; - } - OSSpinLockUnlock(&_lock); - - return result; -} - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDDispatchQueueLogFormatter.h b/Example/Pods/CocoaLumberjack/Classes/Extensions/DDDispatchQueueLogFormatter.h deleted file mode 100644 index 129f6e1..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDDispatchQueueLogFormatter.h +++ /dev/null @@ -1,178 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import -#import - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif - -#import "DDLog.h" - -/** - * Log formatter mode - */ -typedef NS_ENUM(NSUInteger, DDDispatchQueueLogFormatterMode){ - /** - * This is the default option, means the formatter can be reused between multiple loggers and therefore is thread-safe. - * There is, of course, a performance cost for the thread-safety - */ - DDDispatchQueueLogFormatterModeShareble = 0, - /** - * If the formatter will only be used by a single logger, then the thread-safety can be removed - * @note: there is an assert checking if the formatter is added to multiple loggers and the mode is non-shareble - */ - DDDispatchQueueLogFormatterModeNonShareble, -}; - - -/** - * This class provides a log formatter that prints the dispatch_queue label instead of the mach_thread_id. - * - * A log formatter can be added to any logger to format and/or filter its output. - * You can learn more about log formatters here: - * Documentation/CustomFormatters.md - * - * A typical `NSLog` (or `DDTTYLogger`) prints detailed info as `[:]`. - * For example: - * - * `2011-10-17 20:21:45.435 AppName[19928:5207] Your log message here` - * - * Where: - * `- 19928 = process id` - * `- 5207 = thread id (mach_thread_id printed in hex)` - * - * When using grand central dispatch (GCD), this information is less useful. - * This is because a single serial dispatch queue may be run on any thread from an internally managed thread pool. - * For example: - * - * `2011-10-17 20:32:31.111 AppName[19954:4d07] Message from my_serial_dispatch_queue` - * `2011-10-17 20:32:31.112 AppName[19954:5207] Message from my_serial_dispatch_queue` - * `2011-10-17 20:32:31.113 AppName[19954:2c55] Message from my_serial_dispatch_queue` - * - * This formatter allows you to replace the standard `[box:info]` with the dispatch_queue name. - * For example: - * - * `2011-10-17 20:32:31.111 AppName[img-scaling] Message from my_serial_dispatch_queue` - * `2011-10-17 20:32:31.112 AppName[img-scaling] Message from my_serial_dispatch_queue` - * `2011-10-17 20:32:31.113 AppName[img-scaling] Message from my_serial_dispatch_queue` - * - * If the dispatch_queue doesn't have a set name, then it falls back to the thread name. - * If the current thread doesn't have a set name, then it falls back to the mach_thread_id in hex (like normal). - * - * Note: If manually creating your own background threads (via `NSThread/alloc/init` or `NSThread/detachNeThread`), - * you can use `[[NSThread currentThread] setName:(NSString *)]`. - **/ -@interface DDDispatchQueueLogFormatter : NSObject - -/** - * Standard init method. - * Configure using properties as desired. - **/ -- (instancetype)init NS_DESIGNATED_INITIALIZER; - -/** - * Initializer with ability to set the queue mode - * - * @param mode choose between DDDispatchQueueLogFormatterModeShareble and DDDispatchQueueLogFormatterModeNonShareble, depending if the formatter is shared between several loggers or not - */ -- (instancetype)initWithMode:(DDDispatchQueueLogFormatterMode)mode; - -/** - * The minQueueLength restricts the minimum size of the [detail box]. - * If the minQueueLength is set to 0, there is no restriction. - * - * For example, say a dispatch_queue has a label of "diskIO": - * - * If the minQueueLength is 0: [diskIO] - * If the minQueueLength is 4: [diskIO] - * If the minQueueLength is 5: [diskIO] - * If the minQueueLength is 6: [diskIO] - * If the minQueueLength is 7: [diskIO ] - * If the minQueueLength is 8: [diskIO ] - * - * The default minQueueLength is 0 (no minimum, so [detail box] won't be padded). - * - * If you want every [detail box] to have the exact same width, - * set both minQueueLength and maxQueueLength to the same value. - **/ -@property (assign, atomic) NSUInteger minQueueLength; - -/** - * The maxQueueLength restricts the number of characters that will be inside the [detail box]. - * If the maxQueueLength is 0, there is no restriction. - * - * For example, say a dispatch_queue has a label of "diskIO": - * - * If the maxQueueLength is 0: [diskIO] - * If the maxQueueLength is 4: [disk] - * If the maxQueueLength is 5: [diskI] - * If the maxQueueLength is 6: [diskIO] - * If the maxQueueLength is 7: [diskIO] - * If the maxQueueLength is 8: [diskIO] - * - * The default maxQueueLength is 0 (no maximum, so [detail box] won't be truncated). - * - * If you want every [detail box] to have the exact same width, - * set both minQueueLength and maxQueueLength to the same value. - **/ -@property (assign, atomic) NSUInteger maxQueueLength; - -/** - * Sometimes queue labels have long names like "com.apple.main-queue", - * but you'd prefer something shorter like simply "main". - * - * This method allows you to set such preferred replacements. - * The above example is set by default. - * - * To remove/undo a previous replacement, invoke this method with nil for the 'shortLabel' parameter. - **/ -- (NSString *)replacementStringForQueueLabel:(NSString *)longLabel; - -/** - * See the `replacementStringForQueueLabel:` description - */ -- (void)setReplacementString:(NSString *)shortLabel forQueueLabel:(NSString *)longLabel; - -@end - -/** - * Category on `DDDispatchQueueLogFormatter` to make method declarations easier to extend/modify - **/ -@interface DDDispatchQueueLogFormatter (OverridableMethods) - -/** - * Date formatter default configuration - */ -- (void)configureDateFormatter:(NSDateFormatter *)dateFormatter; - -/** - * Formatter method to transfrom from date to string - */ -- (NSString *)stringFromDate:(NSDate *)date; - -/** - * Method to compute the queue thread label - */ -- (NSString *)queueThreadLabelForLogMessage:(DDLogMessage *)logMessage; - -/** - * The actual method that formats a message (transforms a `DDLogMessage` model into a printable string) - */ -- (NSString *)formatLogMessage:(DDLogMessage *)logMessage; - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDDispatchQueueLogFormatter.m b/Example/Pods/CocoaLumberjack/Classes/Extensions/DDDispatchQueueLogFormatter.m deleted file mode 100644 index fdcc87b..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDDispatchQueueLogFormatter.m +++ /dev/null @@ -1,277 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import "DDDispatchQueueLogFormatter.h" -#import -#import - - -#if !__has_feature(objc_arc) -#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). -#endif - -@interface DDDispatchQueueLogFormatter () { - DDDispatchQueueLogFormatterMode _mode; - NSString *_dateFormatterKey; - - int32_t _atomicLoggerCount; - NSDateFormatter *_threadUnsafeDateFormatter; // Use [self stringFromDate] - - OSSpinLock _lock; - - NSUInteger _minQueueLength; // _prefix == Only access via atomic property - NSUInteger _maxQueueLength; // _prefix == Only access via atomic property - NSMutableDictionary *_replacements; // _prefix == Only access from within spinlock -} - -@end - - -@implementation DDDispatchQueueLogFormatter - -- (instancetype)init { - if ((self = [super init])) { - _mode = DDDispatchQueueLogFormatterModeShareble; - - // We need to carefully pick the name for storing in thread dictionary to not - // use a formatter configured by subclass and avoid surprises. - Class cls = [self class]; - Class superClass = class_getSuperclass(cls); - SEL configMethodName = @selector(configureDateFormatter:); - Method configMethod = class_getInstanceMethod(cls, configMethodName); - while (class_getInstanceMethod(superClass, configMethodName) == configMethod) { - cls = superClass; - superClass = class_getSuperclass(cls); - } - // now `cls` is the class that provides implementation for `configureDateFormatter:` - _dateFormatterKey = [NSString stringWithFormat:@"%s_NSDateFormatter", class_getName(cls)]; - - _atomicLoggerCount = 0; - _threadUnsafeDateFormatter = nil; - - _minQueueLength = 0; - _maxQueueLength = 0; - _replacements = [[NSMutableDictionary alloc] init]; - - // Set default replacements: - - _replacements[@"com.apple.main-thread"] = @"main"; - } - - return self; -} - -- (instancetype)initWithMode:(DDDispatchQueueLogFormatterMode)mode { - if ((self = [self init])) { - _mode = mode; - } - return self; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Configuration -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -@synthesize minQueueLength = _minQueueLength; -@synthesize maxQueueLength = _maxQueueLength; - -- (NSString *)replacementStringForQueueLabel:(NSString *)longLabel { - NSString *result = nil; - - OSSpinLockLock(&_lock); - { - result = _replacements[longLabel]; - } - OSSpinLockUnlock(&_lock); - - return result; -} - -- (void)setReplacementString:(NSString *)shortLabel forQueueLabel:(NSString *)longLabel { - OSSpinLockLock(&_lock); - { - if (shortLabel) { - _replacements[longLabel] = shortLabel; - } else { - [_replacements removeObjectForKey:longLabel]; - } - } - OSSpinLockUnlock(&_lock); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark DDLogFormatter -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -- (NSDateFormatter *)createDateFormatter { - NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; - [self configureDateFormatter:formatter]; - return formatter; -} - -- (void)configureDateFormatter:(NSDateFormatter *)dateFormatter { - [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; - [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss:SSS"]; - [dateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]]; - - NSString *calendarIdentifier = nil; -#if defined(__IPHONE_8_0) || defined(__MAC_10_10) - calendarIdentifier = NSCalendarIdentifierGregorian; -#else - calendarIdentifier = NSGregorianCalendar; -#endif - - [dateFormatter setCalendar:[[NSCalendar alloc] initWithCalendarIdentifier:calendarIdentifier]]; -} - -- (NSString *)stringFromDate:(NSDate *)date { - - NSDateFormatter *dateFormatter = nil; - if (_mode == DDDispatchQueueLogFormatterModeNonShareble) { - // Single-threaded mode. - - dateFormatter = _threadUnsafeDateFormatter; - if (dateFormatter == nil) { - dateFormatter = [self createDateFormatter]; - _threadUnsafeDateFormatter = dateFormatter; - } - } else { - // Multi-threaded mode. - // NSDateFormatter is NOT thread-safe. - - NSString *key = _dateFormatterKey; - - NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary]; - dateFormatter = threadDictionary[key]; - - if (dateFormatter == nil) { - dateFormatter = [self createDateFormatter]; - threadDictionary[key] = dateFormatter; - } - } - - return [dateFormatter stringFromDate:date]; -} - -- (NSString *)queueThreadLabelForLogMessage:(DDLogMessage *)logMessage { - // As per the DDLogFormatter contract, this method is always invoked on the same thread/dispatch_queue - - NSUInteger minQueueLength = self.minQueueLength; - NSUInteger maxQueueLength = self.maxQueueLength; - - // Get the name of the queue, thread, or machID (whichever we are to use). - - NSString *queueThreadLabel = nil; - - BOOL useQueueLabel = YES; - BOOL useThreadName = NO; - - if (logMessage->_queueLabel) { - // If you manually create a thread, it's dispatch_queue will have one of the thread names below. - // Since all such threads have the same name, we'd prefer to use the threadName or the machThreadID. - - NSArray *names = @[ - @"com.apple.root.low-priority", - @"com.apple.root.default-priority", - @"com.apple.root.high-priority", - @"com.apple.root.low-overcommit-priority", - @"com.apple.root.default-overcommit-priority", - @"com.apple.root.high-overcommit-priority" - ]; - - for (NSString * name in names) { - if ([logMessage->_queueLabel isEqualToString:name]) { - useQueueLabel = NO; - useThreadName = [logMessage->_threadName length] > 0; - break; - } - } - } else { - useQueueLabel = NO; - useThreadName = [logMessage->_threadName length] > 0; - } - - if (useQueueLabel || useThreadName) { - NSString *fullLabel; - NSString *abrvLabel; - - if (useQueueLabel) { - fullLabel = logMessage->_queueLabel; - } else { - fullLabel = logMessage->_threadName; - } - - OSSpinLockLock(&_lock); - { - abrvLabel = _replacements[fullLabel]; - } - OSSpinLockUnlock(&_lock); - - if (abrvLabel) { - queueThreadLabel = abrvLabel; - } else { - queueThreadLabel = fullLabel; - } - } else { - queueThreadLabel = logMessage->_threadID; - } - - // Now use the thread label in the output - - NSUInteger labelLength = [queueThreadLabel length]; - - // labelLength > maxQueueLength : truncate - // labelLength < minQueueLength : padding - // : exact - - if ((maxQueueLength > 0) && (labelLength > maxQueueLength)) { - // Truncate - - return [queueThreadLabel substringToIndex:maxQueueLength]; - } else if (labelLength < minQueueLength) { - // Padding - - NSUInteger numSpaces = minQueueLength - labelLength; - - char spaces[numSpaces + 1]; - memset(spaces, ' ', numSpaces); - spaces[numSpaces] = '\0'; - - return [NSString stringWithFormat:@"%@%s", queueThreadLabel, spaces]; - } else { - // Exact - - return queueThreadLabel; - } -} - -- (NSString *)formatLogMessage:(DDLogMessage *)logMessage { - NSString *timestamp = [self stringFromDate:(logMessage->_timestamp)]; - NSString *queueThreadLabel = [self queueThreadLabelForLogMessage:logMessage]; - - return [NSString stringWithFormat:@"%@ [%@] %@", timestamp, queueThreadLabel, logMessage->_message]; -} - -- (void)didAddToLogger:(id __attribute__((unused)))logger { - int32_t count = 0; - count = OSAtomicIncrement32(&_atomicLoggerCount); - NSAssert(count <= 1 || _mode == DDDispatchQueueLogFormatterModeShareble, @"Can't reuse formatter with multiple loggers in non-shareable mode."); -} - -- (void)willRemoveFromLogger:(id __attribute__((unused)))logger { - OSAtomicDecrement32(&_atomicLoggerCount); -} - -@end diff --git a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDMultiFormatter.h b/Example/Pods/CocoaLumberjack/Classes/Extensions/DDMultiFormatter.h deleted file mode 100644 index 1d6ceea..0000000 --- a/Example/Pods/CocoaLumberjack/Classes/Extensions/DDMultiFormatter.h +++ /dev/null @@ -1,56 +0,0 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -#import - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif - -#import "DDLog.h" - -/** - * This formatter can be used to chain different formatters together. - * The log message will processed in the order of the formatters added. - **/ -@interface DDMultiFormatter : NSObject - -/** - * Array of chained formatters - */ -@property (readonly) NSArray *formatters; - -/** - * Add a new formatter - */ -- (void)addFormatter:(id)formatter; - -/** - * Remove a formatter - */ -- (void)removeFormatter:(id)formatter; - -/** - * Remove all existing formatters - */ -- (void)removeAllFormatters; - -/** - * Check if a certain formatter is used - */ -- (BOOL)isFormattingWithFormatter:(id)formatter; - -@end diff --git a/Example/Pods/CocoaLumberjack/Framework/Lumberjack/CocoaLumberjack.modulemap b/Example/Pods/CocoaLumberjack/Framework/Lumberjack/CocoaLumberjack.modulemap deleted file mode 100644 index 032ae65..0000000 --- a/Example/Pods/CocoaLumberjack/Framework/Lumberjack/CocoaLumberjack.modulemap +++ /dev/null @@ -1,36 +0,0 @@ -framework module CocoaLumberjack { - umbrella header "CocoaLumberjack.h" - - export * - module * { export * } - - textual header "DDLogMacros.h" - - exclude header "DDLog+LOGV.h" - exclude header "DDLegacyMacros.h" - - explicit module DDContextFilterLogFormatter { - header "DDContextFilterLogFormatter.h" - export * - } - - explicit module DDDispatchQueueLogFormatter { - header "DDDispatchQueueLogFormatter.h" - export * - } - - explicit module DDMultiFormatter { - header "DDMultiFormatter.h" - export * - } - - explicit module DDASLLogCapture { - header "DDASLLogCapture.h" - export * - } - - explicit module DDAbstractDatabaseLogger { - header "DDAbstractDatabaseLogger.h" - export * - } -} diff --git a/Example/Pods/CocoaLumberjack/LICENSE.txt b/Example/Pods/CocoaLumberjack/LICENSE.txt index 9c29fac..66a942c 100644 --- a/Example/Pods/CocoaLumberjack/LICENSE.txt +++ b/Example/Pods/CocoaLumberjack/LICENSE.txt @@ -1,6 +1,6 @@ Software License Agreement (BSD License) -Copyright (c) 2010-2015, Deusty, LLC +Copyright (c) 2010, Deusty, LLC All rights reserved. Redistribution and use of this software in source and binary forms, diff --git a/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogCapture.h b/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogCapture.h new file mode 100644 index 0000000..53dc6b0 --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogCapture.h @@ -0,0 +1,23 @@ +// +// DDASLLogCapture.h +// Lumberjack +// +// Created by Dario Ahdoot on 3/17/14. +// +// + +#import "DDASLLogger.h" + +@protocol DDLogger; + +@interface DDASLLogCapture : NSObject + ++ (void)start; ++ (void)stop; + +// Default log level: LOG_LEVEL_VERBOSE (i.e. capture all ASL messages). ++ (int)captureLogLevel; ++ (void)setCaptureLogLevel:(int)LOG_LEVEL_XXX; + +@end + diff --git a/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogCapture.m b/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogCapture.m new file mode 100644 index 0000000..52bfc1b --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogCapture.m @@ -0,0 +1,188 @@ +// +// DDASLLogCapture.m +// Lumberjack +// +// Created by Dario Ahdoot on 3/17/14. +// +// + +#import "DDASLLogCapture.h" +#import "DDLog.h" + +#include +#include +#include +#include + +static BOOL _cancel = YES; +static int _captureLogLevel = LOG_LEVEL_VERBOSE; + +@implementation DDASLLogCapture + ++ (void)start +{ + // Ignore subsequent calls + if (!_cancel) + return; + + _cancel = NO; + + dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) + { + [DDASLLogCapture captureAslLogs]; + }); +} + ++ (void)stop +{ + _cancel = YES; +} + ++ (int)captureLogLevel +{ + return _captureLogLevel; +} + ++ (void)setCaptureLogLevel:(int)LOG_LEVEL_XXX +{ + _captureLogLevel = LOG_LEVEL_XXX; +} + +# pragma mark - Private methods + ++ (void)configureAslQuery:(aslmsg)query +{ + const char param[] = "7"; // ASL_LEVEL_DEBUG, which is everything. We'll rely on regular DDlog log level to filter + asl_set_query(query, ASL_KEY_LEVEL, param, ASL_QUERY_OP_LESS_EQUAL | ASL_QUERY_OP_NUMERIC); + +#if !TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR + int processId = [[NSProcessInfo processInfo] processIdentifier]; + char pid[16]; + sprintf(pid, "%d", processId); + asl_set_query(query, ASL_KEY_PID, pid, ASL_QUERY_OP_EQUAL | ASL_QUERY_OP_NUMERIC); +#endif +} + ++ (void)aslMessageRecieved:(aslmsg)msg +{ + // NSString * sender = [NSString stringWithCString:asl_get(msg, ASL_KEY_SENDER) encoding:NSUTF8StringEncoding]; + NSString * message = [NSString stringWithCString:asl_get(msg, ASL_KEY_MSG) encoding:NSUTF8StringEncoding]; + NSString * level = [NSString stringWithCString:asl_get(msg, ASL_KEY_LEVEL) encoding:NSUTF8StringEncoding]; + NSString * secondsStr = [NSString stringWithCString:asl_get(msg, ASL_KEY_TIME) encoding:NSUTF8StringEncoding]; + NSString * nanoStr = [NSString stringWithCString:asl_get(msg, ASL_KEY_TIME_NSEC) encoding:NSUTF8StringEncoding]; + + NSTimeInterval seconds = [secondsStr doubleValue]; + NSTimeInterval nanoSeconds = [nanoStr doubleValue]; + NSTimeInterval totalSeconds = seconds + (nanoSeconds / 1e9); + + NSDate * timeStamp = [NSDate dateWithTimeIntervalSince1970:totalSeconds]; + + int flag; + BOOL async; + switch([level intValue]) + { + // By default all NSLog's with a ASL_LEVEL_WARNING level + case ASL_LEVEL_EMERG : + case ASL_LEVEL_ALERT : + case ASL_LEVEL_CRIT : flag = LOG_FLAG_ERROR; async = LOG_ASYNC_ERROR; break; + case ASL_LEVEL_ERR : flag = LOG_FLAG_WARN; async = LOG_ASYNC_WARN; break; + case ASL_LEVEL_WARNING : flag = LOG_FLAG_INFO; async = LOG_ASYNC_INFO; break; + case ASL_LEVEL_NOTICE : flag = LOG_FLAG_DEBUG; async = LOG_ASYNC_DEBUG; break; + case ASL_LEVEL_INFO : + case ASL_LEVEL_DEBUG : + default : flag = LOG_FLAG_VERBOSE; async = LOG_ASYNC_VERBOSE; break; + } + + if (!(_captureLogLevel & flag)) + return; + + DDLogMessage * logMessage = [[DDLogMessage alloc]initWithLogMsg:message + level:_captureLogLevel + flag:flag + context:0 + file:"DDASLLogCapture" + function:0 + line:0 + tag:nil + options:0 + timestamp:timeStamp]; + + [DDLog log:async message:logMessage]; +} + ++ (void)captureAslLogs +{ + @autoreleasepool + { + /* + We use ASL_KEY_MSG_ID to see each message once, but there's no + obvious way to get the "next" ID. To bootstrap the process, we'll + search by timestamp until we've seen a message. + */ + + struct timeval timeval = { .tv_sec = 0 }; + gettimeofday(&timeval, NULL); + unsigned long long startTime = timeval.tv_sec; + __block unsigned long long lastSeenID = 0; + + /* + syslogd posts kNotifyASLDBUpdate (com.apple.system.logger.message) + through the notify API when it saves messages to the ASL database. + There is some coalescing - currently it is sent at most twice per + second - but there is no documented guarantee about this. In any + case, there may be multiple messages per notification. + + Notify notifications don't carry any payload, so we need to search + for the messages. + */ + int notifyToken = 0; // Can be used to unregister with notify_cancel(). + notify_register_dispatch(kNotifyASLDBUpdate, ¬ifyToken, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(int token) + { + // At least one message has been posted; build a search query. + @autoreleasepool + { + aslmsg query = asl_new(ASL_TYPE_QUERY); + char stringValue[64]; + if (lastSeenID > 0) + { + snprintf(stringValue, sizeof stringValue, "%llu", lastSeenID); + asl_set_query(query, ASL_KEY_MSG_ID, stringValue, ASL_QUERY_OP_GREATER | ASL_QUERY_OP_NUMERIC); + } + else + { + snprintf(stringValue, sizeof stringValue, "%llu", startTime); + asl_set_query(query, ASL_KEY_TIME, stringValue, ASL_QUERY_OP_GREATER_EQUAL | ASL_QUERY_OP_NUMERIC); + } + [DDASLLogCapture configureAslQuery:query]; + + // Iterate over new messages. + aslmsg msg; + aslresponse response = asl_search(NULL, query); +#if defined(__IPHONE_8_0) || defined(__MAC_10_10) + while ((msg = asl_next(response))) +#else + while ((msg = aslresponse_next(response))) +#endif + { + [DDASLLogCapture aslMessageRecieved:msg]; + + // Keep track of which messages we've seen. + lastSeenID = atoll(asl_get(msg, ASL_KEY_MSG_ID)); + } +#if defined(__IPHONE_8_0) || defined(__MAC_10_10) + asl_release(response); +#else + aslresponse_free(response); +#endif + if(_cancel) + { + notify_cancel(notifyToken); + return; + } + free(query); + } + }); + } +} + +@end \ No newline at end of file diff --git a/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogger.h b/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogger.h new file mode 100755 index 0000000..a55eb6f --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogger.h @@ -0,0 +1,37 @@ +#import + +#import "DDLog.h" + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted + * + * + * This class provides a logger for the Apple System Log facility. + * + * As described in the "Getting Started" page, + * the traditional NSLog() function directs it's output to two places: + * + * - Apple System Log + * - StdErr (if stderr is a TTY) so log statements show up in Xcode console + * + * To duplicate NSLog() functionality you can simply add this logger and a tty logger. + * However, if you instead choose to use file logging (for faster performance), + * you may choose to use a file logger and a tty logger. +**/ + +@interface DDASLLogger : DDAbstractLogger + ++ (instancetype)sharedInstance; + +// Inherited from DDAbstractLogger + +// - (id )logFormatter; +// - (void)setLogFormatter:(id )formatter; + +@end diff --git a/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogger.m b/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogger.m new file mode 100755 index 0000000..020acb2 --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Lumberjack/DDASLLogger.m @@ -0,0 +1,95 @@ +#import "DDASLLogger.h" +#import +#import + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted +**/ + +#if ! __has_feature(objc_arc) +#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +static DDASLLogger *sharedInstance; + +@implementation DDASLLogger +{ + aslclient client; +} + ++ (instancetype)sharedInstance +{ + static dispatch_once_t DDASLLoggerOnceToken; + dispatch_once(&DDASLLoggerOnceToken, ^{ + sharedInstance = [[[self class] alloc] init]; + }); + + return sharedInstance; +} + +- (id)init +{ + if (sharedInstance != nil) + { + return nil; + } + + if ((self = [super init])) + { + // A default asl client is provided for the main thread, + // but background threads need to create their own client. + + client = asl_open(NULL, "com.apple.console", 0); + } + return self; +} + +- (void)logMessage:(DDLogMessage *)logMessage +{ + // Skip captured log messages. + if (strcmp(logMessage->file, "DDASLLogCapture") == 0) + return; + + NSString *logMsg = logMessage->logMsg; + + if (formatter) + { + logMsg = [formatter formatLogMessage:logMessage]; + } + + if (logMsg) + { + const char *msg = [logMsg UTF8String]; + + int aslLogLevel; + switch (logMessage->logFlag) + { + // Note: By default ASL will filter anything above level 5 (Notice). + // So our mappings shouldn't go above that level. + case LOG_FLAG_ERROR : aslLogLevel = ASL_LEVEL_CRIT; break; + case LOG_FLAG_WARN : aslLogLevel = ASL_LEVEL_ERR; break; + case LOG_FLAG_INFO : aslLogLevel = ASL_LEVEL_WARNING; break; // Regular NSLog's level + case LOG_FLAG_DEBUG : + case LOG_FLAG_VERBOSE : + default : aslLogLevel = ASL_LEVEL_NOTICE; break; + } + + aslmsg m = asl_new(ASL_TYPE_MSG); + asl_set(m, ASL_KEY_READ_UID, "501"); + asl_log(client, m, aslLogLevel, "%s", msg); + asl_free(m); + } +} + +- (NSString *)loggerName +{ + return @"cocoa.lumberjack.aslLogger"; +} + +@end diff --git a/Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.h b/Example/Pods/CocoaLumberjack/Lumberjack/DDAbstractDatabaseLogger.h similarity index 68% rename from Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.h rename to Example/Pods/CocoaLumberjack/Lumberjack/DDAbstractDatabaseLogger.h index aad3666..4e0c33c 100644 --- a/Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.h +++ b/Example/Pods/CocoaLumberjack/Lumberjack/DDAbstractDatabaseLogger.h @@ -1,123 +1,102 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif +#import #import "DDLog.h" /** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted + * + * * This class provides an abstract implementation of a database logger. - * + * * That is, it provides the base implementation for a database logger to build atop of. * All that is needed for a concrete database logger is to extend this class * and override the methods in the implementation file that are prefixed with "db_". - **/ +**/ + @interface DDAbstractDatabaseLogger : DDAbstractLogger { - @protected - NSUInteger _saveThreshold; - NSTimeInterval _saveInterval; - NSTimeInterval _maxAge; - NSTimeInterval _deleteInterval; - BOOL _deleteOnEverySave; + NSUInteger saveThreshold; + NSTimeInterval saveInterval; + NSTimeInterval maxAge; + NSTimeInterval deleteInterval; + BOOL deleteOnEverySave; - BOOL _saveTimerSuspended; - NSUInteger _unsavedCount; - dispatch_time_t _unsavedTime; - dispatch_source_t _saveTimer; - dispatch_time_t _lastDeleteTime; - dispatch_source_t _deleteTimer; + BOOL saveTimerSuspended; + NSUInteger unsavedCount; + dispatch_time_t unsavedTime; + dispatch_source_t saveTimer; + dispatch_time_t lastDeleteTime; + dispatch_source_t deleteTimer; } /** * Specifies how often to save the data to disk. * Since saving is an expensive operation (disk io) it is not done after every log statement. * These properties allow you to configure how/when the logger saves to disk. - * + * * A save is done when either (whichever happens first): - * + * * - The number of unsaved log entries reaches saveThreshold * - The amount of time since the oldest unsaved log entry was created reaches saveInterval - * + * * You can optionally disable the saveThreshold by setting it to zero. * If you disable the saveThreshold you are entirely dependent on the saveInterval. - * + * * You can optionally disable the saveInterval by setting it to zero (or a negative value). * If you disable the saveInterval you are entirely dependent on the saveThreshold. - * + * * It's not wise to disable both saveThreshold and saveInterval. - * + * * The default saveThreshold is 500. * The default saveInterval is 60 seconds. - **/ +**/ @property (assign, readwrite) NSUInteger saveThreshold; - -/** - * See the description for the `saveThreshold` property - */ @property (assign, readwrite) NSTimeInterval saveInterval; /** * It is likely you don't want the log entries to persist forever. * Doing so would allow the database to grow infinitely large over time. - * + * * The maxAge property provides a way to specify how old a log statement can get * before it should get deleted from the database. - * + * * The deleteInterval specifies how often to sweep for old log entries. * Since deleting is an expensive operation (disk io) is is done on a fixed interval. - * + * * An alternative to the deleteInterval is the deleteOnEverySave option. * This specifies that old log entries should be deleted during every save operation. - * + * * You can optionally disable the maxAge by setting it to zero (or a negative value). * If you disable the maxAge then old log statements are not deleted. - * + * * You can optionally disable the deleteInterval by setting it to zero (or a negative value). - * + * * If you disable both deleteInterval and deleteOnEverySave then old log statements are not deleted. - * + * * It's not wise to enable both deleteInterval and deleteOnEverySave. - * + * * The default maxAge is 7 days. * The default deleteInterval is 5 minutes. * The default deleteOnEverySave is NO. - **/ +**/ @property (assign, readwrite) NSTimeInterval maxAge; - -/** - * See the description for the `maxAge` property - */ @property (assign, readwrite) NSTimeInterval deleteInterval; - -/** - * See the description for the `maxAge` property - */ @property (assign, readwrite) BOOL deleteOnEverySave; /** * Forces a save of any pending log entries (flushes log entries to disk). - **/ +**/ - (void)savePendingLogEntries; /** * Removes any log entries that are older than maxAge. - **/ +**/ - (void)deleteOldLogEntries; @end diff --git a/Example/Pods/CocoaLumberjack/Lumberjack/DDAbstractDatabaseLogger.m b/Example/Pods/CocoaLumberjack/Lumberjack/DDAbstractDatabaseLogger.m new file mode 100644 index 0000000..05fcbcb --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Lumberjack/DDAbstractDatabaseLogger.m @@ -0,0 +1,727 @@ +#import "DDAbstractDatabaseLogger.h" +#import + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted +**/ + +#if ! __has_feature(objc_arc) +#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +@interface DDAbstractDatabaseLogger () +- (void)destroySaveTimer; +- (void)destroyDeleteTimer; +@end + +#pragma mark - + +@implementation DDAbstractDatabaseLogger + +- (id)init +{ + if ((self = [super init])) + { + saveThreshold = 500; + saveInterval = 60; // 60 seconds + maxAge = (60 * 60 * 24 * 7); // 7 days + deleteInterval = (60 * 5); // 5 minutes + } + return self; +} + +- (void)dealloc +{ + [self destroySaveTimer]; + [self destroyDeleteTimer]; + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Override Me +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)db_log:(DDLogMessage *)logMessage +{ + // Override me and add your implementation. + // + // Return YES if an item was added to the buffer. + // Return NO if the logMessage was ignored. + + return NO; +} + +- (void)db_save +{ + // Override me and add your implementation. +} + +- (void)db_delete +{ + // Override me and add your implementation. +} + +- (void)db_saveAndDelete +{ + // Override me and add your implementation. +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Private API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)performSaveAndSuspendSaveTimer +{ + if (unsavedCount > 0) + { + if (deleteOnEverySave) + [self db_saveAndDelete]; + else + [self db_save]; + } + + unsavedCount = 0; + unsavedTime = 0; + + if (saveTimer && !saveTimerSuspended) + { + dispatch_suspend(saveTimer); + saveTimerSuspended = YES; + } +} + +- (void)performDelete +{ + if (maxAge > 0.0) + { + [self db_delete]; + + lastDeleteTime = dispatch_time(DISPATCH_TIME_NOW, 0); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Timers +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)destroySaveTimer +{ + if (saveTimer) + { + dispatch_source_cancel(saveTimer); + if (saveTimerSuspended) + { + // Must resume a timer before releasing it (or it will crash) + dispatch_resume(saveTimer); + saveTimerSuspended = NO; + } + #if !OS_OBJECT_USE_OBJC + dispatch_release(saveTimer); + #endif + saveTimer = NULL; + } +} + +- (void)updateAndResumeSaveTimer +{ + if ((saveTimer != NULL) && (saveInterval > 0.0) && (unsavedTime > 0.0)) + { + uint64_t interval = (uint64_t)(saveInterval * NSEC_PER_SEC); + dispatch_time_t startTime = dispatch_time(unsavedTime, interval); + + dispatch_source_set_timer(saveTimer, startTime, interval, 1.0); + + if (saveTimerSuspended) + { + dispatch_resume(saveTimer); + saveTimerSuspended = NO; + } + } +} + +- (void)createSuspendedSaveTimer +{ + if ((saveTimer == NULL) && (saveInterval > 0.0)) + { + saveTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, loggerQueue); + + dispatch_source_set_event_handler(saveTimer, ^{ @autoreleasepool { + + [self performSaveAndSuspendSaveTimer]; + + }}); + + saveTimerSuspended = YES; + } +} + +- (void)destroyDeleteTimer +{ + if (deleteTimer) + { + dispatch_source_cancel(deleteTimer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(deleteTimer); + #endif + deleteTimer = NULL; + } +} + +- (void)updateDeleteTimer +{ + if ((deleteTimer != NULL) && (deleteInterval > 0.0) && (maxAge > 0.0)) + { + uint64_t interval = (uint64_t)(deleteInterval * NSEC_PER_SEC); + dispatch_time_t startTime; + + if (lastDeleteTime > 0) + startTime = dispatch_time(lastDeleteTime, interval); + else + startTime = dispatch_time(DISPATCH_TIME_NOW, interval); + + dispatch_source_set_timer(deleteTimer, startTime, interval, 1.0); + } +} + +- (void)createAndStartDeleteTimer +{ + if ((deleteTimer == NULL) && (deleteInterval > 0.0) && (maxAge > 0.0)) + { + deleteTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, loggerQueue); + + if (deleteTimer != NULL) { + dispatch_source_set_event_handler(deleteTimer, ^{ @autoreleasepool { + + [self performDelete]; + + }}); + + [self updateDeleteTimer]; + + if (deleteTimer != NULL) dispatch_resume(deleteTimer); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSUInteger)saveThreshold +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSUInteger result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = saveThreshold; + }); + }); + + return result; +} + +- (void)setSaveThreshold:(NSUInteger)threshold +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (saveThreshold != threshold) + { + saveThreshold = threshold; + + // Since the saveThreshold has changed, + // we check to see if the current unsavedCount has surpassed the new threshold. + // + // If it has, we immediately save the log. + + if ((unsavedCount >= saveThreshold) && (saveThreshold > 0)) + { + [self performSaveAndSuspendSaveTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (NSTimeInterval)saveInterval +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSTimeInterval result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = saveInterval; + }); + }); + + return result; +} + +- (void)setSaveInterval:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ @autoreleasepool { + + // C99 recommended floating point comparison macro + // Read: isLessThanOrGreaterThan(floatA, floatB) + + if (/* saveInterval != interval */ islessgreater(saveInterval, interval)) + { + saveInterval = interval; + + // There are several cases we need to handle here. + // + // 1. If the saveInterval was previously enabled and it just got disabled, + // then we need to stop the saveTimer. (And we might as well release it.) + // + // 2. If the saveInterval was previously disabled and it just got enabled, + // then we need to setup the saveTimer. (Plus we might need to do an immediate save.) + // + // 3. If the saveInterval increased, then we need to reset the timer so that it fires at the later date. + // + // 4. If the saveInterval decreased, then we need to reset the timer so that it fires at an earlier date. + // (Plus we might need to do an immediate save.) + + if (saveInterval > 0.0) + { + if (saveTimer == NULL) + { + // Handles #2 + // + // Since the saveTimer uses the unsavedTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self createSuspendedSaveTimer]; + [self updateAndResumeSaveTimer]; + } + else + { + // Handles #3 + // Handles #4 + // + // Since the saveTimer uses the unsavedTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self updateAndResumeSaveTimer]; + } + } + else if (saveTimer) + { + // Handles #1 + + [self destroySaveTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (NSTimeInterval)maxAge +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSTimeInterval result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = maxAge; + }); + }); + + return result; +} + +- (void)setMaxAge:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ @autoreleasepool { + + // C99 recommended floating point comparison macro + // Read: isLessThanOrGreaterThan(floatA, floatB) + + if (/* maxAge != interval */ islessgreater(maxAge, interval)) + { + NSTimeInterval oldMaxAge = maxAge; + NSTimeInterval newMaxAge = interval; + + maxAge = interval; + + // There are several cases we need to handle here. + // + // 1. If the maxAge was previously enabled and it just got disabled, + // then we need to stop the deleteTimer. (And we might as well release it.) + // + // 2. If the maxAge was previously disabled and it just got enabled, + // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) + // + // 3. If the maxAge was increased, + // then we don't need to do anything. + // + // 4. If the maxAge was decreased, + // then we should do an immediate delete. + + BOOL shouldDeleteNow = NO; + + if (oldMaxAge > 0.0) + { + if (newMaxAge <= 0.0) + { + // Handles #1 + + [self destroyDeleteTimer]; + } + else if (oldMaxAge > newMaxAge) + { + // Handles #4 + shouldDeleteNow = YES; + } + } + else if (newMaxAge > 0.0) + { + // Handles #2 + shouldDeleteNow = YES; + } + + if (shouldDeleteNow) + { + [self performDelete]; + + if (deleteTimer) + [self updateDeleteTimer]; + else + [self createAndStartDeleteTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (NSTimeInterval)deleteInterval +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSTimeInterval result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = deleteInterval; + }); + }); + + return result; +} + +- (void)setDeleteInterval:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ @autoreleasepool { + + // C99 recommended floating point comparison macro + // Read: isLessThanOrGreaterThan(floatA, floatB) + + if (/* deleteInterval != interval */ islessgreater(deleteInterval, interval)) + { + deleteInterval = interval; + + // There are several cases we need to handle here. + // + // 1. If the deleteInterval was previously enabled and it just got disabled, + // then we need to stop the deleteTimer. (And we might as well release it.) + // + // 2. If the deleteInterval was previously disabled and it just got enabled, + // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) + // + // 3. If the deleteInterval increased, then we need to reset the timer so that it fires at the later date. + // + // 4. If the deleteInterval decreased, then we need to reset the timer so that it fires at an earlier date. + // (Plus we might need to do an immediate delete.) + + if (deleteInterval > 0.0) + { + if (deleteTimer == NULL) + { + // Handles #2 + // + // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, + // if a delete is needed the timer will fire immediately. + + [self createAndStartDeleteTimer]; + } + else + { + // Handles #3 + // Handles #4 + // + // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self updateDeleteTimer]; + } + } + else if (deleteTimer) + { + // Handles #1 + + [self destroyDeleteTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (BOOL)deleteOnEverySave +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block BOOL result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = deleteOnEverySave; + }); + }); + + return result; +} + +- (void)setDeleteOnEverySave:(BOOL)flag +{ + dispatch_block_t block = ^{ + + deleteOnEverySave = flag; + }; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Public API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)savePendingLogEntries +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [self performSaveAndSuspendSaveTimer]; + }}; + + if ([self isOnInternalLoggerQueue]) + block(); + else + dispatch_async(loggerQueue, block); +} + +- (void)deleteOldLogEntries +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [self performDelete]; + }}; + + if ([self isOnInternalLoggerQueue]) + block(); + else + dispatch_async(loggerQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark DDLogger +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)didAddLogger +{ + // If you override me be sure to invoke [super didAddLogger]; + + [self createSuspendedSaveTimer]; + + [self createAndStartDeleteTimer]; +} + +- (void)willRemoveLogger +{ + // If you override me be sure to invoke [super willRemoveLogger]; + + [self performSaveAndSuspendSaveTimer]; + + [self destroySaveTimer]; + [self destroyDeleteTimer]; +} + +- (void)logMessage:(DDLogMessage *)logMessage +{ + if ([self db_log:logMessage]) + { + BOOL firstUnsavedEntry = (++unsavedCount == 1); + + if ((unsavedCount >= saveThreshold) && (saveThreshold > 0)) + { + [self performSaveAndSuspendSaveTimer]; + } + else if (firstUnsavedEntry) + { + unsavedTime = dispatch_time(DISPATCH_TIME_NOW, 0); + [self updateAndResumeSaveTimer]; + } + } +} + +- (void)flush +{ + // This method is invoked by DDLog's flushLog method. + // + // It is called automatically when the application quits, + // or if the developer invokes DDLog's flushLog method prior to crashing or something. + + [self performSaveAndSuspendSaveTimer]; +} + +@end diff --git a/Example/Pods/CocoaLumberjack/Lumberjack/DDAssert.h b/Example/Pods/CocoaLumberjack/Lumberjack/DDAssert.h new file mode 100644 index 0000000..4bad8e2 --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Lumberjack/DDAssert.h @@ -0,0 +1,16 @@ +// +// DDAssert.h +// CocoaLumberjack +// +// Created by Ernesto Rivera on 2014/07/07. +// +// + +#import "DDLog.h" + +#define DDAssert(condition, frmt, ...) if (!(condition)) { \ + NSString * description = [NSString stringWithFormat:frmt, ##__VA_ARGS__]; \ + DDLogError(@"%@", description); \ + NSAssert(NO, description); } +#define DDAssertCondition(condition) DDAssert(condition, @"Condition not satisfied: %s", #condition) + diff --git a/Example/Pods/CocoaLumberjack/Classes/DDFileLogger.h b/Example/Pods/CocoaLumberjack/Lumberjack/DDFileLogger.h similarity index 51% rename from Example/Pods/CocoaLumberjack/Classes/DDFileLogger.h rename to Example/Pods/CocoaLumberjack/Lumberjack/DDFileLogger.h index f0bfdb6..b319c51 100644 --- a/Example/Pods/CocoaLumberjack/Classes/DDFileLogger.h +++ b/Example/Pods/CocoaLumberjack/Lumberjack/DDFileLogger.h @@ -1,74 +1,63 @@ -// Software License Agreement (BSD License) -// -// Copyright (c) 2010-2015, Deusty, LLC -// All rights reserved. -// -// Redistribution and use of this software in source and binary forms, -// with or without modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Neither the name of Deusty nor the names of its contributors may be used -// to endorse or promote products derived from this software without specific -// prior written permission of Deusty, LLC. - -// Disable legacy macros -#ifndef DD_LEGACY_MACROS - #define DD_LEGACY_MACROS 0 -#endif - +#import #import "DDLog.h" @class DDLogFileInfo; /** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted + * + * * This class provides a logger to write log statements to a file. - **/ +**/ // Default configuration and safety/sanity values. -// -// maximumFileSize -> kDDDefaultLogMaxFileSize -// rollingFrequency -> kDDDefaultLogRollingFrequency -// maximumNumberOfLogFiles -> kDDDefaultLogMaxNumLogFiles -// logFilesDiskQuota -> kDDDefaultLogFilesDiskQuota -// +// +// maximumFileSize -> DEFAULT_LOG_MAX_FILE_SIZE +// rollingFrequency -> DEFAULT_LOG_ROLLING_FREQUENCY +// maximumNumberOfLogFiles -> DEFAULT_LOG_MAX_NUM_LOG_FILES +// logFilesDiskQuota -> DEFAULT_LOG_FILES_DISK_QUOTA +// // You should carefully consider the proper configuration values for your application. -extern unsigned long long const kDDDefaultLogMaxFileSize; -extern NSTimeInterval const kDDDefaultLogRollingFrequency; -extern NSUInteger const kDDDefaultLogMaxNumLogFiles; -extern unsigned long long const kDDDefaultLogFilesDiskQuota; +#define DEFAULT_LOG_MAX_FILE_SIZE (1024 * 1024) // 1 MB +#define DEFAULT_LOG_ROLLING_FREQUENCY (60 * 60 * 24) // 24 Hours +#define DEFAULT_LOG_MAX_NUM_LOG_FILES (5) // 5 Files +#define DEFAULT_LOG_FILES_DISK_QUOTA (20 * 1024 * 1024) // 20 MB //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -/** - * The LogFileManager protocol is designed to allow you to control all aspects of your log files. - * - * The primary purpose of this is to allow you to do something with the log files after they have been rolled. - * Perhaps you want to compress them to save disk space. - * Perhaps you want to upload them to an FTP server. - * Perhaps you want to run some analytics on the file. - * - * A default LogFileManager is, of course, provided. - * The default LogFileManager simply deletes old log files according to the maximumNumberOfLogFiles property. - * - * This protocol provides various methods to fetch the list of log files. - * - * There are two variants: sorted and unsorted. - * If sorting is not necessary, the unsorted variant is obviously faster. - * The sorted variant will return an array sorted by when the log files were created, - * with the most recently created log file at index 0, and the oldest log file at the end of the array. - * - * You can fetch only the log file paths (full path including name), log file names (name only), - * or an array of `DDLogFileInfo` objects. - * The `DDLogFileInfo` class is documented below, and provides a handy wrapper that - * gives you easy access to various file attributes such as the creation date or the file size. - */ +// The LogFileManager protocol is designed to allow you to control all aspects of your log files. +// +// The primary purpose of this is to allow you to do something with the log files after they have been rolled. +// Perhaps you want to compress them to save disk space. +// Perhaps you want to upload them to an FTP server. +// Perhaps you want to run some analytics on the file. +// +// A default LogFileManager is, of course, provided. +// The default LogFileManager simply deletes old log files according to the maximumNumberOfLogFiles property. +// +// This protocol provides various methods to fetch the list of log files. +// +// There are two variants: sorted and unsorted. +// If sorting is not necessary, the unsorted variant is obviously faster. +// The sorted variant will return an array sorted by when the log files were created, +// with the most recently created log file at index 0, and the oldest log file at the end of the array. +// +// You can fetch only the log file paths (full path including name), log file names (name only), +// or an array of DDLogFileInfo objects. +// The DDLogFileInfo class is documented below, and provides a handy wrapper that +// gives you easy access to various file attributes such as the creation date or the file size. + @protocol DDLogFileManager @required @@ -79,9 +68,9 @@ extern unsigned long long const kDDDefaultLogFilesDiskQuota; * For example, if this property is set to 3, * then the LogFileManager will only keep 3 archived log files (plus the current active log file) on disk. * Once the active log file is rolled/archived, then the oldest of the existing 3 rolled/archived log files is deleted. - * + * * You may optionally disable this option by setting it to zero. - **/ +**/ @property (readwrite, assign, atomic) NSUInteger maximumNumberOfLogFiles; /** @@ -89,75 +78,30 @@ extern unsigned long long const kDDDefaultLogFilesDiskQuota; * be deleted. * * You may optionally disable this option by setting it to zero. - **/ +**/ @property (readwrite, assign, atomic) unsigned long long logFilesDiskQuota; // Public methods -/** - * Returns the logs directory (path) - */ - (NSString *)logsDirectory; -/** - * Returns an array of `NSString` objects, - * each of which is the filePath to an existing log file on disk. - **/ - (NSArray *)unsortedLogFilePaths; - -/** - * Returns an array of `NSString` objects, - * each of which is the fileName of an existing log file on disk. - **/ - (NSArray *)unsortedLogFileNames; - -/** - * Returns an array of `DDLogFileInfo` objects, - * each representing an existing log file on disk, - * and containing important information about the log file such as it's modification date and size. - **/ - (NSArray *)unsortedLogFileInfos; -/** - * Just like the `unsortedLogFilePaths` method, but sorts the array. - * The items in the array are sorted by creation date. - * The first item in the array will be the most recently created log file. - **/ - (NSArray *)sortedLogFilePaths; - -/** - * Just like the `unsortedLogFileNames` method, but sorts the array. - * The items in the array are sorted by creation date. - * The first item in the array will be the most recently created log file. - **/ - (NSArray *)sortedLogFileNames; - -/** - * Just like the `unsortedLogFileInfos` method, but sorts the array. - * The items in the array are sorted by creation date. - * The first item in the array will be the most recently created log file. - **/ - (NSArray *)sortedLogFileInfos; // Private methods (only to be used by DDFileLogger) -/** - * Generates a new unique log file path, and creates the corresponding log file. - **/ - (NSString *)createNewLogFile; @optional // Notifications from DDFileLogger -/** - * Called when a log file was archieved - */ - (void)didArchiveLogFile:(NSString *)logFilePath; - -/** - * Called when the roll action was executed and the log was archieved - */ - (void)didRollAndArchiveLogFile:(NSString *)logFilePath; @end @@ -168,29 +112,29 @@ extern unsigned long long const kDDDefaultLogFilesDiskQuota; /** * Default log file manager. - * + * * All log files are placed inside the logsDirectory. * If a specific logsDirectory isn't specified, the default directory is used. - * On Mac, this is in `~/Library/Logs/`. - * On iPhone, this is in `~/Library/Caches/Logs`. - * - * Log files are named `" Y|uOf6*!)w#ifSh)%a;#VigYEY+k%97TlOkwA>sfxjM zDJ|?GwW7hvHM+It%S6-UPV3c*o}ddz4~bQvGB|%RstZWJ;)FB2#_i;8OZ_(WJG`ZS z%Ahf$#?{nSHrF=PH_fkId~53WsXwItnEF%d&)lvPe1C@Y55_b$EUsy6UJ?BM#8D&Z z--j()Tw!%i8H6v5u3S`8J-DuBQB8ew@XMCqm)pLgCX4V%BWCp_YguurR4%%tQQfdo z8!)2sh7Gm>71h+RjHsevsi^vfRW3f|4J-YkpvdWm8g^QB%i_A)D*S$drq?h?dT~$q zdf>NZ1l!wJ=qs8gHQwO*X8an19ll<^-o9dAA75WzKW>-oJ2Qg~HL$L-sVUfS_O`#* z&aJI!41UoTY2>EGl~pxb8s2Ujzd(#E)qG{XLGq=VZ?F%$>BU4?)#@AS8|E7xJ>~mG zayj_5MEHFy{3NM^|KI(3ovbN?2DDU#ehtl;%j#>Y%bJ@TYv;6}mdh%e2G>_LRM%8X zze{EbrhNSBqdc)IhEm&kO!UoCvsCj{_-6YmeRF(OzG`2MZ!QrUiO@uZW+E&l!ZIQ( zC&CIMtW+~R(35?GgG2qKcMukz&QgsLRV>x0sOqJf?|g;RsHIwq)Rn0!vsCkK@@>|z zRAam=mTL4UnwDyesA8!`MKvwe7*WMijf!eosxhL9r5Y90v{Yk66-zZL8eXdTj`%+D zeaej6&*`{rx24)yblgTQ)qFqr{y5cA&3Dvy%y-=Pr|&P{-)ZQnxSX(t2xk*PBmyA< z7H{iR3|Hw|g0EaYT%(q1!RdUJ)}`7BL}g1gF-lbZwS@B&PUY(~{L)Jlfm&6+PD7eA z=dVa}Dyk{X8Bs-=Q&CN6&WI|~oQi5nb4FB==2SG4<|_J>wDx6wm!Pbsp|x-1mkDYr zDx=C4H6p61sEjCE)QG62qB5dvQ6r+XsNsuVX~Fw4Y2o`aX(zicgNt5i_h8U=0b`{P zWayVkLpbflwl5|SV+;Ms9s1(8!tvwzK{xTyr`(Ag%l&ISV&P(O$#YT zRI!kvqM8;`jHqHEMMe7rx9P3vmFaWRnVT}{xG57Jx9#;&jLmf1Mtu|`y*^#KDI?ef zyWkL-{DVHa{Eu2ms>ppRsyg@mgB4Dra({<3kx% z(ex;)b6>iW&WOr$KX@gbifYPzMpTjeR8&*$Gop&zr=ps2pAl8$J{1k;zJHs4yZ>fp z+};`)x9yetchhkjmHYl({)bML`~HXhkNEfaANB9`Kjz;@gk40qhY0r);XWeVPlN}E z@SvLEirLw(mk(EY?uR~tdDrR0J|n7#eJZLc_8Cz{>{C%qvCoJqVxNj?ihV{@5&KkB zQ|vRMirA;3qqM|6BdUmfD$2xuAWw?@K*vz*2hKQo><0>>Vn6r`!vlDw=f55M57XGc zOp5)74ED3%+dz|g?3=&2(x0#nC@?Hc)eq^V;! z8~K3=fthL|KQJp$5ttpQ49p2s1*!uzM0k`4dx`KE5gsSP6GV8D2u~5=X*F{Q5-u=E zK8Gkj-tx%lL_Q;`h=Y?2L1~CO@sqPc$Nsy5#f0vyg-B(iSQpa z!xfPqh`Pn8jQstl6Zwp&BJ!!IrpRYR6_HOxHAOxns)&3lswwgrQAOlaQB9H0h$I#l7S8WYeaaR2yYPKEh4;4gm;M0N`!aS%p(Y-jC21v z_Fp-j*k?o)u}?)+$9~4M3a3%A|E3iCRF#SSjJGo0RvG(JLNngwu^&umdKA^MFRdjQ zQF-hK*Ai4zQ|vRMirA;3nqr?3Rm46O)fD@Ts3P{MXgKyWvofW{1eQZLipW zpN`w8*w5^i+51$npIMyQC$n#6zs!=%(#-xu_<#r>65${bJ|e;)A{-{d$7+TvVn5?N z`EXUn{(Gkr`;4d}_Nl0**k?o)u}?)c#Xcjdhz9GW5MEH&f-xJ{nHS-8kF7tqV9#QW0KRcb+ zXG9gTPeoP7ewIPuG%EJLmSUf(GO?fK&hn^?eJPY>e!do5{#%k_JeB) zDyk{=8Bs;-Q&CN^&xk5wpNeXVeMVFf`&6_~@VZ26R!!F2ta;JnHmf!=ZrdyNf1=|y zD)zG)v+!$N{`tm!)~c+tvQ}rU$vQhr%)&<^eK*K_OpJ8iv2ZGXUoDXmj3P7|C7f4l~U{<55@lG;y>DA zKUEJJu|yX~bgDjegFwoTlg}f{*#GNvVxJLJ#6A^O z9sAkC6i(%_uhU7fPgR-N&z_P!Rb}i;3C(zy$9^!O=}}b2zO_<-( z``LT5AIsjC{do2h*-vIaMRbWoXCyi^(OHPjN^~}&bEu5{Y=eBbDq>$3uQm3w-$3t- z7L~<5(U~x^BC4U>x5cTNT(C%CgxTni=xU07MwE^Ih^VIMXGGcPkBF*{{v4a)^kvac zbar`NL)l+p^^7YYwy1Jm&4h+QmCcnC8Y}CY<~B4gs%bpE-oY*lB4-g=7K9&t%;}0p zA02<=|8A|m&=J{A=k&-a3g&9~fEOjtBfccs}WJvv6{0|;WR2%Q>9KytunEib3x99Dq~ejK*qZ~R;2`t z97R*CGNNp(MnpBmDkI9qYD831tTLi(tVTpN#VRAp#%e?~9IH8R=e(2C$_(50=&)_C z=M8n4;b9x~xM9wboUf6p|M=>noNscz&G|0p`cWtPqtwZYWNX zh7onTP^~Cest}bu87C&lMeEdx1_yj}P2~^91qVEJx=5|4v^ZyK%03tuIsJOIq9}Ql+k1g z8qo~ZGrS7P-GWs}Wbu)EL+*`IxU_v?TkiJgFWj2DL;6DSoejCS<=#GP%Aiqr^+4U2 z%BqD}Lfx8sNA8`ucjexlyNlb!e6PY9ddWukNB`FsHPAD-FX{{)%-!AQ@8R4>rb_RW z2s+%JyC?TiZdcnECWo!V*XZ#hYF6N#3XN6sYnRqEzM8v_%bU>H(9kUM9LE_zGROdD zfS#ZjlyY3`z2G762Kbia;$ndVq=3$#4;Tf`1mnO&Fa=BlGr%mc1l$gu2cP0_F~ALq zKncLl8Rmm?z=hyW@HqGg90s3&&%u}AYXBdHAHdJxSMWPH3XX%nI4&O6<8fZ`I8MAB zz-xRq$OTm9|79ag!VN327U)q@L4WNxJsGa3juoK({ zJ_o3m1@*F^UKZ5K0&f=7%JLIHy)3AgMe_OwK)o#JAC^Bk&YA#>zy!?;1vLU z+s_?j#U629Pr>kogMJtfCmT8 z(Sh1I-UjI3&U8=?hJxW>B*1Z+LF3%}0Q|e30PydIe-CQxL47^&?tym?yn7M>{CiMe56-~@{~mbvH~{>6 z;NJuPp1uHHJ?DTs0NUGww)UWo9@No;I(p#40}mc}@SsMX=fOMRUGP5m5PSpVJfOjvv<9(}~_ay-T-v0uegBSk2 z@a}yRz`qy%z3`vv0Vx39Q{g=o-cwQcRQOLt?Nf0Msqmi)@2RMJD*UIye=7W^7J@-w zET{l=0JTls1a^X-IL?;@@N>RNU<#N9W`J2>HmC$upc>SGd0;-k`0yk@@)VYgG<4c0LS%R4bYE$IA`C_9G8akODhFsU?3O_hJayU1Q-QIgXv%< z!12=17HK$E8rmXlDOe8B4ryqEG_*k)j+b^hxB^@SHiK)x7H~bd5qt2SDA@dx2um2lNH~Knds%1^}E>I@%z;9N@gtQJ3`f z;6iW_xCFp&`egu~(>DUtDjl^-hwpUMEB#sk@9Ai>^xFaImwqSs3VZ|L4-aT_>5@0p zG#z6*{a5fC_yZgR$HAZAZ;nG3;{2$&A2s*GAD*V={P5-<3Q%|daDdwTQG5THU|ck0bK$5L;sR3C%}{7DXWNc7un&Bj8bhw#r36&P7}0qOEfO=C}?#(192b2Miz{B!EO<0v3RN z*1-lG0O!{M{kTIqz&Uk5jXI!49jZVLK%F|kV~1J*pB)y0MW6woPjy%VngD9q0e(Bc zYX|u30G}P;vjco~fX@#50K9g150XINf5hXdFb#q0nk0;bMPhj3VaQ|1>b?6Ij$2Abie=-fDzaL+P0Gm z6a$QrPHVw!;0|yX*ahwd_k#z)L*QY6{?iHlrxUz&g11ia)(PG^eFWgE(-8nqo#3g{ zAK)0kId}S-<2u7jXL#ux1JL(6$Ah6@I2Z{=gE3$%7zf6K31AYK0;Ym#UVh;JiAcEjs@QP@~Sjg5LrBc0LN=x$|-G7l$XZ z0DN~50NSk!ymzq!CqVtWB!P)wGJwA>(*eA7K~1| z;I9j6-UZ&e+y_wiE)M|Iz6)yK$pAY}}w*b^P zAKvrfJs;llQQLg@&qsapaSr+LpAYZ(y8-;?!+$>f=O5&_uJGD57mNmI@2=wj+PN#* zxhvYaD|~c?hpzC@6*cNw2UdaA;A}v^I&dyHA6x*g2G;`giLU4qUC}4HqEB>1pXiGI z(Df7W8TbNx1-=2_g6{zOLf0R_Pv94T{?PR|@CP``aou`@KA;~c1!Z6W7zhS|!C(j& z2GEzgjR2zn&ad0W;8JioxB^@SHi69mp1WNGwt(vae0RGMYz6S%?GX4FzeCq=n0AdycfWG0lXKK0QfKH4{#0z@LvG$1?2$# z3*f&1{tGSu8vwi)zU+@7q2;jW{-V5Np z;8Otq1)l?)Ljn93z6$eS6}(dOiW30;o~X1K>FTzdc_7@Z9r1;AQX%fbX8Kfj0oW_dE=a0MxH1+P5$p z4MsCgm06|M!Sd*L|%wJ${N z3oif{f(_tea0x(N3sKiX_$x$R3*oKsEr8kMO|02}4 z2zs?c57Kdci|4c<6;1 z_38^ofKlK~Fcypl6TxIK70d?<0Qy8P^od^R6TQ$UdZADBLVxJ>Ab1Eo0v-jAfqmd{ zfWFY{N$@my2B1ImdKNqnUf{Ui@gNbHfCboq9XNm!xPTiZ1N7zIUf=^bzuq%J1*im7 zpa#qZ^8h^et_2IhA^_jL8^96(@4fE@_XBwE4e!0-z4smf|GoDDoI`K;?+x$0p91jT z8~%I4e{l?m1Mpr9@5S(5YzFXOYy~)nV)!qH_u?b~|HbfM4FAPb!E^xc#qeGX@5OTf z{1;aPoI^4E7sGooYF-Tg#qeJY|HXHKT>##T;k_8%i+2O~FUFiujB_Z4|6+JAM*WN7 zzZm|D;lB?&_Q81RGX|jj`k)4V(0+Z;etj^O`>X`3z-q7toDB$A3oustoCnSa>%oO! z1Go%a4{iiEgImE)0H1x}vk!dsc?%rnxV~twzJtLKFbs?UqrjP9EEo?a0Mw%|YS9opTRHSSMWRdgX8-30>uDhq+bc>56Zv*Fc1s^n z05$5j5nKu2x8EiJ&;70j*MjQ+eD}Kn+yvmg-`(IIfco{j5Bvar0`S-GHz0XKP5T`K z$HAZAZ;mVBfdF&>eY7MNpynm0c?tZLpynm;Rx%o(?j>UYYF~odmrMkcz+^BLOarKE z3F=w`eLiZKDYo}2sQxph5i?VOTpy;{h|LA;3}{QTn*j^ zt>8WI0XPUg0*An1@GGI%dT+YHD9X8?E~0Ph3feLw+#{{g7?0Gz`B z_#Xi815on;@IL_l2f+V;)!=LZ?*rg{0K5-455WI`^#JEE0R9KS`+!RT{11Tt0q{Qn z9tU#35BdYtU?9fwz;b}GJa9M|0Y-t*;7l+Ui~|$EBrq9F0T?F(r-RvG5oiF-U@2G* z;Bz2+4usEvw}KY{+G~&txIr@T0v|{R0gwr@0O~OawHO3%gWzotybXf4K_dZt4Z`>x z1W$wDX;2NA2Wr7W055~!WzZ7P1X{oYU^jRe>;ZehV_+Y696SM@0{g);-~f0IJkN21 z4Ilv+ff-nV4cLJLI6)Hd0G!j{6p#vVUV~?VSpYQ}JO@+*_#HeK!1LhwU;(HD@IAO5 zEC%pCcnuH%>Nj{TcoIAf;BWA=K=Ou~4t^2*2fPIS3tk1Uf!6{0=-{_Ft{gQlN6pLO zuN*Zmhqv-HfV!9a0cu~4+Lvd89FPn0Ku3VOmZPra@K=tymcv_lBS3A-(ca~#Z}~E? z0;~k9z-j>h<*0Ev>RS%)LkR*~> z#A24P3~#x7Dl72&%HQG+g%kw>6^c=u5@@baiZTSFyTXG!jP?pusEYOq+ACB7&|g7+g&E93dj;(kv{#si{t6558!G6ppuIv0`YY(Kpua*sH_=`} zdj;(k?xDY80KcK4{)*Zw+PtFviux<+uULUfXs@WfqV|f_(O=R3CRFqrD(bJOy`udq z>aVE3qW+2+EBbq>crg&DRQsPZ4|Q{`>mMQfE0&|BprG*{7FMROJVRnc8Vca^djph^gC zT%{V(j6q+O323V_kx5Ku3e$*ZI^Jg$@3YEmPH_f(RW5M9?IO8c;xcxxa*gZcbCZHV zAmkI=H$-2^7ibHyYslBQb%=dKzNZ}Jarcl)=nolzeM7W|Xb;gIV%re?A@&XN8$$Gl zXb+i+{t*2k`a_Ok-w^E~+C#L5*fvCehZq!ts*b7}s``Fa8&;h|BFp)Wm8@nh>)FU=_TW8KJ%G1RRby56ud1)Awra2P zI&b3U)!xI+t9^i*SNjNiR3ugd>7>xF6!_Z&NmeocQg|}MG z@2KXzR5eS^1n2hE|H zLp6tL4*i%<(HyEd)H@B;99ous^k*Rc#zG^}9jZH2cc|`A+lSgdRClQEQ2U4K4z*|K zUiM?tP@9J84%Hp1J5+b5eM9|yh3XE~9ct%L-PK>?KiImut*hI*y6)<_tLv_=ySlBb z+q$~$>bk4jy1MS_+Nv)gjf?mjsO}9_zshxPaFg5I;V$MG(O}!SlSx zE9k4CuZBHq=>oMm_9RVRMGafjutg19)HsM6)$m(u=%}HghK8B}isSn= zOXK@BAK+P@!}n{xjJH_R_iMUo&3Ca&%}#Wo8$IYvANr!JW;g@z{%S_x4c2tennU>= z%{4XG+=b?vnf!tFnpy0_`>UzHrv94lS<^jh{R3^awAFeE`_$4`%SN?cM`tZ}tmTfi z>{Ux|Et}QSTE!HV}J; z4Z)US{wBh7hv^Q}9i}_X-$mFsbcg8EkPwyUk7wuagoYWsd|->&W3wQW{Ao*676g{3U#H&(Kmwd}(E zYrA{xedw!g)7r<-Rp&)s;@`M)9rvyC25wyEE!??|8`pUsTh{pyoptP4ryD)!MIXG$ zI{ndK$BuRUhC1$F$NlU09d*2cI(|zXd)C>BwmRu#l7+viIvVTPu}(Gz(OSorb-aN( z_N;Rv5UBeC|Ker5&$_RpyRPoKy6ft$>;84$Lw8->bw5IP-L7<}C;q1D_Ct4F-F0=> z)m_*9>)O7q?z+0`4o7$09i*`vo7S~yUEOtc*VSEDcU}9|b^p4$>*}s+=eoM%E5Vdb;cBuBW@6t?Svkp6+_O>)E=V?t0ql&0-U$@b>F@`}Mq`dO75Bi7Q;? z8u{Gd7I(NyVIWXHKru@5Bv0{o{=swTt8df#+UkeUi5S+fj*V>Qceb+=cdT!d`hItP zzq!7a`gv%ne;pn53%HGr`u75X29NRtzTe;(p2hbY*r^^f%Doz)c%u1_BKqLt8^_4WGt7 z4fQp&QNw?tv*8PPgAMJ~P;WzF}C#=hOyW{t-& zf!QSREAv=D5{pP-1Mc70-5c*fUt^m#-h-|tPw_N=!=0PBZ|0a5y>_zjBx_!V}@6pm!OH(aPwKQ!&BlI-Y z)6}<{x=T~vZt6`nb%&XnfKDnZ)oN>H1oID>}}q~Z)x@!|Kkh1k7oVQ)y&^nv%zR<7Kz(7 zvsE)&H5-M!nng2)v1n}OeKhkvn(1q%t=avy!yMr#$2rL@s^rTW-4)bf6b>e zlUZnMuB-X|rVCsomrGnGkE>iKpBof#D-dY$Pi)-6#x1;u7UijgrWT>pq&D?vNE3dh zIjtCoy<6D1g^gR-w}s!(Vm$g==x#9$y)Cr1NFbS2tmgnWZgGWsfk4YrJj~yCfq&Bh zo3vcQ9^9&>TebAxZRu7m{r6iQ!wxM^VT+b-)Y2X;?a|U6E$z|L9xZ*Rr7c?C3KH{8v}vX#n9GTTk9a~(^_9^8?}A}ovk0|N$k~HZ)=;i*4)~? zTEEI`ypFckx?1aM-HwiQqBC9SN_TqFi{A95AKF^$YOSmFCbsfB+t|(y(y)2!bTaq@ zZ={VK+h}X^S9G;`mVfY1p5u95 zaOZaJ+wL*kxZM-Db2~R~_cXR__cwI5vuC^dG^8;<;!U<|hW>VTZ09$$bN_bk-_Gx7 z=MA*;TiV&P-6FKLTZ*^YZUz3P+G%WO$98L2ht_tsZ08NMvuC@lfk69Glpz@Jv;8CJ zZm+w&?)JLdyMKH4Z?C((?)HC2cl&xYpb`G2+W(C1_PX2aZm+w&`?t4!d)@7Iw{MT` z_Q@<}2{vtS)AqXC>u#^Rz3%q*ZSVf=b+^~u-p=iHcPNRiJJ`B|tvfu7?hd*;=>-Q&WOIN+9Oft|ILRr_a*m7K;!Yso z9D$C-(b&FDsy+EK-F?_$1yLPfqC;N1&ObFGePA$Tyjjm30sgF0XzQ%2v#!oRb(I?xFp?X0b{uFkqT z|Hdj-vxc?!`{=v@n|I!fch>oLypb+;?4qrUzmG1ux;%zA*5wJFehVXzr%D+x_OEK%l#~+1;D$uDiSL?z+3{?*0G|p}V{8 z?zZi&yL&aNQP5yuDtv@b-Gu!7Y2)s#gQ1pt+akUNg|#%l&#KpuLy3 z*UQ`MaDl8&3bF@{TKeq z(>#N=-nx40>fI23552vu-riPkZ>x7xn$wb2{6ZVrqOG^C-nx3HuncdjcPhWJf>qeO z_gdDm0dJ#^9s6kO<9+qf)#m}ctv(O&FpuyUPw*tV`{?eYyH6PQ>{Ab0_VM@7M|U6H zeRTKH-KQBX(A`IOpVsK^<304TXP;zj*=H%b`{?eYyN~WZD_M>1KDzsO&wX_FIU5M{ z)!FxL+_kUm`fBK_p|6I%zTelk`}%fYoAvEXS0Wg~P=+&-C`L1u8MuF6ckeq7eSK}( zcL}=s=5v!Q<*0GUIY~^>hv4frLB7;o+U@!YP#ziin zt-rSZ+WPD2uV;Xk0k2}K0qu!lB2$=#9R}E8fE@-TV2c5M=YZvC7_c4RAK?1~(((NP zd+_}M`#DbzzCYj!*YW)U?m56N1MM-eEMN02-%}p<8(0xt1HHY0-rhiOZ(t~HInY)E zYcU?p12qqvjOKyvH!vRU1HHY0-rhiOZ=n8x`Ukq>KzAH?0c``d4a~zn1N9BG(ZCz% z9O#Av?_jTi_j_$NNb{hFd6dU^9BqSi4bn9z41W)Uysbgr)*x?dP(vE?BR}yo&1jCc zLAnO%8Z@6IysbftSWF5_vH74>eq$xx#vnTm(l*HZiqI8N0&gp#6s0LcFc0z&x+8Q) z=#Hq0JtJyh%LsoD5xOIEN9c~w9npv;=#J1G(G=Yg-a~{vBYwq}5ev~Bp*uo%gzktX zEJJsM?g;NWLU+WmKwz-W!LQ=3gKalh!(a`AH4OIs!M;7%w+Gv7a2wjumv9E)2!jU` z$uLGR8TTJNomuD`Y}3ID&^7oHdARdnHy&Jo`wqU18xOYQU^@=6WJ2f2hWxHXT|N2n^FT>{Tk!o){*gVVE~HY&!NB zW{+X^80L<{{N`cqG)&8|?PwXc3mwCB4BLZ_Vf#5x4!%F^3fJ-dVeUH2KEv%Zyewby zE#Fg~3RFbb@XCbX4Gs^*I~-nKYT9pxPZ3d z+J^i47;c~8`i9$R_ziRpcgx{-u-EYWy*3-6d4zk7c$CL@9Bm_XjnFkB3~y|NH#VX^ z-r0zTH0DQs;%A!C9Bm_XjnFk>K1n3Ah{gE(7_k(ak4VKk8?h2^WP}|@XdB`0W2CN; zCGf^ZmZCIe2K>_kr0$XaK1Mb{_ek9%o1%N9 z_b}3)BY(w~BNw83r0$WrN9rEAgk|U+se9xKbdNk12#nG>>Q&rzl=;!He;ZM@jIw3acl?028RcC@RY7}{4Wsl&*)hs*i1Hhv+&`)|wv6&y zq9!o~ZBg;~dy3K*Wy7dB=!~*slpUkoKT2;@5}KnlM`@0_inkf%O-AXC(jBEcN_SKt zMS(!H?r7c7wvE;u{Vm^9jtW#pceL(k-O;+Ez1e7QHd=SI?r8f*>yDm?J)@@*#|(5w z>yFkPtvfmq`$p@I)*bB)MC*>uQ??8|&?jt%O^Swbj_FM4@@C=CR|^Jl6fjPD1-w zZ*Q!(H`d!5tADKivFLi$DJp*@q}C0a>5<%ciJ=N3(Dec#(0-8<QR>jkSHO z=2*9nwSVjc{DWdAF$Fh|)g7xlR(GuKShtV0Z>;WE-Ldn~9c$-UJIC5NHV+%e>WnkSDz^JMp%9E0}BhMnx~P4@OC>z}NDvO7+8$H~XhHd))`bJ%CHzR5P4 zoQuxMZaDcW_L{7Bvdt!Io>G$1lpzRhQ*=$yHKhvv7N&SxQ>x=_O{qy3b*M`{8qg4J zQ*=$yHDxAq@V2HT@+)(hkIknfk<4PejVX4VqHT)zHRXQSZM>~1cPZpvAmAS#m|C0? z=$@*3s_v;jV9%))vE@{M4^wqd)jd`BRNYf+Q5)S;bx*C2?y26xRC`X1$CguPqkF3E zsk*1?o;r^O=$@*3s`orq_tbspoc0{_=+8jL;QrIxecEL7O|$8=S?HQ}hI3rNou|3)w9B~hv@5vtG&i1h16xij;C`n) z<38hme92dMlX2gnKhBPEenXu5$GLx;-x22x#Q808_Kb^0TiiIj&A5s9n~KvIXUDjy z#Gy6LmT}%doIT^_aGJB6$NP-SMR%O;INfo&>a{o{1U>5jXF?)d-mIbYy!D*hXE z$Lo&Q9j`my{o`#PuRC6Mdbma>yEc|yzc4RrnexHM7;g!-u`rNXnHa!EMYmR z{KiUFv4-_*U?W@D%1*L5$T3c!ak@>X=b&rGlYBybyrmftXqYjKk=SE~J!aTrhC9yi zn`gMw3@tO3p=HJjbj;8(V;wqXY$A*O`2LI|9LM)(oW(vf>@)KX-r^nJ<3m2;V|2~@ zl+W-6XMTZqIP)vMrazizYMwa+%`=BH0_`(L5sf!EQ~yl;Gba$kUbM~BHuEs{nW=B4 zjb@%i=S;Vpc@BHc)H~B=Gw(OM*UUTIr7#efrEQk3S-NI@!}t6^IlQx36{tiNLa0h8 z)zLOf*DPJLVwuborZNqGAG4-o^I5Z)O#^ZwEww(PPx@YU2t$Via+5SFeS4Q`2-LtEqd$#v5+n%%ieayDy>`CaJ zt$Via*}7-PGXveTb8*^+q z$Ch*KnD9Q{W`cK_;7umjFF|{P4HMix!EZ?L8xp?7??|v^f*lk5mV`*OC5&Jc_DdLp z#)NU$F~N=rS`%!UV9NwMCfG4SbHY))%>?f*x)W`kXzN7XiMkVQov8a)ZND}qoOov7 z{rozIMCLJ{B$8P~3QJhVZ>(S?Ygo%BGWdi296;l*HvRP!y5>H}JJg^Teb6v>01?<@ zu07`3W3D^S^_%Cq(_Afc7ouhEVsy;aF*g+*b61f@I=(-5FWLD1Tz8#opSkv#_Y(i+ zRbJ;!-r{X^&3l*k@doF8gm*aaQ~pa2G|$sKuOFJ{4P+47=M5$jZ*ZRedHUy#B8pvT zo2PAF7WSE^Z=Q|j9Yp6mx14tjd(G23&t~)PH@nxoYg{Kk5SXuRzOMPY=6{AaHs2eY zUl#9d{?~lV_xwOPDo_z^^L5SFH9wlMjAJ|#@b@u)A~v5t1@CNr9Nx%$JI>cO-`~f4 zUGvZ3jm^KnpX89sWxTWby65YjuY17<*mJ=r*m8mQw?Ov--3xRt(7nLl$Aa(By+HSZ z^5|aRJuI;2f|1yA!5DNe(7izS0^JK@nS|~Ix))4C_kvC6T=*dFy3lqDH7wMyP{Tss zU+CKleS4wJ7FMG=Kk_rpXhADl)0X!1!~GY!`@%@{Ewt&v(db(E2U+aHofo?A!o#@n z!lStJLN{J`3R^Bb!~ITsCcVn*yun*|lS%KPKgo_senXP`C%J!;-;v}EB>62#_Dt%7 zwxs@en@NN5Hx|3hwHU5LYspNOiovb@qce3te_fNKcvhHNv$)BS;xi@_Y$EL|PP1c>PJ6U(K z?qvHWyMMCoWZlVjPS%~AfvuBmoowsm1L#iHovb@qce1UMZJn$;S$DFnlXWlBwx}ju z7>~EV$lG7!4K12VJky!QZ03;2ugqg1NhFiP5>~UFooHL6ZIQM`x)$kKq-AkQY_+&D zo#{#sdSQpfc35nO#e=ZLV!w0oI5aFy!1ovX{^AAr{^CXW{^F%``csH5&{$lrB zY?sCMNcjiP@dE$i-?(4OtLRGc_ENmP6mKu(ZQL@&Rw?h(5zQ%@Q@W!$#r;zHpgqOg zOY!znyuB3tDf&~~F~uEIwxTUXTgoo%lcF!hMk#yHnc{{i+1M*ZZ;H)Q?l=F*MRK_m z2rSXIMAs5sOFrOZysahP))H@P$!GkJFDc7ce8abBTcT@;t|dbmfw#3}6j4MohH;E% z0RzgQ z>F0cb?xnhyevR&>-osLRE**?5mkvkwQr$~+FV($tG-J`dRQJ*tbT3_v&SfQW*JZX_ zreT?eWg3?G{xaWQ=G)6`wyXk`s7C`D(S)C9N^@G$9rs`6?#ue4Z<$S(4MW$mo$Ml= zOx$^@GoBG6}-vi|3Uw9J1+Mdmb?G*_wYNGdjre; zmgV+b-W6@jd*W>_?}xvsm|^J5_h8?o{2W?w@M=RNbk%Q$Iv^Y8SfE1DmGW zG*x%1?o{2Wx>N0&>i(&^Q+21>IaPP+c5Iz$>r`8({( zI#u^?+J37-TSnpS|2BqkOkg6Dn94Nbn9dAllgO{kWdRFGVI3RT$~Nr!o3`Jy{if-+ z+kwD}=kT^yw4ycb=s;(>;)W~SaD_cq_`NIq-W8M4vO>oS9V=#{V?_cwR?K4+Yw`UR zoB18zU$F}}U16J*Pw*H1$}>F6KlmrQR=VZN7jes#FXNsoU&T#Vwm|br%`5G;QuE4= zbVB<|w_NF#E8TLX{+0Sy_Q6e8u14ERZ7VlnpOyMn+Gyo=bguONR(gLc?X^-sOEhVIL+)vsX7 z)o-ACweHorSL=nuhzX<_iEj%hcFb~t97p) ziSE@Y=v;j}5Ln}`Yizeh!x{~1G_3LcHNL&Zx7XNgO({f9&A8|Kt!!g^Ah00_%^Ngtcm&NGp5RHe zZ+MEQ`5XE-=-=>9o}(e!HfY<>6#Hz@x4}jmenIC3x7^Skdu`CW!DbsYZ*a2>gYfn@ z3`W}qT^n?5*nwMa@WwWzlR+kX*vme=vkeD07zk|Cwo%tcT^pb0U%bT2{F_&J4V!O# z1Mh6(+xWZLXvdA(HhOCtb#3&{HhO0pd(w;E^d+4B=-#M%qwbBHu;<3#vE{}zbZ^wX zQTImO8~-2+-5Ygp%trSn?_rZYH~IV6WXnx2qI;9>O}aPf-t;Q3qkEI?O>d!lQw?-( znu5D-vfU;Pn>1|Fu*vr~`SvE?-ej{)$2rLru9448ZgGb~iUNVnkKq2B-F@@Z=-X`5 z%`c#9^G`ITIqtmKeK)tkjW@T$oj1Gj=FZr1b60e3w&&(lR^V-J_AWPXK>KDJZq~oq zj+_04&3?mX_usq|TWb&J+5J#qIfeOSdB)}eWe)-77MY(?)D?{$mb zEq33ccZ=RF_j@w~fvu161e&*M-l}=4=B=8yYTl}OtLCkmw`$(1d8_8Fnzw4+s(EWG z?7Y>!TXk>My;b*C-CJ$EwHvy(>fYK5-CHAwWj1!-s(I^vE(HRT1HYH$Yrf@s%2R=g zRH8B=R3nt?)T9>UiD4p>nMNG(OlJnOm_q`I%w--I$RU@@T*Z6*{W{*{?>8vmHg~wo zJ&FQ>ZC~;g-|!vf(7jFfHr?BFZ>vI8bZ^tWtp>WcHO710_6xS$)`f7Qn1se{tI6OT zxA5KV5ArZ>xcx7<;dZy%{v2+${RjLrxBKU8Uxux>uf#uV`#LtTi7j|9+xM`Svz+Ho zF5)iR^RUNuTWq((jxs!e@9%hwC-MCq-p~$jXUFq2q6t6Ilot4%J6fS@hu^xRE#A$J z4tO^^I^*~6Sc2vqns=-~^A7KP$6B=SSkFehogMmj=-;u8?Sa6~Ahhk&w)0W!vs2$r z8|{1wojaf5@7Qan-kmnvsd?v{yv;kji?*G*cIw*M74LXwPkP}k@9aZA`ZIumL@*d_ zJ9X{UwQ~o%*iAYaWU>dF@7#yKft?397zm`Vzri$JY5&6CU)sz3n^$;^|L_L7 z({!ilPHTrf(>h_xwC?Cm)19U}O?O&f!qJ_kJ8clU)4Yc?d#3%4Ez{D_ou)fYcbe|B zKgdFNn(nl0bf?`51a|4%^#ks@t34WaY1pM z#|qqkm%H!Uh`wFh*om&)5A!IGUz-MtIV>6+6ur)y5voUS=tbGqhq&FPxcHK%J%*PO07U30qTbj|6S(>14SPS>2S zIbCzQ=5)>Jn$tC>Yfjgkt~p(Ey5@Av>8tSfl)fI_>AKT(r|VANiha{{r|VAl2GVtB zJj5eBhQFzdr_i0DJ41Jd?hJ1>!<)^}ouND9MRaHU$j@lb(43(;Lvx1a49yvuGc;#t z&d{8pIYV=X<_yglnlm(KXwJ}_p*cfyhUN^-8JaURXK2pQoS`{GbEf7@&6%1rHD_wh z)SRg~Q*)-~OwE~^Gc{*w&eWW#Ia70{=FDdJ->%GG(4DC}Q+KBBOn+yY-gu_&Ox>B? z(4DC*^GqP{M=AcztGvz|yu~}b%LjbOM|{Gke8&Iyf-m`+ZzxAi!l+L}8sjbeq3;iU ze?+r{{oD%#_5>(_jy*vCLiPmgX#PEz8Zade9SXS-P@x zWvyic?wRGSWqE5^TlgJsEz4WW+Q}|<2LgMw?bWqc*WM@jD^K$bf5YGP-hW{8z0dOk z|KcU=xL4a=ewznUfp}s(7n%l*k{juk73JwPoaCC?tQxV>E8D&|3vpb-TPid_r7xI+!u|z z?z7!K4f{0g)3DF?_xbid-`;1leU7_tA7?nn1#-ycGFP}32<$J8`|o%6{STsVzfJf5 z1zr2YsDnH2cjNs{aNqqu;l}&zxZjTZ?YO@c{x(17ltvg$Hb~9R_J6m_QZL@V}&u1aYEM_UXvvp_d&eomn&1QSE*}AiJ zXWKtp_kjTRJW!I-1f% zn6gwRgc|5OsPCXX59&Io=U^oEI+)H43b;dIAaKYQhiq}k7Kdzcs0?m&$ZtKQ~bic8O&l1zcQD3=sL83B)q>vi}3~z zx#yu}Tu1XE&4+Gtzxf_Tfxuzyhl}C;9rpeX>p!gjuzMbM&%+hac39itYS`znzQZ;; z9EQ%r?s(W858Lao-orLKtod+T+S7rKXgjRyu&%?)S%LR;cop8*;Weyd0~^`I7PbZg zN3&Rm~jyHDXFWCIZ(>%l9c@{e!(RRc;JEH5z&v;))n&FKdX+bMm z(+1r~bRW@uBpG`iS%NK({D$r$x{v5SqWj2N)}#A~?jxJgebjq6YR{uV*z%~qiKDuY z>OQLbsP3ar@)Wv{>OSgCAJu)dEIN-y;;u(+cT~es4M#N`_5Gv1ebl#)+U#f=yE(!! zPH>7doZ|vH+{XR=M*_!6pzoMXk3EF0VK-;mq+zSMb7sKDwagE3A zc)S#4(0bgK$Gw5$_B{RwRj5iR-skZ!bRXA!T=#L^#~aWH-N$tw{|Vj4XEK`v{7oI7 zkM85TkLy0J`?&ibxBYS5$8{f1MfdTW+~N*4HF@BK?i0FC=suzQgnduA{|VhEbf2*E z3Ed|uW9t*PK4I$x(MJkJaKiHnea zw6@dVVxQCcPTS~o1$3Tv&(rRC+Fqyip0?R(&8NM!(?9VuP0@B**J)j+=du9zJngNW z_SQ}>A_Z^lw6}J8Ilr+2ZKrje)^++`AaJG_#VJ8a{EeI`gU!!8fWMJ55Az6iJfrQ5 zzmYS#&V=EuovDMjcBUQ;XoRsi~K)o@nB zSq*1>|EzDH_3g7ZJG+hzWbg-B>?50l9Ofvwxc^yqKYIgxXKi}6C=fWO>)h9T%lEkR zIrlwR2{%4h1$RE@#^_Ezfxa=k0mE7+>)X-|+(#(0yL_dEMuA zpLhTB?tfnQdEMu0q5J#kGENVCxIz(S1Sp1>F~PU$FHBTVK$9 zLH7k)U(kI)+l5V>3IzTvfw%vsxBq7_5AraN@HkKKBv0{Ip5gC2%RhLY7kHhI`IIlv z_ou!;?fIv!KlS|C5qtf)g5#Xz4Ck=LpSJkZ7Ju5}&nvi5j^CQ2BS%AyhMcGQ8@`|O zPkcY;Mc(Hld_U)NzQp%)+%%^gcF7sW2u2ajSjI6PT{$sK#QV#cf;X7so;mRxMRShk zoYQE|@qltJpgl)uPA=YGj{Y3|IqsR`o)B(n%@7j<3Kbupg;yswM5 zxq~-$@m?U{X9RMKQJj)^AGvnS)t2j>lszlp53`4Bff9b9LuV!k)R) zuw|~liCo>ex^s2s>dy6dkvk9Fxw>pN#7-#Ub=y<%b)TY|HGXxyYJ<1aO2D0;m()c_;LkodASlgFWd9-D54p| zIK0WrvFN{S$IE`hW%s}A{+IoZ%ih3czvZ$$FP}u)<+FI3mvitpby?$O<6OSX6|`Qq ztP{ zdLB2r>bG9gaZSTD4c8vwF?|2pU-12F&+s~L;``U$=OcXonwwtx61!aM&p;v=!Z3z2 z0$tZe5smkEZ7kz(&ucO4Me{Yy*AAii+A)r!{n|;qziZy#HT~E0UvtlE?s@$`XuGcM z`g_>ty1wf+y8a0|ue;-Qcf4+|>w2%-?7HUbAygxj>S(*J>$lRzTcuIsw4>-t45lgAaVa*cd$Qot?l;C|{2%bf^2<|^ z%2YvjzV3Y8`J=IC{&;Me?{6YscfRg?-TAun{axhGKzF|G{5j~(_a5@?nSTLW=KGt- z*PX9BUw6Ll{OjC6cfRg?Z#rN1jTg{)qb2ToV+O%TC6z{5Pk)4a?pyn(*k`fl6vwyxWHZa2nWw-eb-27B0xEpFT5 zwk>Yk;`TAz=(gW_TgM#@cQo87MHzhm&V%^=okw|rm+<{Nuk$9pf5%Ple1u)@bf+i1 z=}UhGFc4jLA{c`AcV`&h;2rn8Gm11c-_d;M4>aG|$9}ZmIlv*jzdQQx=)dEhcii*t zi)g#6?e1&X=dQlHHoE&ZI`6vUU3a`|ue*Bh+U&08yWdle@>D?EU0ru|-Hl=l-q+o6 zcw=`b5X&ScGlglyq3y1&ySnb4;w;|R-Sb?)8@qcEo8P@m9#`=`3hh{^tcS`I>L}4&8;i3w0MpV$Z@6*s{>yM4|3N-G#afbr<@(D4dAyLfwT^(Ou|0 z6xy@!IJPYGH&Lj&P60{Hx#*lQ8czJ@>`1Bz9^FO|% zEdA)uKq81_D8m@e2u2ajXvQ#(@$6wQ*&O5uM>)oEPH>7doaG!B&|It(-|++GsYn$< zs7f_LsX;Bms7+n!@iWb6PJ8;`w-g)1VEm3^Bhg)KEU6r#C{V2UE4+ryircLCJG_U@ zircHWy^4R!_xNt{I{0Ss1~j4xKhYH5FW#MA^u?CNH54C$4U20iuA%sDGWdfm_G7o= z2e4gnZN=?Z+;+unSKM~RPjNa>ti((Fn^$q`5^tir#M@{u@h)y&!o5rAFQLDLdzbhx zJ?-FiZ!mUf_EfIz05>tsIp6O^Sp{s>N`A%{d`Vfp;%mOe<|Tii92M{`O4_lcwvygqNnIt~ zv!pjzay%1=VIq^6g6@*KOX@Cp6nmCDi7iWdgC%vB)Ll|{N!=xLxs2|Tx=UU|cPa0o zls!v*f-Ot^58b77m(pEIcd2jq4&9}6mnx6$Qti-LYB}y&%66qRl+sY@EE-Drekq%k z^8He`xf>`}`avG%Q6A?>p5kf##%sKR+n4rcOWU=yzS1@=t*vw)`q3Y^F73vp-MF;B zhtfl_WocWMwq`hrj!lDX8l*c& zcaZKN-9h#Z@@|842k8#7bCB+!Uf4Rw)i~qlWeRci+ O``7>f|BF2s{C@zeBnA-x literal 0 HcmV?d00001 diff --git a/Extensions/Roster/CoreDataStorage/XMPPRoster.xcdatamodel/layout b/Extensions/Roster/CoreDataStorage/XMPPRoster.xcdatamodel/layout new file mode 100644 index 0000000000000000000000000000000000000000..275e1a470d24b1b0aa33a853bf1635cd8768fe7c GIT binary patch literal 12354 zcmcIq349aP*1z}8G;PxCP0}SznxtvM($pl)DqEYTg~}>rDO*cuL#c+gwyA&>y-yU7 z$HN^(1*Am~^dXBPq9TicxS@i8%HxTMyMQZ-JpIm0nl8M;r|)}T^7|z-cV_OK<$wO? z+%q$+AyV5Ewc8IPj0j>7ixQB4Eno}F%{PV{n`-Op%FXrDt_xR1nZHg%fCa3C<&>N2Bo4j*fya&RE$c{HE1*{N0q1=)u8K9J!(R;&|lGF^f$Bw z-G%N$528oV8nhNYfu2Mg(GIj5?L%*)_s~i7G5QpJiO!&J(GTc%j4;M5mSH(oU?on$ z8k~XkxDW1&ufqA*i?7B5@DN;zufap{FnleZjHloVJPl9B*WoC>6;pgWUWgatyYRhu zIbMNR;x@biKaDryO?Wfjinrq(_yB$lzlGn$NAUajIQ|TOj=#WP;;-;od;$N8FA_vp zl0;NQP11>u7>Su!iIe0L7x9rI5+uc>lnf`=lF?*5nM$UUT2e=5lNNFdxt%N^i^$z% z6=@|;kQd1-WG{J@yhGk4hsZJV4|0N>B%hP7$v5O%a)JCxE;5J_G0BXIQ8OAQgUMkm zjFsub}i8OB1< zP+5I*V^w%!d2n!Js9{EJRTFFm!;x@JC>o}xsQ|u-RPZj$n~buNnQmN%a*zdCQ7_aR z^+A16F0#=iYN38QoKB@p)J%s_<8gEqvLgp_qI~2+Zn)ZmydaYg`O(#=AL@?=pn+%* zDnNxO08%O|Is_<*gld{9Fb7k6;XoCYv2+_K$DWig8@ z7Xf_p6ITcYA&7?Whkn%fxC_)GH;G zm)q`e+I=3M%jq0Zb<+06hQ+fRARx|}wbnL;#NS+B}7f(eYa7h=j+t4&rg(|ua0v5XZ zGdF!C7bqszuk012~K=4bl{<;Km|oCaRkNk<}D!1XNIErQBTX z_vL$v3WK&nhr7h)Ds;GPe!tUY15bGJ-JW2;TjGh`r2*Xlm>N+wyg3@0773T}bF`rb z)EHBW-;bRdMa`2s%v>65MK_{~t^*&U*=P>B3ALb`L9Dsx7IZ7PkD|Yz+t564qj=$*3{O4IYxkEV7x3+-w-wp0_Rb+X%~gRbgDHKavP~s12LOcDOZE3>zhGS zFl_y-ruN%RiH#XzgO&1LJjDGzN50$bbUJ-@kKflL#K*VT9JvNN58u1dJ?LJbxUz7~ zOwg%`W{i{%i6IGoaUuK^6m;yjkL=_|xI^3rbF%X1bQ|H$iMil@^Z=l?(-^I2DXQo; z5Ue7c&Bp9>DHS;E z{ycxa-*1Nq_vAy&d*_a8vF92b9cEhpU$xC4^b`PYII^J)98Um4jtMo^grn^QPmR<> zlRgB3j?wcbIC(SL0!f3T=V#EfXe)XSZ9~rkF>mKn2!v2-d9VnwMyR1F4D30xA=DV< zomweR=ZH1vL2su^=pA$u-1+XVcYX)G3k<|ZTPz8-p+o2} zPfJ|}0Wo+FXG` zx2@1u;;?UxI#A0fETLvZ~J!F2)s3c+PTzjX^Pdpx-EAh;~_s{c5+ zF!NsxuFGhI6R-d$q7gU=-GN0o8H2O&pm~52q2g;iCDVk-Ji^0FI6}hH7hO zMB9nCJiZ@S_s?@KR&)9)xm)rdo@lwPuEAsBg9qH-$rKiz7c7^BHqOEZY{Vv-duyz?ap&^MXG503e=(6kFoNsHsYETRD@e1kxBe6DPF<5+%Y za!mVtw+pn7XU0Q#IPmS=cm%o!kD4^1jJtwQ;MoP7li+zEy-QQ!< zO^7k#l(=-0=tPii@)a2|j#1u6mtb@YNd9a$$^RGr>%}7y9M_4+Ua)L6(czbelpnv0 zcj8@mH{OF^0Z8p@hg2gC(J-AxYv@dX)D3?SQg3jOdXrXlht%7sH$bYIPVI!$Vf@~e zAax8NH9Zcg4`ZvH61T6==X2QX0Vm+f>v7ovF1N$xfZE;TaJZbl;-bqr?H~9A{wFx? zBXHWs_!RyG^~RsZoHir2hKO)Zqd-&F(OOvVU;oF}5Lfcv*WkS~-Msf-TP9pi&2tbr z-+`LngPK3$pCEF2<6pW(PD31Lxe!Y44f!||@j!8F?O!(%H0`-b8N!Sl)ICEH9IhNh-jSq|qC@ z!IEfEAAsd7+SCC{l0^(xfF;QWSk7sOC9&|ZEV29TB>}h326dwg_^sGy^OZQ=wxW_? z&}a9$T=rt`W&A~Y;X$Mi8bSJ^JBW?sfxr3?M>l`Xjr*$w{MAHnjsuQYv^!l8H@b&- z|Gc{{C!8NYPWpjx{Xw{aWDpQt9}?(BbbpBpcPkKG6Qyx5^;8*<5|D04H|efGW?U}q z!$Svr(+QnEJanLo1|^uo>4CMT-NzMR4lm?(r`O(5f{`&))=I{badbXi^d2cE6OfQh zM8nA>GMP*v6{s)Qnt_ht^N3z%+(SrkYp&XBtTOs*6J+Y71V9#?o?OvzD=grUOcHH^- zPCG0&ys@P}sUb6<9mLPnN@}=v5HzRAb>#XEd3l-INQBIc^_MyYy`;Z%gw&G;ZpBP) zAdRGnL`gHWH3MW8zjhwn2;DL0Uli8WM8e%h_?z=x!;;!aq|*doBW{2{Y${p+7FbOG z1`hdaS3BH97mk!?+Vi}A&<^^6b{}*OxlOO%@3ZH-eNeA=T5T?Ow#A<3hmtnm>9KS6 z@p-*=r`KcW?c;-Crz0OK?RNXzN@%5A*KGw3ns5!?4q;c5zmVI=JXG<&o!G7rui+)O z<3b#|aAZEj`(5#Pzq7rvDRCE-6uNBrUJq0@9=F|ASmJftf`Q_~BD=%wDG8KtS3p~t zET$XV$lu5kau>aaw$XL(lY7X$)YjwXC_Jp}sN7Bfc;a z4L8=oBLaSUFcY-lx|<1r5@?P_>+538CyJCT{;h!Cr%ZxJUvwpXls*8)dWb@wMMz#E zJIKpqC)q`IlRe``lyR1VKh6cvL7vXbO&_F7vkNR>X!ywGlZQ1|)mDeVv~|_tSoH5B z`{AMzc*4<2_MwXR$N`x8HK3l?$s0gEZ;^xK?J1x=*fUaJ!`*|k3rA~w$FzN$E~5+S z!*T(AWL(*tCa?!QK8b=EF3~R*o`+%HBjhMh(fAp)@W5oDoC!G4rFZ@i?tGk|cR5`# zft!rKYu710q6-7|_SpTHoZ_phE-;Au<#F-}IZZw#pK(zO&D$PM;rrK|-%4u3k!sGF zK)HqdBNT22EX4iAEcONYvZp9&C0}v#1NJYOe>~r}bVK8kW7{+2Yl5;%QcDbsj zNdAs|50U&MAIayrNTzG(;}AxxA%dQWMUcDa2r7(11Aba_G|W8)i#H)V<7i^%@y?K1 zLVkzHX`^ee6gl59O!vqEG1e%$12wLE4=OQYMuPe>GEOB%!K?HXeG*h!4=Oz!Q_0t} zO5L90^{Ca9Zl`oAwv0&!#Ss1b@lHx$w3m_shz6=L`mVG9U3qA;M)|HhqEFEcUE^_7 zTuFly_w2g5vP99wyv&IjBSn0bC%n&Fsa=8*CRv$bM^}(=i^#L>DPi1^Q2{YG15`LjQ2{U8(B>Mp)cRz^|mKrW;`Terkt6;Or$T-U370xIG+qjI5(DrnGlcj z9e}`L^j^UEP7dcC@PwIa0G=<=m#+|a%;ZZm7R<4uD;ncq>`9m=;E~)Ik1(@&VRi$L z4C8s^mF~h!U~U0ncF{do5~isK64?Dmg;@j)klW4xck#mP0|pqzGr+6egdxkA2l&9< zf91ek^zVTS^X%&$t#SKwL?hh{`KpEEX**x>JK;&4w+r6aaCl$KJVD=}Z*|4{dge(U z?>72o9Ph@ic;EP+<9#!;1@LY|0cLBg@$)v|-AE61o&I@l`afC^GcUFa(_Rm|JiaRy z#If*r8N%b;czEpQ!y`}_aOMY!^WniTJh^ZMgMM4U4c#|~-BaTB`hq16r#l`Vd%5t~ z$LwcbrHASJ^h8gHd=0|G#=Oa=frC5vV4sL)5ILoVY{K~4F>M;U7 z{Nt)zWc~%J{LZPuVqTS#^dnH^pP5-9EmuZWnvh?PSlob%}01GqP!H zdJpfbw6YmoPW-p`SitZIUP59sSsiPo=hoo}>W^ok_u&cYR=kUdu?afsa%hbAh3BL1 zKsR$X9k`C|#r9_VuzlHF*2d=R zsZ2VfWpqpyV`Q?KF-#S6BfK@XhtX+lLSdgNunfKk|HT3Nu89M6i6yf8l5yIX=Tz= zNjs8uChbmoCFxsXvd}8@2nPuZg+;<*;b38@aHw#&aHQ~B;Y4ALFe02IoG)A`TqIm9 zd|vp1@Fn5P!d=2W!o9-%!UMwBg@=V73%?Ma7h#c8@iFlS;^PvDL@7~8G?Fw)h9py>mlz}_iCJQi6iTj<43mtMTq~I- zsg_Ka%#buoW=W{zPRYHJ`z1>yDE7$P3b}DJJJ)<)6(yxze@ilGs_&Ze3@J3 zl?7!bvLUi-WV2**WG%9}vRh?;k=(I6E|yE>a=A{PCGRD7$gh_7mk*Q=mY2#KxNlX!@B&SGHa#HeB z3Q`8Aj7gc2Qkk+oPsgA2os7|U*seVxXr21KP zL7k`;s+DT3+N`#yd#N4jeDy^20`+3`67_xR2hKiGxcEV=cyOc#A#V+ebek|&NNq=C(V~uoHjUZdRkLjbJ~iuwQ2j)-cCE0b}>CU zU6rm$PfItZXQyA2UY0&KeR6t5`qcDk=?l^yNpDNvo_;X>%kb}?X@)#QnUR{2 zo}taqWn^Vsn^BPw$(WTfC!-}}ZpQkI%^5o~UeEYEC3!2vw!Bm%psX$GskC6%&f|6$(*0LB6DNrOPQ}^zL|M4 z^V7@=I-*O~rR(&%T-~+0GTm6+c-;iuB;6EUr7omfrdzGsq}!r*wkh=@;vl=9Y)3y|V^n1+&VtsE+K6NZC^V}=h5Ck>|zrwyMO#YU-7Zd4joMvXDem|@H`_BQr4+Kg8jCmSn_Q;pM% z)yC<@8OH035o6T2%=n0LxpAd&m9fqExN)s4fPc(`nOZrY}rCnSRbTW%tW2$PQ!&vrDpvWRJ}rpM7KY zyzD!&pU&Qvy*>NI?7i9hv(IFo&;G@X%*4!^1?IkHoB1lU!<=t+o4sbgxu1Ea`6lzt z=3C6vyve-9{H*yo^Yi8x%rBW=Ht#a;F~4p;Xa3IogZU@(&*lr}i{{^QP>wIBIA?H9 zY0l7`n{#f-p*gqZ+@3Q(XJO8woW(gya+c=2kn>W`%Q?Gp_T=o%*`ISD=XHy~qOuq* z*_IrO)lzPmXqjxOuuQc~vs7EATV`0Uvou;-EORaMEz2w`ESoGlEITcSEbm#4THd#O zX!(cbpO%xBQICt5?+ eI_oU!&DJH>`>YS~JxWAK?4J$B{^P&ahyMqgw|ZXy literal 0 HcmV?d00001 diff --git a/Extensions/Roster/CoreDataStorage/XMPPRosterCoreDataStorage.h b/Extensions/Roster/CoreDataStorage/XMPPRosterCoreDataStorage.h new file mode 100644 index 0000000..8f8af4a --- /dev/null +++ b/Extensions/Roster/CoreDataStorage/XMPPRosterCoreDataStorage.h @@ -0,0 +1,75 @@ +#import +#import + +#import "XMPPRoster.h" +#import "XMPPCoreDataStorage.h" +#import "XMPPUserCoreDataStorageObject.h" +#import "XMPPGroupCoreDataStorageObject.h" +#import "XMPPResourceCoreDataStorageObject.h" + +/** + * This class is an example implementation of XMPPRosterStorage using core data. + * You are free to substitute your own roster storage class. +**/ + +@interface XMPPRosterCoreDataStorage : XMPPCoreDataStorage +{ + // Inherits protected variables from XMPPCoreDataStorage + +#if __has_feature(objc_arc_weak) + __weak XMPPRoster *parent; +#else + __unsafe_unretained XMPPRoster *parent; +#endif + dispatch_queue_t parentQueue; + void *parentQueueTag; + + NSMutableSet *rosterPopulationSet; +} + +/** + * Convenience method to get an instance with the default database name. + * + * IMPORTANT: + * You are NOT required to use the sharedInstance. + * + * If your application uses multiple xmppStreams, and you use a sharedInstance of this class, + * then all of your streams share the same database store. You might get better performance if you create + * multiple instances of this class instead (using different database filenames), as this way you can have + * concurrent writes to multiple databases. +**/ ++ (instancetype)sharedInstance; + + +/* Inherited from XMPPCoreDataStorage + * Please see the XMPPCoreDataStorage header file for extensive documentation. + +- (id)initWithDatabaseFilename:(NSString *)databaseFileName storeOptions:(NSDictionary *)storeOptions; +- (id)initWithInMemoryStore; + +@property (readonly) NSString *databaseFileName; + +@property (readwrite) NSUInteger saveThreshold; + +@property (readonly) NSManagedObjectModel *managedObjectModel; +@property (readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator; + +@property (readonly) NSManagedObjectContext *mainThreadManagedObjectContext; + +*/ + +- (XMPPUserCoreDataStorageObject *)myUserForXMPPStream:(XMPPStream *)stream + managedObjectContext:(NSManagedObjectContext *)moc; + +- (XMPPResourceCoreDataStorageObject *)myResourceForXMPPStream:(XMPPStream *)stream + managedObjectContext:(NSManagedObjectContext *)moc; + +- (XMPPUserCoreDataStorageObject *)userForJID:(XMPPJID *)jid + xmppStream:(XMPPStream *)stream + managedObjectContext:(NSManagedObjectContext *)moc; + +- (XMPPResourceCoreDataStorageObject *)resourceForJID:(XMPPJID *)jid + xmppStream:(XMPPStream *)stream + managedObjectContext:(NSManagedObjectContext *)moc; + +@end diff --git a/Extensions/Roster/CoreDataStorage/XMPPRosterCoreDataStorage.m b/Extensions/Roster/CoreDataStorage/XMPPRosterCoreDataStorage.m new file mode 100644 index 0000000..ec9c6b6 --- /dev/null +++ b/Extensions/Roster/CoreDataStorage/XMPPRosterCoreDataStorage.m @@ -0,0 +1,575 @@ +#import "XMPPRosterCoreDataStorage.h" +#import "XMPPGroupCoreDataStorageObject.h" +#import "XMPPUserCoreDataStorageObject.h" +#import "XMPPResourceCoreDataStorageObject.h" +#import "XMPPRosterPrivate.h" +#import "XMPPCoreDataStorageProtected.h" +#import "XMPP.h" +#import "XMPPLogging.h" +#import "NSNumber+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +#define AssertPrivateQueue() \ + NSAssert(dispatch_get_specific(storageQueueTag), @"Private method: MUST run on storageQueue"); + + +@implementation XMPPRosterCoreDataStorage + +static XMPPRosterCoreDataStorage *sharedInstance; + ++ (instancetype)sharedInstance +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + sharedInstance = [[XMPPRosterCoreDataStorage alloc] initWithDatabaseFilename:nil storeOptions:nil]; + }); + + return sharedInstance; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Setup +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)commonInit +{ + XMPPLogTrace(); + [super commonInit]; + + // This method is invoked by all public init methods of the superclass + autoRemovePreviousDatabaseFile = YES; + autoRecreateDatabaseFile = YES; + + rosterPopulationSet = [[NSMutableSet alloc] init]; +} + +- (BOOL)configureWithParent:(XMPPRoster *)aParent queue:(dispatch_queue_t)queue +{ + NSParameterAssert(aParent != nil); + NSParameterAssert(queue != NULL); + + @synchronized(self) + { + if ((parent == nil) && (parentQueue == NULL)) + { + parent = aParent; + parentQueue = queue; + parentQueueTag = &parentQueueTag; + dispatch_queue_set_specific(parentQueue, parentQueueTag, parentQueueTag, NULL); + +#if !OS_OBJECT_USE_OBJC + dispatch_retain(parentQueue); +#endif + + return YES; + } + } + + return NO; +} + +- (void)dealloc +{ +#if !OS_OBJECT_USE_OBJC + if (parentQueue) + dispatch_release(parentQueue); +#endif +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)_clearAllResourcesForXMPPStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + AssertPrivateQueue(); + + NSManagedObjectContext *moc = [self managedObjectContext]; + + NSEntityDescription *entity = [NSEntityDescription entityForName:@"XMPPResourceCoreDataStorageObject" + inManagedObjectContext:moc]; + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:entity]; + [fetchRequest setFetchBatchSize:saveThreshold]; + + if (stream) + { + NSPredicate *predicate; + predicate = [NSPredicate predicateWithFormat:@"streamBareJidStr == %@", + [[self myJIDForXMPPStream:stream] bare]]; + + [fetchRequest setPredicate:predicate]; + } + + NSArray *allResources = [moc executeFetchRequest:fetchRequest error:nil]; + + NSUInteger unsavedCount = [self numberOfUnsavedChanges]; + + for (XMPPResourceCoreDataStorageObject *resource in allResources) + { + XMPPUserCoreDataStorageObject *user = resource.user; + [moc deleteObject:resource]; + [user recalculatePrimaryResource]; + + if (++unsavedCount >= saveThreshold) + { + [self save]; + unsavedCount = 0; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Overrides +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)didCreateManagedObjectContext +{ + // This method is overriden from the XMPPCoreDataStore superclass. + // From the documentation: + // + // Override me, if needed, to provide customized behavior. + // + // For example, you may want to perform cleanup of any non-persistent data before you start using the database. + // + // The default implementation does nothing. + + + // Reserved for future use (directory versioning). + // Perhaps invoke [self _clearAllResourcesForXMPPStream:nil] ? +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Public API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (XMPPUserCoreDataStorageObject *)myUserForXMPPStream:(XMPPStream *)stream + managedObjectContext:(NSManagedObjectContext *)moc +{ + // This is a public method, so it may be invoked on any thread/queue. + + XMPPLogTrace(); + + XMPPJID *myJID = stream.myJID; + if (myJID == nil) + { + return nil; + } + + return [self userForJID:myJID xmppStream:stream managedObjectContext:moc]; +} + +- (XMPPResourceCoreDataStorageObject *)myResourceForXMPPStream:(XMPPStream *)stream + managedObjectContext:(NSManagedObjectContext *)moc +{ + // This is a public method, so it may be invoked on any thread/queue. + + XMPPLogTrace(); + + XMPPJID *myJID = stream.myJID; + if (myJID == nil) + { + return nil; + } + + return [self resourceForJID:myJID xmppStream:stream managedObjectContext:moc]; +} + +- (XMPPUserCoreDataStorageObject *)userForJID:(XMPPJID *)jid + xmppStream:(XMPPStream *)stream + managedObjectContext:(NSManagedObjectContext *)moc +{ + // This is a public method, so it may be invoked on any thread/queue. + + XMPPLogTrace(); + + if (jid == nil) return nil; + if (moc == nil) return nil; + + NSString *bareJIDStr = [jid bare]; + + NSEntityDescription *entity = [NSEntityDescription entityForName:@"XMPPUserCoreDataStorageObject" + inManagedObjectContext:moc]; + + NSPredicate *predicate; + if (stream == nil) + predicate = [NSPredicate predicateWithFormat:@"jidStr == %@", bareJIDStr]; + else + predicate = [NSPredicate predicateWithFormat:@"jidStr == %@ AND streamBareJidStr == %@", + bareJIDStr, [[self myJIDForXMPPStream:stream] bare]]; + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:entity]; + [fetchRequest setPredicate:predicate]; + [fetchRequest setIncludesPendingChanges:YES]; + [fetchRequest setFetchLimit:1]; + + NSArray *results = [moc executeFetchRequest:fetchRequest error:nil]; + + return (XMPPUserCoreDataStorageObject *)[results lastObject]; +} + +- (XMPPResourceCoreDataStorageObject *)resourceForJID:(XMPPJID *)jid + xmppStream:(XMPPStream *)stream + managedObjectContext:(NSManagedObjectContext *)moc +{ + // This is a public method, so it may be invoked on any thread/queue. + + XMPPLogTrace(); + + if (jid == nil) return nil; + if (moc == nil) return nil; + + NSString *fullJIDStr = [jid full]; + + NSEntityDescription *entity = [NSEntityDescription entityForName:@"XMPPResourceCoreDataStorageObject" + inManagedObjectContext:moc]; + + NSPredicate *predicate; + if (stream == nil) + predicate = [NSPredicate predicateWithFormat:@"jidStr == %@", fullJIDStr]; + else + predicate = [NSPredicate predicateWithFormat:@"jidStr == %@ AND streamBareJidStr == %@", + fullJIDStr, [[self myJIDForXMPPStream:stream] bare]]; + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:entity]; + [fetchRequest setPredicate:predicate]; + [fetchRequest setIncludesPendingChanges:YES]; + [fetchRequest setFetchLimit:1]; + + NSArray *results = [moc executeFetchRequest:fetchRequest error:nil]; + + return (XMPPResourceCoreDataStorageObject *)[results lastObject]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Protocol Private API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)beginRosterPopulationForXMPPStream:(XMPPStream *)stream withVersion:(NSString *)version +{ + XMPPLogTrace(); + + [self scheduleBlock:^{ + + [rosterPopulationSet addObject:[NSNumber xmpp_numberWithPtr:(__bridge void *)stream]]; + + // Clear anything already in the roster core data store. + // + // Note: Deleting a user will delete all associated resources + // because of the cascade rule in our core data model. + + NSManagedObjectContext *moc = [self managedObjectContext]; + + NSEntityDescription *entity = [NSEntityDescription entityForName:@"XMPPUserCoreDataStorageObject" + inManagedObjectContext:moc]; + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:entity]; + [fetchRequest setFetchBatchSize:saveThreshold]; + + if (stream) + { + NSPredicate *predicate; + predicate = [NSPredicate predicateWithFormat:@"streamBareJidStr == %@", + [[self myJIDForXMPPStream:stream] bare]]; + + [fetchRequest setPredicate:predicate]; + } + + NSArray *allUsers = [moc executeFetchRequest:fetchRequest error:nil]; + + for (XMPPUserCoreDataStorageObject *user in allUsers) + { + [moc deleteObject:user]; + } + + [XMPPGroupCoreDataStorageObject clearEmptyGroupsInManagedObjectContext:moc]; + }]; +} + +- (void)endRosterPopulationForXMPPStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + + [self scheduleBlock:^{ + + [rosterPopulationSet removeObject:[NSNumber xmpp_numberWithPtr:(__bridge void *)stream]]; + }]; +} + +- (void)handleRosterItem:(NSXMLElement *)itemSubElement xmppStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + + // Remember XML heirarchy memory management rules. + // The passed parameter is a subnode of the IQ, and we need to pass it to an asynchronous operation. + NSXMLElement *item = [itemSubElement copy]; + + [self scheduleBlock:^{ + + NSManagedObjectContext *moc = [self managedObjectContext]; + + if ([rosterPopulationSet containsObject:[NSNumber xmpp_numberWithPtr:(__bridge void *)stream]]) + { + NSString *streamBareJidStr = [[self myJIDForXMPPStream:stream] bare]; + + [XMPPUserCoreDataStorageObject insertInManagedObjectContext:moc + withItem:item + streamBareJidStr:streamBareJidStr]; + } + else + { + NSString *jidStr = [item attributeStringValueForName:@"jid"]; + XMPPJID *jid = [[XMPPJID jidWithString:jidStr] bareJID]; + + XMPPUserCoreDataStorageObject *user = [self userForJID:jid xmppStream:stream managedObjectContext:moc]; + + NSString *subscription = [item attributeStringValueForName:@"subscription"]; + if ([subscription isEqualToString:@"remove"]) + { + if (user) + { + [moc deleteObject:user]; + } + } + else + { + if (user) + { + [user updateWithItem:item]; + } + else + { + NSString *streamBareJidStr = [[self myJIDForXMPPStream:stream] bare]; + + [XMPPUserCoreDataStorageObject insertInManagedObjectContext:moc + withItem:item + streamBareJidStr:streamBareJidStr]; + } + } + } + }]; +} + +- (void)handlePresence:(XMPPPresence *)presence xmppStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + + BOOL allowRosterlessOperation = [parent allowRosterlessOperation]; + + [self scheduleBlock:^{ + + XMPPJID *jid = [presence from]; + NSManagedObjectContext *moc = [self managedObjectContext]; + + NSString *streamBareJidStr = [[self myJIDForXMPPStream:stream] bare]; + + XMPPUserCoreDataStorageObject *user = [self userForJID:jid xmppStream:stream managedObjectContext:moc]; + + if (user == nil && allowRosterlessOperation) + { + // This may happen if the roster is in rosterlessOperation mode. + + user = [XMPPUserCoreDataStorageObject insertInManagedObjectContext:moc + withJID:[presence from] + streamBareJidStr:streamBareJidStr]; + } + + [user updateWithPresence:presence streamBareJidStr:streamBareJidStr]; + }]; +} + +- (BOOL)userExistsWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + + __block BOOL result = NO; + + [self executeBlock:^{ + + NSManagedObjectContext *moc = [self managedObjectContext]; + XMPPUserCoreDataStorageObject *user = [self userForJID:jid xmppStream:stream managedObjectContext:moc]; + + result = (user != nil); + }]; + + return result; +} + +#if TARGET_OS_IPHONE +- (void)setPhoto:(UIImage *)photo forUserWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream +#else +- (void)setPhoto:(NSImage *)photo forUserWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream +#endif +{ + XMPPLogTrace(); + + [self scheduleBlock:^{ + + NSManagedObjectContext *moc = [self managedObjectContext]; + XMPPUserCoreDataStorageObject *user = [self userForJID:jid xmppStream:stream managedObjectContext:moc]; + + if (user) + { + user.photo = photo; + } + }]; +} + +- (void)clearAllResourcesForXMPPStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + + [self scheduleBlock:^{ + + [self _clearAllResourcesForXMPPStream:stream]; + }]; +} + +- (void)clearAllUsersAndResourcesForXMPPStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + + [self scheduleBlock:^{ + + // Note: Deleting a user will delete all associated resources + // because of the cascade rule in our core data model. + + NSManagedObjectContext *moc = [self managedObjectContext]; + + NSEntityDescription *entity = [NSEntityDescription entityForName:@"XMPPUserCoreDataStorageObject" + inManagedObjectContext:moc]; + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:entity]; + [fetchRequest setFetchBatchSize:saveThreshold]; + + if (stream) + { + NSPredicate *predicate; + predicate = [NSPredicate predicateWithFormat:@"streamBareJidStr == %@", + [[self myJIDForXMPPStream:stream] bare]]; + + [fetchRequest setPredicate:predicate]; + } + + NSArray *allUsers = [moc executeFetchRequest:fetchRequest error:nil]; + + NSUInteger unsavedCount = [self numberOfUnsavedChanges]; + + for (XMPPUserCoreDataStorageObject *user in allUsers) + { + [moc deleteObject:user]; + + if (++unsavedCount >= saveThreshold) + { + [self save]; + unsavedCount = 0; + } + } + + [XMPPGroupCoreDataStorageObject clearEmptyGroupsInManagedObjectContext:moc]; + }]; +} + +- (NSArray *)jidsForXMPPStream:(XMPPStream *)stream{ + + XMPPLogTrace(); + + __block NSMutableArray *results = [NSMutableArray array]; + + [self executeBlock:^{ + + NSManagedObjectContext *moc = [self managedObjectContext]; + + NSEntityDescription *entity = [NSEntityDescription entityForName:@"XMPPUserCoreDataStorageObject" + inManagedObjectContext:moc]; + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:entity]; + [fetchRequest setFetchBatchSize:saveThreshold]; + + if (stream) + { + NSPredicate *predicate; + predicate = [NSPredicate predicateWithFormat:@"streamBareJidStr == %@", + [[self myJIDForXMPPStream:stream] bare]]; + + [fetchRequest setPredicate:predicate]; + } + + NSArray *allUsers = [moc executeFetchRequest:fetchRequest error:nil]; + + for(XMPPUserCoreDataStorageObject *user in allUsers){ + [results addObject:[user.jid bareJID]]; + } + + }]; + + return results; +} + +- (void)getSubscription:(NSString **)subscription + ask:(NSString **)ask + nickname:(NSString **)nickname + groups:(NSArray **)groups + forJID:(XMPPJID *)jid + xmppStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + + [self executeBlock:^{ + + NSManagedObjectContext *moc = [self managedObjectContext]; + XMPPUserCoreDataStorageObject *user = [self userForJID:jid xmppStream:stream managedObjectContext:moc]; + + if(user) + { + if(subscription) + { + *subscription = user.subscription; + } + + if(ask) + { + *ask = user.ask; + } + + if(nickname) + { + *nickname = user.nickname; + } + + if(groups) + { + if([user.groups count]) + { + NSMutableArray *groupNames = [NSMutableArray array]; + + for(XMPPGroupCoreDataStorageObject *group in user.groups){ + [groupNames addObject:group.name]; + } + + *groups = groupNames; + } + } + } + }]; +} + +@end diff --git a/Extensions/Roster/CoreDataStorage/XMPPUserCoreDataStorageObject.h b/Extensions/Roster/CoreDataStorage/XMPPUserCoreDataStorageObject.h new file mode 100644 index 0000000..0d8e077 --- /dev/null +++ b/Extensions/Roster/CoreDataStorage/XMPPUserCoreDataStorageObject.h @@ -0,0 +1,79 @@ +#import +#import + +#if TARGET_OS_IPHONE + #import +#else + #import +#endif + +#import "XMPPUser.h" +#import "XMPP.h" + +@class XMPPGroupCoreDataStorageObject; +@class XMPPResourceCoreDataStorageObject; + + +@interface XMPPUserCoreDataStorageObject : NSManagedObject +{ + NSInteger section; +} + +@property (nonatomic, strong) XMPPJID *jid; +@property (nonatomic, strong) NSString * jidStr; +@property (nonatomic, strong) NSString * streamBareJidStr; + +@property (nonatomic, strong) NSString * nickname; +@property (nonatomic, strong) NSString * displayName; +@property (nonatomic, strong) NSString * subscription; +@property (nonatomic, strong) NSString * ask; +@property (nonatomic, strong) NSNumber * unreadMessages; + +#if TARGET_OS_IPHONE +@property (nonatomic, strong) UIImage *photo; +#else +@property (nonatomic, strong) NSImage *photo; +#endif + +@property (nonatomic, assign) NSInteger section; +@property (nonatomic, strong) NSString * sectionName; +@property (nonatomic, strong) NSNumber * sectionNum; + +@property (nonatomic, strong) NSSet * groups; +@property (nonatomic, strong) XMPPResourceCoreDataStorageObject * primaryResource; +@property (nonatomic, strong) NSSet * resources; + ++ (id)insertInManagedObjectContext:(NSManagedObjectContext *)moc + withJID:(XMPPJID *)jid + streamBareJidStr:(NSString *)streamBareJidStr; + ++ (id)insertInManagedObjectContext:(NSManagedObjectContext *)moc + withItem:(NSXMLElement *)item + streamBareJidStr:(NSString *)streamBareJidStr; + +- (void)updateWithItem:(NSXMLElement *)item; +- (void)updateWithPresence:(XMPPPresence *)presence streamBareJidStr:(NSString *)streamBareJidStr; +- (void)recalculatePrimaryResource; + +- (NSComparisonResult)compareByName:(XMPPUserCoreDataStorageObject *)another; +- (NSComparisonResult)compareByName:(XMPPUserCoreDataStorageObject *)another options:(NSStringCompareOptions)mask; + +- (NSComparisonResult)compareByAvailabilityName:(XMPPUserCoreDataStorageObject *)another; +- (NSComparisonResult)compareByAvailabilityName:(XMPPUserCoreDataStorageObject *)another + options:(NSStringCompareOptions)mask; + +@end + +@interface XMPPUserCoreDataStorageObject (CoreDataGeneratedAccessors) + +- (void)addResourcesObject:(XMPPResourceCoreDataStorageObject *)value; +- (void)removeResourcesObject:(XMPPResourceCoreDataStorageObject *)value; +- (void)addResources:(NSSet *)value; +- (void)removeResources:(NSSet *)value; + +- (void)addGroupsObject:(XMPPGroupCoreDataStorageObject *)value; +- (void)removeGroupsObject:(XMPPGroupCoreDataStorageObject *)value; +- (void)addGroups:(NSSet *)value; +- (void)removeGroups:(NSSet *)value; + +@end diff --git a/Extensions/Roster/CoreDataStorage/XMPPUserCoreDataStorageObject.m b/Extensions/Roster/CoreDataStorage/XMPPUserCoreDataStorageObject.m new file mode 100644 index 0000000..f7f9b82 --- /dev/null +++ b/Extensions/Roster/CoreDataStorage/XMPPUserCoreDataStorageObject.m @@ -0,0 +1,493 @@ +#import "XMPP.h" +#import "XMPPRosterCoreDataStorage.h" +#import "XMPPUserCoreDataStorageObject.h" +#import "XMPPResourceCoreDataStorageObject.h" +#import "XMPPGroupCoreDataStorageObject.h" +#import "NSNumber+XMPP.h" +#import "XMPPLogging.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG +static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO; // | XMPP_LOG_FLAG_TRACE; +#else +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +@interface XMPPUserCoreDataStorageObject () + +@property(nonatomic,strong) XMPPJID *primitiveJid; +@property(nonatomic,strong) NSString *primitiveJidStr; + +@property(nonatomic,strong) NSString *primitiveDisplayName; +@property(nonatomic,assign) NSInteger primitiveSection; +@property(nonatomic,strong) NSString *primitiveSectionName; +@property(nonatomic,strong) NSNumber *primitiveSectionNum; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPUserCoreDataStorageObject + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Accessors +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + +@dynamic jid, primitiveJid; +@dynamic jidStr, primitiveJidStr; +@dynamic streamBareJidStr; + +@dynamic nickname; +@dynamic displayName, primitiveDisplayName; +@dynamic subscription; +@dynamic ask; +@dynamic unreadMessages; +@dynamic photo; + +@dynamic section, primitiveSection; +@dynamic sectionName, primitiveSectionName; +@dynamic sectionNum, primitiveSectionNum; + +@dynamic groups; +@dynamic primaryResource; +@dynamic resources; + +- (XMPPJID *)jid +{ + // Create and cache the jid on demand + + [self willAccessValueForKey:@"jid"]; + XMPPJID *tmp = [self primitiveJid]; + [self didAccessValueForKey:@"jid"]; + + if (tmp == nil) { + tmp = [XMPPJID jidWithString:[self jidStr]]; + + [self setPrimitiveJid:tmp]; + } + return tmp; +} + +- (void)setJid:(XMPPJID *)jid +{ + self.jidStr = [jid bare]; +} + +- (void)setJidStr:(NSString *)jidStr +{ + [self willChangeValueForKey:@"jidStr"]; + [self setPrimitiveJidStr:jidStr]; + [self didChangeValueForKey:@"jidStr"]; + + // If the jidStr changes, the jid becomes invalid. + [self setPrimitiveJid:nil]; +} + +- (NSInteger)section +{ + // Create and cache the section on demand + [self willAccessValueForKey:@"section"]; + NSInteger tmp = [self primitiveSection]; + [self didAccessValueForKey:@"section"]; + + // section uses zero, so to distinguish unset values, use NSNotFound + if (tmp == NSNotFound) { + tmp = [[self sectionNum] integerValue]; + + [self setPrimitiveSection:tmp]; + } + return tmp; +} + +- (void)setSection:(NSInteger)value +{ + self.sectionNum = @(value); +} + +- (NSInteger)primitiveSection +{ + return section; +} + +- (void)setPrimitiveSection:(NSInteger)primitiveSection +{ + section = primitiveSection; +} + + + +- (void)setSectionNum:(NSNumber *)sectionNum +{ + [self willChangeValueForKey:@"sectionNum"]; + [self setPrimitiveSectionNum:sectionNum]; + [self didChangeValueForKey:@"sectionNum"]; + + // If the sectionNum changes, the section becomes invalid. + // section uses zero, so to distinguish unset values, use NSNotFound + [self setPrimitiveSection:NSNotFound]; +} + +- (NSString *)sectionName +{ + // Create and cache the sectionName on demand + + [self willAccessValueForKey:@"sectionName"]; + NSString *tmp = [self primitiveSectionName]; + [self didAccessValueForKey:@"sectionName"]; + + if (tmp == nil) { + // Section names are organized by capitalizing the first letter of the displayName + + NSString *upperCase = [self.displayName uppercaseString]; + + // return the first character with support UTF-16: + tmp = [upperCase substringWithRange:[upperCase rangeOfComposedCharacterSequenceAtIndex:0]]; + + [self setPrimitiveSectionName:tmp]; + } + return tmp; +} + +- (void)setDisplayName:(NSString *)displayName +{ + [self willChangeValueForKey:@"displayName"]; + [self setPrimitiveDisplayName:displayName]; + [self didChangeValueForKey:@"displayName"]; + + // If the displayName changes, the sectionName becomes invalid. + [self setPrimitiveSectionName:nil]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark NSManagedObject +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)awakeFromInsert +{ + // Section uses zero, so to distinguish unset values, use NSNotFound. + + self.primitiveSection = NSNotFound; +} + +- (void)awakeFromFetch +{ + // Section uses zero, so to distinguish unset values, use NSNotFound. + // + // Note: Do NOT use "self.section = NSNotFound" as this will in turn set the sectionNum. + + self.primitiveSection = NSNotFound; +} + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Creation & Updates +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (id)insertInManagedObjectContext:(NSManagedObjectContext *)moc + withJID:(XMPPJID *)jid + streamBareJidStr:(NSString *)streamBareJidStr +{ + if (jid == nil) + { + XMPPLogVerbose(@"XMPPUserCoreDataStorageObject: invalid jid (nil)"); + return nil; + } + + XMPPUserCoreDataStorageObject *newUser; + newUser = [NSEntityDescription insertNewObjectForEntityForName:@"XMPPUserCoreDataStorageObject" + inManagedObjectContext:moc]; + + newUser.streamBareJidStr = streamBareJidStr; + + newUser.jid = jid; + newUser.nickname = nil; + + newUser.displayName = [jid bare]; + + return newUser; +} + ++ (id)insertInManagedObjectContext:(NSManagedObjectContext *)moc + withItem:(NSXMLElement *)item + streamBareJidStr:(NSString *)streamBareJidStr +{ + NSString *jidStr = [item attributeStringValueForName:@"jid"]; + XMPPJID *jid = [XMPPJID jidWithString:jidStr]; + + if (jid == nil) + { + XMPPLogVerbose(@"XMPPUserCoreDataStorageObject: invalid item (missing or invalid jid): %@", item); + return nil; + } + + XMPPUserCoreDataStorageObject *newUser; + newUser = [NSEntityDescription insertNewObjectForEntityForName:@"XMPPUserCoreDataStorageObject" + inManagedObjectContext:moc]; + + newUser.streamBareJidStr = streamBareJidStr; + + [newUser updateWithItem:item]; + + return newUser; +} + +- (void)updateGroupsWithItem:(NSXMLElement *)item +{ + XMPPGroupCoreDataStorageObject *group = nil; + + // clear existing group memberships first + if ([self.groups count] > 0) { + [self removeGroups:self.groups]; + } + + NSArray *groupItems = [item elementsForName:@"group"]; + NSString *groupName = nil; + + for (NSXMLElement *groupElement in groupItems) { + groupName = [groupElement stringValue]; + + group = [XMPPGroupCoreDataStorageObject fetchOrInsertGroupName:groupName + inManagedObjectContext:[self managedObjectContext]]; + + if (group != nil) { + [self addGroupsObject:group]; + } + } +} + +- (void)updateWithItem:(NSXMLElement *)item +{ + NSString *jidStr = [item attributeStringValueForName:@"jid"]; + XMPPJID *jid = [XMPPJID jidWithString:jidStr]; + + if (jid == nil) + { + XMPPLogVerbose(@"XMPPUserCoreDataStorageObject: invalid item (missing or invalid jid): %@", item); + return; + } + + self.jid = jid; + self.nickname = [item attributeStringValueForName:@"name"]; + + self.displayName = self.nickname ? : jidStr; + + self.subscription = [item attributeStringValueForName:@"subscription"]; + self.ask = [item attributeStringValueForName:@"ask"]; + + [self updateGroupsWithItem:item]; +} + +- (void)recalculatePrimaryResource +{ + self.primaryResource = nil; + + NSArray *sortedResources = [[self allResources] sortedArrayUsingSelector:@selector(compare:)]; + if ([sortedResources count] > 0) + { + XMPPResourceCoreDataStorageObject *resource = sortedResources[0]; + + // Primary resource must have a non-negative priority + if ([resource priority] >= 0) + { + self.primaryResource = resource; + + if (resource.intShow >= 3) + self.section = 0; + else + self.section = 1; + } + } + + if (self.primaryResource == nil) + { + self.section = 2; + } +} + +- (void)updateWithPresence:(XMPPPresence *)presence streamBareJidStr:(NSString *)streamBareJidStr +{ + XMPPResourceCoreDataStorageObject *resource = + (XMPPResourceCoreDataStorageObject *)[self resourceForJID:[presence from]]; + + if ([[presence type] isEqualToString:@"unavailable"] || [presence isErrorPresence]) + { + if (resource) + { + [self removeResourcesObject:resource]; + [[self managedObjectContext] deleteObject:resource]; + } + } + else + { + if (resource) + { + [resource updateWithPresence:presence]; + } + else + { + XMPPResourceCoreDataStorageObject *newResource; + newResource = [XMPPResourceCoreDataStorageObject insertInManagedObjectContext:[self managedObjectContext] + withPresence:presence + streamBareJidStr:streamBareJidStr]; + + [self addResourcesObject:newResource]; + } + } + + [self recalculatePrimaryResource]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPUser Protocol +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isOnline +{ + return (self.primaryResource != nil); +} + +- (BOOL)isPendingApproval +{ + // Either of the following mean we're waiting to have our presence subscription approved: + // + // + + NSString *subscription = self.subscription; + NSString *ask = self.ask; + + if ([subscription isEqualToString:@"none"] || [subscription isEqualToString:@"from"]) + { + if ([ask isEqualToString:@"subscribe"]) + { + return YES; + } + } + + return NO; +} + +- (id )resourceForJID:(XMPPJID *)jid +{ + NSString *jidStr = [jid full]; + + for (XMPPResourceCoreDataStorageObject *resource in [self resources]) + { + if ([jidStr isEqualToString:[resource jidStr]]) + { + return resource; + } + } + + return nil; +} + +- (NSArray *)allResources +{ + NSMutableArray *allResources = [NSMutableArray array]; + + for (XMPPResourceCoreDataStorageObject *resource in [[self resources] allObjects]) { + + if(![resource isDeleted]) + { + [allResources addObject:resource]; + } + } + + return allResources; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Comparisons +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns the result of invoking compareByName:options: with no options. +**/ +- (NSComparisonResult)compareByName:(XMPPUserCoreDataStorageObject *)another +{ + return [self compareByName:another options:0]; +} + +/** + * This method compares the two users according to their display name. + * + * Options for the search — you can combine any of the following using a C bitwise OR operator: + * NSCaseInsensitiveSearch, NSLiteralSearch, NSNumericSearch. + * See "String Programming Guide for Cocoa" for details on these options. +**/ +- (NSComparisonResult)compareByName:(XMPPUserCoreDataStorageObject *)another options:(NSStringCompareOptions)mask +{ + NSString *myName = [self displayName]; + NSString *theirName = [another displayName]; + + return [myName compare:theirName options:mask]; +} + +/** + * Returns the result of invoking compareByAvailabilityName:options: with no options. +**/ +- (NSComparisonResult)compareByAvailabilityName:(XMPPUserCoreDataStorageObject *)another +{ + return [self compareByAvailabilityName:another options:0]; +} + +/** + * This method compares the two users according to availability first, and then display name. + * Thus available users come before unavailable users. + * If both users are available, or both users are not available, + * this method follows the same functionality as the compareByName:options: as documented above. +**/ +- (NSComparisonResult)compareByAvailabilityName:(XMPPUserCoreDataStorageObject *)another + options:(NSStringCompareOptions)mask +{ + if ([self isOnline]) + { + if ([another isOnline]) + return [self compareByName:another options:mask]; + else + return NSOrderedAscending; + } + else + { + if ([another isOnline]) + return NSOrderedDescending; + else + return [self compareByName:another options:mask]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark KVO compliance methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (NSSet *)keyPathsForValuesAffectingJid { + // If the jidStr changes, the jid may change as well. + return [NSSet setWithObject:@"jidStr"]; +} + ++ (NSSet *)keyPathsForValuesAffectingIsOnline { + return [NSSet setWithObject:@"primaryResource"]; +} + ++ (NSSet *)keyPathsForValuesAffectingSection { + // If the value of sectionNum changes, the section may change as well. + return [NSSet setWithObject:@"sectionNum"]; +} + ++ (NSSet *)keyPathsForValuesAffectingSectionName { + // If the value of displayName changes, the sectionName may change as well. + return [NSSet setWithObject:@"displayName"]; +} + ++ (NSSet *)keyPathsForValuesAffectingAllResources { + return [NSSet setWithObject:@"resources"]; +} + +@end diff --git a/Extensions/Roster/MemoryStorage/XMPPResourceMemoryStorageObject.h b/Extensions/Roster/MemoryStorage/XMPPResourceMemoryStorageObject.h new file mode 100644 index 0000000..9999102 --- /dev/null +++ b/Extensions/Roster/MemoryStorage/XMPPResourceMemoryStorageObject.h @@ -0,0 +1,28 @@ +#import +#import "XMPPResource.h" + +@class XMPPJID; +@class XMPPIQ; +@class XMPPPresence; + + +@interface XMPPResourceMemoryStorageObject : NSObject +{ + XMPPJID *jid; + XMPPPresence *presence; + + NSDate *presenceDate; +} + +/* From the XMPPResource protocol + +- (XMPPJID *)jid; +- (XMPPPresence *)presence; + +- (NSDate *)presenceDate; + +- (NSComparisonResult)compare:(id )another; + +*/ + +@end diff --git a/Extensions/Roster/MemoryStorage/XMPPResourceMemoryStorageObject.m b/Extensions/Roster/MemoryStorage/XMPPResourceMemoryStorageObject.m new file mode 100644 index 0000000..fa8e27d --- /dev/null +++ b/Extensions/Roster/MemoryStorage/XMPPResourceMemoryStorageObject.m @@ -0,0 +1,206 @@ +#import "XMPP.h" +#import "NSXMLElement+XEP_0203.h" +#import "XMPPRosterMemoryStoragePrivate.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation XMPPResourceMemoryStorageObject + +- (id)initWithPresence:(XMPPPresence *)aPresence +{ + if((self = [super init])) + { + jid = [aPresence from]; + presence = aPresence; + + presenceDate = [presence delayedDeliveryDate]; + if (presenceDate == nil) + { + presenceDate = [[NSDate alloc] init]; + } + } + return self; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Copying +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)copyWithZone:(NSZone *)zone +{ + // We use [self class] to support subclassing + + XMPPResourceMemoryStorageObject *deepCopy = (XMPPResourceMemoryStorageObject *)[[[self class] alloc] init]; + + deepCopy->jid = [jid copy]; + deepCopy->presence = presence; // No need to bother with a copy sicne we don't alter presence + deepCopy->presenceDate = [presenceDate copy]; + + return deepCopy; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Encoding, Decoding +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if ! TARGET_OS_IPHONE +- (id)replacementObjectForPortCoder:(NSPortCoder *)encoder +{ + if([encoder isBycopy]) + return self; + else + return [super replacementObjectForPortCoder:encoder]; + // return [NSDistantObject proxyWithLocal:self connection:[encoder connection]]; +} +#endif + +- (id)initWithCoder:(NSCoder *)coder +{ + if ((self = [super init])) + { + if([coder allowsKeyedCoding]) + { + if([coder respondsToSelector:@selector(requiresSecureCoding)] && + [coder requiresSecureCoding]) + { + jid = [coder decodeObjectOfClass:[XMPPJID class] forKey:@"jid"]; + presence = [coder decodeObjectOfClass:[XMPPPresence class] forKey:@"presence"]; + presenceDate = [coder decodeObjectOfClass:[NSDate class] forKey:@"presenceDate"]; + } + else + { + jid = [coder decodeObjectForKey:@"jid"]; + presence = [coder decodeObjectForKey:@"presence"]; + presenceDate = [coder decodeObjectForKey:@"presenceDate"]; + } + } + else + { + jid = [coder decodeObject]; + presence = [coder decodeObject]; + presenceDate = [coder decodeObject]; + } + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + if ([coder allowsKeyedCoding]) + { + [coder encodeObject:jid forKey:@"jid"]; + [coder encodeObject:presence forKey:@"presence"]; + [coder encodeObject:presenceDate forKey:@"presenceDate"]; + } + else + { + [coder encodeObject:jid]; + [coder encodeObject:presence]; + [coder encodeObject:presenceDate]; + } +} + ++ (BOOL) supportsSecureCoding +{ + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Standard Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (XMPPJID *)jid +{ + return jid; +} + +- (XMPPPresence *)presence +{ + return presence; +} + +- (NSDate *)presenceDate +{ + return presenceDate; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Update Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)updateWithPresence:(XMPPPresence *)aPresence +{ + presence = aPresence; + + presenceDate = [presence delayedDeliveryDate]; + if (presenceDate == nil) + { + presenceDate = [[NSDate alloc] init]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Comparison Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSComparisonResult)compare:(id )another +{ + XMPPPresence *mp = [self presence]; + XMPPPresence *ap = [another presence]; + + int mpp = [mp priority]; + int app = [ap priority]; + + if(mpp < app) + return NSOrderedDescending; + if(mpp > app) + return NSOrderedAscending; + + // Priority is the same. + // Determine who is more available based on their show. + int mps = [mp intShow]; + int aps = [ap intShow]; + + if(mps < aps) + return NSOrderedDescending; + if(mps > aps) + return NSOrderedAscending; + + // Priority and Show are the same. + // Determine based on who was the last to receive a presence element. + NSDate *mpd = [self presenceDate]; + NSDate *apd = [another presenceDate]; + + return [mpd compare:apd]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark NSObject Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSUInteger)hash +{ + return [jid hash]; +} + +- (BOOL)isEqual:(id)anObject +{ + if([anObject isMemberOfClass:[self class]]) + { + XMPPResourceMemoryStorageObject *another = (XMPPResourceMemoryStorageObject *)anObject; + + return [jid isEqualToJID:[another jid]]; + } + + return NO; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"", self, [jid full]]; +} + +@end diff --git a/Extensions/Roster/MemoryStorage/XMPPRosterMemoryStorage.h b/Extensions/Roster/MemoryStorage/XMPPRosterMemoryStorage.h new file mode 100644 index 0000000..5b64615 --- /dev/null +++ b/Extensions/Roster/MemoryStorage/XMPPRosterMemoryStorage.h @@ -0,0 +1,128 @@ +#import +#import "XMPPRoster.h" +#import "XMPPUserMemoryStorageObject.h" +#import "XMPPResourceMemoryStorageObject.h" + + +/** + * This class is an example implementation of the XMPPRosterStorage protocol. + * It simply keeps all roster informatin in memory. + * + * You are free to substitute your own storage class. +**/ + +@interface XMPPRosterMemoryStorage : NSObject +{ + #if __has_feature(objc_arc_weak) + __weak XMPPRoster *parent; + #else + __unsafe_unretained XMPPRoster *parent; + #endif + dispatch_queue_t parentQueue; + void *parentQueueTag; + + Class userClass; + Class resourceClass; + + BOOL isRosterPopulation; + NSMutableDictionary *roster; + + XMPPJID *myJID; + XMPPUserMemoryStorageObject *myUser; +} + +- (id)init; + +@property (readonly) XMPPRoster *parent; + +/** + * You can optionally extend the XMPPUserMemoryStorage and XMPPResourceMemoryStorage classes. + * Then just set the classes here, and your subclasses will automatically get used. +**/ +@property (readwrite, assign) Class userClass; +@property (readwrite, assign) Class resourceClass; + +/** + * The methods below provide access to the roster data. + * + * If invoked from a dispatch queue other than the roster's queue, + * the methods return snapshots (copies) of the roster data. + * These snapshots provide a thread-safe version of the roster data. + * The thread-safety comes from the fact that the copied data will not be altered, + * so it can therefore be used from multiple threads/queues if needed. +**/ + +- (XMPPUserMemoryStorageObject *)myUser; +- (XMPPResourceMemoryStorageObject *)myResource; + +- (XMPPUserMemoryStorageObject *)userForJID:(XMPPJID *)jid; +- (XMPPResourceMemoryStorageObject *)resourceForJID:(XMPPJID *)jid; + +- (NSArray *)sortedUsersByName; +- (NSArray *)sortedUsersByAvailabilityName; + +- (NSArray *)sortedAvailableUsersByName; +- (NSArray *)sortedUnavailableUsersByName; + +- (NSArray *)unsortedUsers; +- (NSArray *)unsortedAvailableUsers; +- (NSArray *)unsortedUnavailableUsers; + +- (NSArray *)sortedResources:(BOOL)includeResourcesForMyUserExcludingMyself; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPRosterMemoryStorageDelegate +@optional + +/** + * The XMPPRosterStorage classes use the same delegate as their parent XMPPRoster. +**/ + +/** + * Catch-all change notification. + * + * When the roster changes, for any of the reasons listed below, this delegate method fires. + * This method always fires after the more precise delegate methods listed below. +**/ +- (void)xmppRosterDidChange:(XMPPRosterMemoryStorage *)sender; + +/** + * Notification that the roster has received the roster from the server. + * + * If parent.autoFetchRoster is YES, the roster will automatically be fetched once the user authenticates. +**/ +- (void)xmppRosterDidPopulate:(XMPPRosterMemoryStorage *)sender; + +/** + * Notifications that the roster has changed. + * + * This includes times when users are added or removed from our roster, or when a nickname is changed, + * including when other resources logged in under the same user account as us make changes to our roster. + * + * This does not include when resources simply go online / offline. +**/ +- (void)xmppRoster:(XMPPRosterMemoryStorage *)sender didAddUser:(XMPPUserMemoryStorageObject *)user; +- (void)xmppRoster:(XMPPRosterMemoryStorage *)sender didUpdateUser:(XMPPUserMemoryStorageObject *)user; +- (void)xmppRoster:(XMPPRosterMemoryStorage *)sender didRemoveUser:(XMPPUserMemoryStorageObject *)user; + +/** + * Notifications when resources go online / offline. +**/ +- (void)xmppRoster:(XMPPRosterMemoryStorage *)sender + didAddResource:(XMPPResourceMemoryStorageObject *)resource + withUser:(XMPPUserMemoryStorageObject *)user; + +- (void)xmppRoster:(XMPPRosterMemoryStorage *)sender + didUpdateResource:(XMPPResourceMemoryStorageObject *)resource + withUser:(XMPPUserMemoryStorageObject *)user; + +- (void)xmppRoster:(XMPPRosterMemoryStorage *)sender + didRemoveResource:(XMPPResourceMemoryStorageObject *)resource + withUser:(XMPPUserMemoryStorageObject *)user; + +@end diff --git a/Extensions/Roster/MemoryStorage/XMPPRosterMemoryStorage.m b/Extensions/Roster/MemoryStorage/XMPPRosterMemoryStorage.m new file mode 100644 index 0000000..cd6e457 --- /dev/null +++ b/Extensions/Roster/MemoryStorage/XMPPRosterMemoryStorage.m @@ -0,0 +1,860 @@ +#import "XMPP.h" +#import "XMPPRosterPrivate.h" +#import "XMPPRosterMemoryStorage.h" +#import "XMPPRosterMemoryStoragePrivate.h" +#import "XMPPLogging.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +#define AssertPrivateQueue() \ + NSAssert(dispatch_get_specific(parentQueueTag), @"Private method: MUST run on parentQueue"); + +#define AssertParentQueue() \ + NSAssert(dispatch_get_specific(parentQueueTag), @"Private protocol method: MUST run on parentQueue"); + +@interface XMPPRosterMemoryStorage () + +@property (readonly) dispatch_queue_t parentQueue; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPRosterMemoryStorage + +- (id)init +{ + if ((self = [super init])) + { + userClass = [XMPPUserMemoryStorageObject class]; + resourceClass = [XMPPResourceMemoryStorageObject class]; + + roster = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (BOOL)configureWithParent:(XMPPRoster *)aParent queue:(dispatch_queue_t)queue +{ + NSParameterAssert(aParent != nil); + NSParameterAssert(queue != NULL); + + @synchronized(self) + { + if ((parent == nil) && (parentQueue == NULL)) + { + parent = aParent; + parentQueue = queue; + parentQueueTag = &parentQueueTag; + dispatch_queue_set_specific(parentQueue, parentQueueTag, parentQueueTag, NULL); + + #if !OS_OBJECT_USE_OBJC + dispatch_retain(parentQueue); + #endif + + return YES; + } + } + + return NO; +} + +- (void)dealloc +{ + #if !OS_OBJECT_USE_OBJC + if (parentQueue) + dispatch_release(parentQueue); + #endif +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@synthesize userClass; +@synthesize resourceClass; + +- (XMPPRoster *)parent +{ + XMPPRoster *result = nil; + + @synchronized(self) // synchronized with configureWithParent:queue: + { + result = parent; + } + + return result; +} + +- (dispatch_queue_t)parentQueue +{ + dispatch_queue_t result = NULL; + + @synchronized(self) // synchronized with configureWithParent:queue: + { + result = parentQueue; + } + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Internal API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (GCDMulticastDelegate *)multicastDelegate +{ + return (GCDMulticastDelegate *)[parent multicastDelegate]; +} + +- (XMPPUserMemoryStorageObject *)_userForJID:(XMPPJID *)jid +{ + AssertPrivateQueue(); + + XMPPUserMemoryStorageObject *result = roster[[jid bareJID]]; + + if (result) + { + return result; + } + + XMPPJID *myBareJID = [myJID bareJID]; + XMPPJID *bareJID = [jid bareJID]; + + if ([bareJID isEqualToJID:myBareJID]) + { + return myUser; + } + + return nil; +} + +- (XMPPResourceMemoryStorageObject *)_resourceForJID:(XMPPJID *)jid +{ + AssertPrivateQueue(); + + XMPPUserMemoryStorageObject *user = [self _userForJID:jid]; + return (XMPPResourceMemoryStorageObject *)[user resourceForJID:jid]; +} + +- (NSArray *)_unsortedUsers +{ + AssertPrivateQueue(); + + return [roster allValues]; +} + +- (NSArray *)_unsortedAvailableUsers +{ + AssertPrivateQueue(); + + NSArray *allUsers = [roster allValues]; + + NSMutableArray *result = [NSMutableArray arrayWithCapacity:[allUsers count]]; + + for (id user in allUsers) + { + if ([user isOnline]) + { + [result addObject:user]; + } + } + + return result; +} + +- (NSArray *)_unsortedUnavailableUsers +{ + AssertPrivateQueue(); + + NSArray *allUsers = [roster allValues]; + + NSMutableArray *result = [NSMutableArray arrayWithCapacity:[allUsers count]]; + + for (id user in allUsers) + { + if (![user isOnline]) + { + [result addObject:user]; + } + } + + return result; +} + +- (NSArray *)_sortedUsersByName +{ + AssertPrivateQueue(); + + return [[roster allValues] sortedArrayUsingSelector:@selector(compareByName:)]; +} + +- (NSArray *)_sortedUsersByAvailabilityName +{ + AssertPrivateQueue(); + + return [[roster allValues] sortedArrayUsingSelector:@selector(compareByAvailabilityName:)]; +} + +- (NSArray *)_sortedAvailableUsersByName +{ + AssertPrivateQueue(); + + return [[self _unsortedAvailableUsers] sortedArrayUsingSelector:@selector(compareByName:)]; +} + +- (NSArray *)_sortedUnavailableUsersByName +{ + AssertPrivateQueue(); + + return [[self _unsortedUnavailableUsers] sortedArrayUsingSelector:@selector(compareByName:)]; +} + +- (NSArray *)_sortedResources:(BOOL)includeResourcesForMyUserExcludingMyself +{ + AssertPrivateQueue(); + + // Add all the resouces from all the available users in the roster + // + // Remember: There may be multiple resources per user + + NSArray *availableUsers = [self unsortedAvailableUsers]; + + NSMutableArray *result = [NSMutableArray arrayWithCapacity:[availableUsers count]]; + + for (id user in availableUsers) + { + [result addObjectsFromArray:[user allResources]]; + } + + if (includeResourcesForMyUserExcludingMyself) + { + // Now add all the available resources from our own user account (excluding ourselves) + + NSArray *myResources = [myUser allResources]; + + for (id resource in myResources) + { + if (![myJID isEqualToJID:[resource jid]]) + { + [result addObject:resource]; + } + } + } + + return [result sortedArrayUsingSelector:@selector(compare:)]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Roster Management +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (XMPPUserMemoryStorageObject *)myUser +{ + // This is a public method, so it may be invoked on any thread/queue. + + if (self.parentQueue == NULL) + { + // Haven't been attached to parent yet + return nil; + } + + if (dispatch_get_specific(parentQueueTag)) + { + return myUser; + } + else + { + __block XMPPUserMemoryStorageObject *result; + + dispatch_sync(parentQueue, ^{ + result = [myUser copy]; + }); + + return result; + } +} + +- (XMPPResourceMemoryStorageObject *)myResource +{ + // This is a public method, so it may be invoked on any thread/queue. + + if (self.parentQueue == NULL) + { + // Haven't been attached to parent yet + return nil; + } + + if (dispatch_get_specific(parentQueueTag)) + { + return (XMPPResourceMemoryStorageObject *)[myUser resourceForJID:myJID]; + } + else + { + __block XMPPResourceMemoryStorageObject *result; + + dispatch_sync(parentQueue, ^{ + XMPPResourceMemoryStorageObject *resource = + (XMPPResourceMemoryStorageObject *)[myUser resourceForJID:myJID]; + result = [resource copy]; + }); + + return result; + } +} + +- (XMPPUserMemoryStorageObject *)userForJID:(XMPPJID *)jid +{ + // This is a public method, so it may be invoked on any thread/queue. + + if (self.parentQueue == NULL) + { + // Haven't been attached to parent yet + return nil; + } + + if (dispatch_get_specific(parentQueueTag)) + { + return [self _userForJID:jid]; + } + else + { + __block XMPPUserMemoryStorageObject *result; + + dispatch_sync(parentQueue, ^{ @autoreleasepool { + + XMPPUserMemoryStorageObject *user = [self _userForJID:jid]; + result = [user copy]; + + }}); + + return result; + } +} + +- (XMPPResourceMemoryStorageObject *)resourceForJID:(XMPPJID *)jid +{ + // This is a public method, so it may be invoked on any thread/queue. + + if (self.parentQueue == NULL) + { + // Haven't been attached to parent yet + return nil; + } + + if (dispatch_get_specific(parentQueueTag)) + { + return [self _resourceForJID:jid]; + } + else + { + __block XMPPResourceMemoryStorageObject *result; + + dispatch_sync(parentQueue, ^{ @autoreleasepool { + + XMPPResourceMemoryStorageObject *resource = [self _resourceForJID:jid]; + result = [resource copy]; + + }}); + + return result; + } +} + +- (NSArray *)sortedUsersByName +{ + // This is a public method, so it may be invoked on any thread/queue. + + if (self.parentQueue == NULL) + { + // Haven't been attached to parent yet + return nil; + } + + if (dispatch_get_specific(parentQueueTag)) + { + return [self _sortedUsersByName]; + } + else + { + __block NSArray *result; + + dispatch_sync(parentQueue, ^{ @autoreleasepool { + + NSArray *temp = [self _sortedUsersByName]; + + result = [[NSArray alloc] initWithArray:temp copyItems:YES]; + + }}); + + return result; + } +} + +- (NSArray *)sortedUsersByAvailabilityName +{ + // This is a public method, so it may be invoked on any thread/queue. + + if (self.parentQueue == NULL) + { + // Haven't been attached to parent yet + return nil; + } + + if (dispatch_get_specific(parentQueueTag)) + { + return [self _sortedUsersByAvailabilityName]; + } + else + { + __block NSArray *result; + + dispatch_sync(parentQueue, ^{ @autoreleasepool { + + NSArray *temp = [self _sortedUsersByAvailabilityName]; + + result = [[NSArray alloc] initWithArray:temp copyItems:YES]; + + }}); + + return result; + } +} + +- (NSArray *)sortedAvailableUsersByName +{ + // This is a public method, so it may be invoked on any thread/queue. + + if (self.parentQueue == NULL) + { + // Haven't been attached to parent yet + return nil; + } + + if (dispatch_get_specific(parentQueueTag)) + { + return [self _sortedAvailableUsersByName]; + } + else + { + __block NSArray *result; + + dispatch_sync(parentQueue, ^{ @autoreleasepool { + + NSArray *temp = [self _sortedAvailableUsersByName]; + result = [[NSArray alloc] initWithArray:temp copyItems:YES]; + + }}); + + return result; + } +} + +- (NSArray *)sortedUnavailableUsersByName +{ + // This is a public method, so it may be invoked on any thread/queue. + + if (self.parentQueue == NULL) + { + // Haven't been attached to parent yet + return nil; + } + + if (dispatch_get_specific(parentQueueTag)) + { + return [self _sortedUnavailableUsersByName]; + } + else + { + __block NSArray *result; + + dispatch_sync(parentQueue, ^{ @autoreleasepool { + + NSArray *temp = [self _sortedUnavailableUsersByName]; + result = [[NSArray alloc] initWithArray:temp copyItems:YES]; + + }}); + + return result; + } +} + +- (NSArray *)unsortedUsers +{ + // This is a public method, so it may be invoked on any thread/queue. + + if (self.parentQueue == NULL) + { + // Haven't been attached to parent yet + return nil; + } + + if (dispatch_get_specific(parentQueueTag)) + { + return [self _unsortedUsers]; + } + else + { + __block NSArray *result; + + dispatch_sync(parentQueue, ^{ @autoreleasepool { + + NSArray *temp = [self _unsortedUsers]; + result = [[NSArray alloc] initWithArray:temp copyItems:YES]; + + }}); + + return result; + } +} + +- (NSArray *)unsortedAvailableUsers +{ + // This is a public method, so it may be invoked on any thread/queue. + + if (self.parentQueue == NULL) + { + // Haven't been attached to parent yet + return nil; + } + + if (dispatch_get_specific(parentQueueTag)) + { + return [self _unsortedAvailableUsers]; + } + else + { + __block NSArray *result; + + dispatch_sync(parentQueue, ^{ @autoreleasepool { + + NSArray *temp = [self _unsortedAvailableUsers]; + result = [[NSArray alloc] initWithArray:temp copyItems:YES]; + + }}); + + return result; + } +} + +- (NSArray *)unsortedUnavailableUsers +{ + // This is a public method, so it may be invoked on any thread/queue. + + if (self.parentQueue == NULL) + { + // Haven't been attached to parent yet + return nil; + } + + if (dispatch_get_specific(parentQueueTag)) + { + return [self _unsortedUnavailableUsers]; + } + else + { + __block NSArray *result; + + dispatch_sync(parentQueue, ^{ @autoreleasepool { + + NSArray *temp = [self _unsortedUnavailableUsers]; + result = [[NSArray alloc] initWithArray:temp copyItems:YES]; + + }}); + + return result; + } +} + +- (NSArray *)sortedResources:(BOOL)includeResourcesForMyUserExcludingMyself +{ + // This is a public method, so it may be invoked on any thread/queue. + + if (self.parentQueue == NULL) + { + // Haven't been attached to parent yet + return nil; + } + + if (dispatch_get_specific(parentQueueTag)) + { + return [self _sortedResources:includeResourcesForMyUserExcludingMyself]; + } + else + { + __block NSArray *result; + + dispatch_sync(parentQueue, ^{ @autoreleasepool { + + NSArray *temp = [self _sortedResources:includeResourcesForMyUserExcludingMyself]; + result = [[NSArray alloc] initWithArray:temp copyItems:YES]; + + }}); + + return result; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPRosterStorage Protocol +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)beginRosterPopulationForXMPPStream:(XMPPStream *)stream withVersion:(NSString *)version +{ + XMPPLogTrace(); + AssertParentQueue(); + + isRosterPopulation = YES; + + myJID = self.parent.xmppStream.myJID; + + myUser = [[self.userClass alloc] initWithJID:myJID]; +} + +- (void)endRosterPopulationForXMPPStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + AssertParentQueue(); + + isRosterPopulation = NO; + + [[self multicastDelegate] xmppRosterDidPopulate:self]; + [[self multicastDelegate] xmppRosterDidChange:self]; +} + +- (void)handleRosterItem:(NSXMLElement *)item xmppStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + AssertParentQueue(); + + NSString *jidStr = [item attributeStringValueForName:@"jid"]; + XMPPJID *jid = [[XMPPJID jidWithString:jidStr] bareJID]; + + if (isRosterPopulation) + { + XMPPUserMemoryStorageObject *newUser = + (XMPPUserMemoryStorageObject *)[[self.userClass alloc] initWithItem:item]; + + roster[jid] = newUser; + + XMPPLogVerbose(@"roster(%lu): %@", (unsigned long)[roster count], roster); + } + else + { + NSString *subscription = [item attributeStringValueForName:@"subscription"]; + + if ([subscription isEqualToString:@"remove"]) + { + XMPPUserMemoryStorageObject *user = roster[jid]; + if (user) + { + [roster removeObjectForKey:jid]; + + XMPPLogVerbose(@"roster(%lu): %@", (unsigned long)[roster count], roster); + + [[self multicastDelegate] xmppRoster:self didRemoveUser:user]; + [[self multicastDelegate] xmppRosterDidChange:self]; + } + } + else + { + XMPPUserMemoryStorageObject *user = roster[jid]; + if (user) + { + [user updateWithItem:item]; + + XMPPLogVerbose(@"roster(%lu): %@", (unsigned long)[roster count], roster); + + [[self multicastDelegate] xmppRoster:self didUpdateUser:user]; + [[self multicastDelegate] xmppRosterDidChange:self]; + } + else + { + XMPPUserMemoryStorageObject *newUser = + (XMPPUserMemoryStorageObject *)[[self.userClass alloc] initWithItem:item]; + + roster[jid] = newUser; + + XMPPLogVerbose(@"roster(%lu): %@", (unsigned long)[roster count], roster); + + [[self multicastDelegate] xmppRoster:self didAddUser:newUser]; + [[self multicastDelegate] xmppRosterDidChange:self]; + + } + } + } +} + +- (void)handlePresence:(XMPPPresence *)presence xmppStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + AssertParentQueue(); + + int change = XMPP_USER_NO_CHANGE; + + XMPPUserMemoryStorageObject *user = nil; + XMPPResourceMemoryStorageObject *resource = nil; + + XMPPJID *jidKey = [[presence from] bareJID]; + + user = roster[jidKey]; + if (user == nil) + { + // Not a presence element from anyone in our roster (that we know of). + + if ([[myJID bareJID] isEqualToJID:jidKey]) + { + // It's a presence element for our user, either our resource or another resource. + + user = myUser; + } + else if([parent allowRosterlessOperation]) + { + // Unknown user (this is the first time we've encountered them). + // This happens if the roster is in rosterlessOperation mode. + + user = (XMPPUserMemoryStorageObject *)[[self.userClass alloc] initWithJID:jidKey]; + + roster[jidKey] = user; + + [[self multicastDelegate] xmppRoster:self didAddUser:user]; + [[self multicastDelegate] xmppRosterDidChange:self]; + } + } + + change = [user updateWithPresence:presence resourceClass:self.resourceClass andGetResource:&resource]; + + XMPPLogVerbose(@"roster(%lu): %@", (unsigned long)[roster count], roster); + + if (change == XMPP_USER_ADDED_RESOURCE) + [[self multicastDelegate] xmppRoster:self didAddResource:resource withUser:user]; + + if (change == XMPP_USER_UPDATED_RESOURCE) + [[self multicastDelegate] xmppRoster:self didUpdateResource:resource withUser:user]; + + if (change == XMPP_USER_REMOVED_RESOURCE) + [[self multicastDelegate] xmppRoster:self didRemoveResource:resource withUser:user]; + + if (change != XMPP_USER_NO_CHANGE) + [[self multicastDelegate] xmppRosterDidChange:self]; +} + +- (BOOL)userExistsWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + AssertParentQueue(); + + XMPPJID *jidKey = [jid bareJID]; + XMPPUserMemoryStorageObject *rosterUser = roster[jidKey]; + + return (rosterUser != nil); +} + +#if TARGET_OS_IPHONE +- (void)setPhoto:(UIImage *)photo forUserWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream +#else +- (void)setPhoto:(NSImage *)photo forUserWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream +#endif +{ + XMPPLogTrace(); + AssertParentQueue(); + + XMPPJID *jidKey = [jid bareJID]; + XMPPUserMemoryStorageObject *rosterUser = roster[jidKey]; + + if (rosterUser) + { + [rosterUser setPhoto:photo]; + } +} + +- (void)clearAllResourcesForXMPPStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + AssertParentQueue(); + + for (XMPPUserMemoryStorageObject *user in [roster objectEnumerator]) + { + [user clearAllResources]; + } + + [[self multicastDelegate] xmppRosterDidChange:self]; +} + +- (void)clearAllUsersAndResourcesForXMPPStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + AssertParentQueue(); + + [roster removeAllObjects]; + + myUser = nil; + + [[self multicastDelegate] xmppRosterDidChange:self]; +} + +- (NSArray *)jidsForXMPPStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + AssertParentQueue(); + + NSMutableArray *results = [NSMutableArray array]; + + for (XMPPUserMemoryStorageObject *user in [roster objectEnumerator]) + { + [results addObject:[user.jid bareJID]]; + } + + return results; +} + +- (void)getSubscription:(NSString **)subscription + ask:(NSString **)ask + nickname:(NSString **)nickname + groups:(NSArray **)groups + forJID:(XMPPJID *)jid + xmppStream:(XMPPStream *)stream +{ + + XMPPLogTrace(); + AssertParentQueue(); + + XMPPJID *jidKey = [jid bareJID]; + XMPPUserMemoryStorageObject *rosterUser = roster[jidKey]; + + if(rosterUser) + { + if(subscription) + { + *subscription = rosterUser.subscription; + } + + if(ask) + { + *ask = rosterUser.ask; + } + + if(nickname) + { + *nickname = rosterUser.nickname; + } + + if(groups) + { + *groups = rosterUser.groups; + } + } +} + +@end diff --git a/Extensions/Roster/MemoryStorage/XMPPRosterMemoryStoragePrivate.h b/Extensions/Roster/MemoryStorage/XMPPRosterMemoryStoragePrivate.h new file mode 100644 index 0000000..c6f019d --- /dev/null +++ b/Extensions/Roster/MemoryStorage/XMPPRosterMemoryStoragePrivate.h @@ -0,0 +1,50 @@ +#import "XMPPUserMemoryStorageObject.h" +#import "XMPPResourceMemoryStorageObject.h" + +/** + * The following methods are designed to be invoked ONLY from + * within the XMPPRosterMemoryStorage implementation. + * + * Warning: XMPPUserMemoryStorage and XMPPResourceMemoryStorage are not explicitly thread-safe. + * Only copies that are no longer being actively + * altered by the XMPPRosterMemoryStorage class should be considered read-only and therefore thread-safe. +**/ + +#define XMPP_USER_NO_CHANGE 0 +#define XMPP_USER_ADDED_RESOURCE 1 +#define XMPP_USER_UPDATED_RESOURCE 2 +#define XMPP_USER_REMOVED_RESOURCE 3 + + +@interface XMPPUserMemoryStorageObject () + +- (void)commonInit; + +- (id)initWithJID:(XMPPJID *)aJid; +- (id)initWithItem:(NSXMLElement *)item; + +- (void)updateWithItem:(NSXMLElement *)item; + +- (int)updateWithPresence:(XMPPPresence *)presence + resourceClass:(Class)resourceClass + andGetResource:(XMPPResourceMemoryStorageObject **)resourcePtr; + +- (void)clearAllResources; + +#if TARGET_OS_IPHONE +@property (nonatomic, strong, readwrite) UIImage *photo; +#else +@property (nonatomic, strong, readwrite) NSImage *photo; +#endif + +@end + +@interface XMPPResourceMemoryStorageObject () + +- (id)initWithPresence:(XMPPPresence *)aPresence; + +- (void)updateWithPresence:(XMPPPresence *)presence; + +- (XMPPPresence *)presence; + +@end diff --git a/Extensions/Roster/MemoryStorage/XMPPUserMemoryStorageObject.h b/Extensions/Roster/MemoryStorage/XMPPUserMemoryStorageObject.h new file mode 100644 index 0000000..3841df9 --- /dev/null +++ b/Extensions/Roster/MemoryStorage/XMPPUserMemoryStorageObject.h @@ -0,0 +1,82 @@ +#import +#import "XMPPUser.h" +#import "XMPP.h" + +#if TARGET_OS_IPHONE + #import +#else + #import +#endif + +@class XMPPResourceMemoryStorageObject; + + +@interface XMPPUserMemoryStorageObject : NSObject +{ + XMPPJID *jid; + NSMutableDictionary *itemAttributes; + NSMutableArray *groups; + + NSMutableDictionary *resources; + XMPPResourceMemoryStorageObject *primaryResource; + +#if TARGET_OS_IPHONE + UIImage *photo; +#else + NSImage *photo; +#endif +} + +/* From the XMPPUser protocol + +- (XMPPJID *)jid; +- (NSString *)nickname; + +- (BOOL)isOnline; +- (BOOL)isPendingApproval; + +- (id )primaryResource; +- (id )resourceForJID:(XMPPJID *)jid; + +- (NSArray *)allResources; + +*/ + +- (NSString *)subscription; + +- (NSString *)ask; + +/** + * Simple convenience method. + * If a nickname exists for the user, the nickname is returned. + * Otherwise the jid is returned (as a string). +**/ +- (NSString *)displayName; + +/** + * An array of Group Names. +**/ +- (NSArray *)groups; + +/** + * If XMPPvCardAvatarModule is included in the framework, the XMPPRoster will automatically integrate with it, + * and we'll save the the user photos after they've been downloaded. +**/ +#if TARGET_OS_IPHONE +@property (nonatomic, strong, readonly) UIImage *photo; +#else +@property (nonatomic, strong, readonly) NSImage *photo; +#endif + +/** + * Simple comparison methods. +**/ + +- (NSComparisonResult)compareByName:(XMPPUserMemoryStorageObject *)another; +- (NSComparisonResult)compareByName:(XMPPUserMemoryStorageObject *)another options:(NSStringCompareOptions)mask; + +- (NSComparisonResult)compareByAvailabilityName:(XMPPUserMemoryStorageObject *)another; +- (NSComparisonResult)compareByAvailabilityName:(XMPPUserMemoryStorageObject *)another + options:(NSStringCompareOptions)mask; + +@end diff --git a/Extensions/Roster/MemoryStorage/XMPPUserMemoryStorageObject.m b/Extensions/Roster/MemoryStorage/XMPPUserMemoryStorageObject.m new file mode 100644 index 0000000..14633d1 --- /dev/null +++ b/Extensions/Roster/MemoryStorage/XMPPUserMemoryStorageObject.m @@ -0,0 +1,512 @@ +#import "XMPP.h" +#import "XMPPRosterMemoryStoragePrivate.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +@interface XMPPUserMemoryStorageObject (PrivateAPI) +- (void)recalculatePrimaryResource; +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPUserMemoryStorageObject + +- (void)commonInit +{ + // This method is here to more easily support subclassing. + // That way subclasses can optionally override just commonInit, instead of each individual init method. + // + // If you override this method, don't forget to invoke [super commonInit]; + + resources = [[NSMutableDictionary alloc] initWithCapacity:1]; +} + +- (id)initWithJID:(XMPPJID *)aJid +{ + if ((self = [super init])) + { + jid = [aJid bareJID]; + + itemAttributes = [[NSMutableDictionary alloc] initWithCapacity:0]; + + groups = [[NSMutableArray alloc] initWithCapacity:0]; + + [self commonInit]; + } + return self; +} + +- (id)initWithItem:(NSXMLElement *)item +{ + if ((self = [super init])) + { + NSString *jidStr = [item attributeStringValueForName:@"jid"]; + jid = [[XMPPJID jidWithString:jidStr] bareJID]; + + itemAttributes = [item attributesAsDictionary]; + + groups = [[NSMutableArray alloc] initWithCapacity:0]; + + NSArray *groupElements = [item elementsForName:@"group"]; + + for (NSXMLElement *groupElement in groupElements) { + NSString *groupName = [groupElement stringValue]; + + if ([groupName length]) + { + [groups addObject:groupName]; + } + } + + [self commonInit]; + } + return self; +} + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Copying +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)copyWithZone:(NSZone *)zone +{ + // We use [self class] to support subclassing + + XMPPUserMemoryStorageObject *deepCopy = (XMPPUserMemoryStorageObject *)[[[self class] alloc] init]; + + deepCopy->jid = [jid copy]; + deepCopy->itemAttributes = [itemAttributes mutableCopy]; + deepCopy->groups = [groups mutableCopy]; + + deepCopy->resources = [[NSMutableDictionary alloc] initWithCapacity:[resources count]]; + + for (XMPPJID *key in resources) + { + XMPPResourceMemoryStorageObject *resourceCopy = [resources[key] copy]; + + deepCopy->resources[key] = resourceCopy; + } + + [deepCopy recalculatePrimaryResource]; + + return deepCopy; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Encoding, Decoding +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if ! TARGET_OS_IPHONE +- (id)replacementObjectForPortCoder:(NSPortCoder *)encoder +{ + if ([encoder isBycopy]) + return self; + else + return [super replacementObjectForPortCoder:encoder]; + // return [NSDistantObject proxyWithLocal:self connection:[encoder connection]]; +} +#endif + +- (id)initWithCoder:(NSCoder *)coder +{ + if ((self = [super init])) + { + if ([coder allowsKeyedCoding]) + { + if([coder respondsToSelector:@selector(requiresSecureCoding)] && + [coder requiresSecureCoding]) + { + jid = [coder decodeObjectOfClass:[XMPPJID class] forKey:@"jid"]; + itemAttributes = [[coder decodeObjectOfClass:[NSDictionary class] forKey:@"itemAttributes"] mutableCopy]; + groups = [[coder decodeObjectOfClass:[NSArray class] forKey:@"groups"] mutableCopy]; +#if TARGET_OS_IPHONE + photo = [[UIImage alloc] initWithData:[coder decodeObjectOfClass:[NSData class] forKey:@"photo"]]; +#else + photo = [[NSImage alloc] initWithData:[coder decodeObjectOfClass:[NSData class] forKey:@"photo"]]; +#endif + resources = [[coder decodeObjectOfClass:[NSDictionary class] forKey:@"resources"] mutableCopy]; + primaryResource = [coder decodeObjectOfClass:[XMPPResourceMemoryStorageObject class] forKey:@"primaryResource"]; + } + else + { + jid = [coder decodeObjectForKey:@"jid"]; + itemAttributes = [[coder decodeObjectForKey:@"itemAttributes"] mutableCopy]; + groups = [[coder decodeObjectForKey:@"groups"] mutableCopy]; +#if TARGET_OS_IPHONE + photo = [[UIImage alloc] initWithData:[coder decodeObjectForKey:@"photo"]]; +#else + photo = [[NSImage alloc] initWithData:[coder decodeObjectForKey:@"photo"]]; +#endif + resources = [[coder decodeObjectForKey:@"resources"] mutableCopy]; + primaryResource = [coder decodeObjectForKey:@"primaryResource"]; + } + } + else + { + jid = [coder decodeObject]; + itemAttributes = [[coder decodeObject] mutableCopy]; + groups = [[coder decodeObject] mutableCopy]; + #if TARGET_OS_IPHONE + photo = [[UIImage alloc] initWithData:[coder decodeObject]]; + #else + photo = [[NSImage alloc] initWithData:[coder decodeObject]]; + #endif + resources = [[coder decodeObject] mutableCopy]; + primaryResource = [coder decodeObject]; + } + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + if ([coder allowsKeyedCoding]) + { + [coder encodeObject:jid forKey:@"jid"]; + [coder encodeObject:itemAttributes forKey:@"itemAttributes"]; + [coder encodeObject:groups forKey:@"groups"]; + #if TARGET_OS_IPHONE + [coder encodeObject:UIImagePNGRepresentation(photo) forKey:@"photo"]; + #else + [coder encodeObject:[photo TIFFRepresentation] forKey:@"photo"]; + #endif + [coder encodeObject:resources forKey:@"resources"]; + [coder encodeObject:primaryResource forKey:@"primaryResource"]; + } + else + { + [coder encodeObject:jid]; + [coder encodeObject:itemAttributes]; + [coder encodeObject:groups]; + #if TARGET_OS_IPHONE + [coder encodeObject:UIImagePNGRepresentation(photo)]; + #else + [coder encodeObject:[photo TIFFRepresentation]]; + #endif + [coder encodeObject:resources]; + [coder encodeObject:primaryResource]; + } +} + ++ (BOOL) supportsSecureCoding +{ + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Standard Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@synthesize photo; + +- (XMPPJID *)jid +{ + return jid; +} + +- (NSString *)nickname +{ + return (NSString *) itemAttributes[@"name"]; +} + +- (NSString *)subscription +{ + return (NSString *) itemAttributes[@"subscription"]; +} + +- (NSString *)ask +{ + return (NSString *) itemAttributes[@"ask"]; +} + +- (NSString *)displayName +{ + NSString *nickname = [self nickname]; + if (nickname) + return nickname; + else + return [jid bare]; +} + +- (NSArray *)groups +{ + return [groups copy]; +} + +- (BOOL)isOnline +{ + return (primaryResource != nil); +} + +- (BOOL)isPendingApproval +{ + // Either of the following mean we're waiting to have our presence subscription approved: + // + // + + NSString *subscription = itemAttributes[@"subscription"]; + NSString *ask = itemAttributes[@"ask"]; + + if ([subscription isEqualToString:@"none"] || [subscription isEqualToString:@"from"]) + { + if([ask isEqualToString:@"subscribe"]) + { + return YES; + } + } + + return NO; +} + +- (id )primaryResource +{ + return primaryResource; +} + +- (id )resourceForJID:(XMPPJID *)aJid +{ + return resources[aJid]; +} + +- (NSArray *)allResources +{ + return [resources allValues]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Hooks +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)didAddResource:(XMPPResourceMemoryStorageObject *)resource withPresence:(XMPPPresence *)presence +{ + // Override / customization hook +} + +- (void)willUpdateResource:(XMPPResourceMemoryStorageObject *)resource withPresence:(XMPPPresence *)presence +{ + // Override / customization hook +} + +- (void)didUpdateResource:(XMPPResourceMemoryStorageObject *)resource withPresence:(XMPPPresence *)presence +{ + // Override / customization hook +} + +- (void)didRemoveResource:(XMPPResourceMemoryStorageObject *)resource withPresence:(XMPPPresence *)presence +{ + // Override / customization hook +} + +- (void)recalculatePrimaryResource +{ + // Override me to customize how the primary resource is chosen. + // + // This method uses the [XMPPResourceMemoryStorage compare:] method to sort the resources, + // and properly supports negative (bot) priorities. + + primaryResource = nil; + + NSArray *sortedResources = [[self allResources] sortedArrayUsingSelector:@selector(compare:)]; + if ([sortedResources count] > 0) + { + XMPPResourceMemoryStorageObject *possiblePrimary = sortedResources[0]; + + // Primary resource must have a non-negative priority + if ([[possiblePrimary presence] priority] >= 0) + { + primaryResource = possiblePrimary; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Update Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)clearAllResources +{ + [resources removeAllObjects]; + + primaryResource = nil; +} + +- (void)updateWithItem:(NSXMLElement *)item +{ + [itemAttributes removeAllObjects]; + + for (NSXMLNode *node in [item attributes]) + { + NSString *key = [node name]; + NSString *value = [node stringValue]; + + itemAttributes[key] = value; + } + + [groups removeAllObjects]; + + NSArray *groupElements = [item elementsForName:@"group"]; + + for (NSXMLElement *groupElement in groupElements) { + NSString *groupName = [groupElement stringValue]; + + if ([groupName length]) + { + [groups addObject:groupName]; + } + } +} + +- (int)updateWithPresence:(XMPPPresence *)presence + resourceClass:(Class)resourceClass + andGetResource:(XMPPResourceMemoryStorageObject **)resourcePtr +{ + int result = XMPP_USER_NO_CHANGE; + XMPPResourceMemoryStorageObject *resource; + + XMPPJID *key = [presence from]; + NSString *presenceType = [presence type]; + + if ([presenceType isEqualToString:@"unavailable"] || [presenceType isEqualToString:@"error"]) + { + resource = resources[key]; + if (resource) + { + [resources removeObjectForKey:key]; + [self didRemoveResource:resource withPresence:presence]; + + result = XMPP_USER_REMOVED_RESOURCE; + } + } + else + { + resource = resources[key]; + if (resource) + { + [self willUpdateResource:resource withPresence:presence]; + [resource updateWithPresence:presence]; + [self didUpdateResource:resource withPresence:presence]; + + result = XMPP_USER_UPDATED_RESOURCE; + } + else + { + resource = (XMPPResourceMemoryStorageObject *)[[resourceClass alloc] initWithPresence:presence]; + + resources[key] = resource; + [self didAddResource:resource withPresence:presence]; + + result = XMPP_USER_ADDED_RESOURCE; + } + } + + [self recalculatePrimaryResource]; + + if (resourcePtr) + *resourcePtr = resource; + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Comparisons +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns the result of invoking compareByName:options: with no options. +**/ +- (NSComparisonResult)compareByName:(XMPPUserMemoryStorageObject *)another +{ + return [self compareByName:another options:0]; +} + +/** + * This method compares the two users according to their display name. + * + * Options for the search — you can combine any of the following using a C bitwise OR operator: + * NSCaseInsensitiveSearch, NSLiteralSearch, NSNumericSearch. + * See "String Programming Guide for Cocoa" for details on these options. +**/ +- (NSComparisonResult)compareByName:(XMPPUserMemoryStorageObject *)another options:(NSStringCompareOptions)mask +{ + NSString *myName = [self displayName]; + NSString *theirName = [another displayName]; + + return [myName compare:theirName options:mask]; +} + +/** + * Returns the result of invoking compareByAvailabilityName:options: with no options. +**/ +- (NSComparisonResult)compareByAvailabilityName:(XMPPUserMemoryStorageObject *)another +{ + return [self compareByAvailabilityName:another options:0]; +} + +/** + * This method compares the two users according to availability first, and then display name. + * Thus available users come before unavailable users. + * If both users are available, or both users are not available, + * this method follows the same functionality as the compareByName:options: as documented above. +**/ +- (NSComparisonResult)compareByAvailabilityName:(XMPPUserMemoryStorageObject *)another + options:(NSStringCompareOptions)mask +{ + if ([self isOnline]) + { + if ([another isOnline]) + return [self compareByName:another options:mask]; + else + return NSOrderedAscending; + } + else + { + if ([another isOnline]) + return NSOrderedDescending; + else + return [self compareByName:another options:mask]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark NSObject Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSUInteger)hash +{ + return [jid hash]; +} + +- (BOOL)isEqual:(id)anObject +{ + if ([anObject isMemberOfClass:[self class]]) + { + XMPPUserMemoryStorageObject *another = (XMPPUserMemoryStorageObject *)anObject; + + return [jid isEqualToJID:[another jid]]; + } + + return NO; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"", self, [jid bare]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark KVO Compliance methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (NSSet *)keyPathsForValuesAffectingIsOnline +{ + return [NSSet setWithObject:@"primaryResource"]; +} + ++ (NSSet *)keyPathsForValuesAffectingAllResources { + return [NSSet setWithObject:@"resources"]; +} + +@end diff --git a/Extensions/Roster/XMPPResource.h b/Extensions/Roster/XMPPResource.h new file mode 100644 index 0000000..43ee555 --- /dev/null +++ b/Extensions/Roster/XMPPResource.h @@ -0,0 +1,15 @@ +#import +#import "XMPP.h" + + +@protocol XMPPResource +@required + +- (XMPPJID *)jid; +- (XMPPPresence *)presence; + +- (NSDate *)presenceDate; + +- (NSComparisonResult)compare:(id )another; + +@end diff --git a/Extensions/Roster/XMPPRoster.h b/Extensions/Roster/XMPPRoster.h new file mode 100644 index 0000000..7342edd --- /dev/null +++ b/Extensions/Roster/XMPPRoster.h @@ -0,0 +1,406 @@ +#import + +#if TARGET_OS_IPHONE + #import +#else + #import +#endif + +#import "XMPP.h" +#import "XMPPUser.h" +#import "XMPPResource.h" + +#define _XMPP_ROSTER_H + +@protocol XMPPRosterStorage; +@class DDList; +@class XMPPIDTracker; + +/** + * The XMPPRoster provides the scaffolding for a roster solution. + * It handles the basics of the xmpp roster communication, + * and leaves the rest up to the xmppRosterStorage classes. + * + * You are free to extend, change, customize, or tweak the sample storage classes that come with the framework: + * XMPPRosterMemoryStorage + * XMPPRosterCoreDataStorage + * + * You can also completely implment your own roster storage class if you'd like. + * The point of all this customizability is simple: + * The roster is the component of the xmpp stack that is most often customized for a particular application. + * + * + * Inter-Module Interaction: + * + * If you use XMPPvCardAvatarModule, the roster will automatically support user photos. +**/ +@interface XMPPRoster : XMPPModule +{ +/* Inherited from XMPPModule: + + XMPPStream *xmppStream; + + dispatch_queue_t moduleQueue; + id multicastDelegate; + */ + __strong id xmppRosterStorage; + + XMPPIDTracker *xmppIDTracker; + + Byte config; + Byte flags; + + NSMutableArray *earlyPresenceElements; + + DDList *mucModules; +} + +- (id)initWithRosterStorage:(id )storage; +- (id)initWithRosterStorage:(id )storage dispatchQueue:(dispatch_queue_t)queue; + +/* Inherited from XMPPModule: + +- (BOOL)activate:(XMPPStream *)xmppStream; +- (void)deactivate; + +@property (readonly) XMPPStream *xmppStream; + +- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; +- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; +- (void)removeDelegate:(id)delegate; + +- (NSString *)moduleName; + +*/ + +@property (strong, readonly) id xmppRosterStorage; + +/** + * Whether or not to automatically fetch the roster from the server. + * + * The default value is YES. +**/ +@property (assign) BOOL autoFetchRoster; + +/** + * Whether or not to automatically clear all Users and Resources when the stream disconnects. + * If you are using XMPPRosterCoreDataStorage you may want to set autoRemovePreviousDatabaseFile to NO. + * + * All Users and Resources will be cleared when the roster is next populated regardless of this property. + * + * The default value is YES. +**/ +@property (assign) BOOL autoClearAllUsersAndResources; + +/** + * In traditional IM applications, the "buddy" system is rather straightforward. + * User A sends a request to become "friends" with user B. + * User B accepts the friend request. + * And now user A and B appear in each other's roster. + * + * But, internally, XMPP presence actually works more like twitter. + * User A sends a presence subscription request to user B. + * Think about this more like following on twitter. + * User A is requesting permission to receive presence information from user B (following their presence). + * User B can accept this request, but may not be interested in receiving presence info from user B. + * Again, this is more like twitter following, but with the addition of required permission. + * So when user B accepts the request, this *only* means that user A will receive presence from user B. + * It does *not* mean that user B will receive presence from user A. + * Again, twitter-style, user A is now "following" user B. + * In diagram form, presence is flowing like this: + * + * A <-- B + * + * Now, if B also wants to receive presence from user A, then user B must request this permission. + * And furthermore, user A must accept the request. + * Just because B has granted A permission to receive presence, + * doesn't mean that B gets a free pass to receive presence from A. + * Presence always requires permission. + * + * This property does the following: + * If a presence subscription request is received from user X, + * and user X has already been added to our roster (is a "known" user), + * then the presence subscription request is automatically accepted. + * + * Continuing the example from above, if we were user A, and we've already added B to our roster, + * then if B sends a presence susbcription request, we'll auto accept it. + * + * This makes the roster act more like traditional IM applications. + * + * The default value is YES. +**/ +@property (assign) BOOL autoAcceptKnownPresenceSubscriptionRequests; + +/** + * Allows the roster module to function without ever fetching the full roster. + * This is helpful for situations in which the roster is very big, yet the application only cares about online users. + * + * Typically, the roster module creates users based on the fetched full roster, + * and then creates resources based on received presence. + * + * In this mode, the roster module will automatically create a user once a presence is received, + * if the user has never been seen before. + * + * If allowRosterlessOperation is enabled, and autoFetchRoster is disabled (and roster is never manually fetched), + * then XMPPUser's will be missing certain information that is only available via a roster fetch + * (such as nickname, group, and subscription information). + * + * The default value is NO. +**/ +@property (assign) BOOL allowRosterlessOperation; + +/** + * The roster has either been requested manually (fetchRoster:) + * or automatically (autoFetchRoster) but has yet to be populated. +**/ +@property (assign, getter = hasRequestedRoster, readonly) BOOL requestedRoster; + +/** + * The initial roster has been received by client and is currently being populated. + * @see xmppRosterDidBeginPopulating:withVersion: + * @see xmppRosterDidEndPopulating: +**/ +@property (assign, getter = isPopulating, readonly) BOOL populating; + +/** + * The initial roster has been received by client and populated. +**/ +@property (assign, readonly) BOOL hasRoster; + +/** + * Manually fetch the roster from the server. + * Useful if you disable autoFetchRoster. +**/ +- (void)fetchRoster; +- (void)fetchRosterVersion:(NSString *)version; + +/** + * Adds the given user to the roster with an optional nickname + * and requests permission to receive presence information from them. +**/ +- (void)addUser:(XMPPJID *)jid withNickname:(NSString *)optionalName; + +/** + * Adds the given user to the roster with an optional nickname, + * adds the given user to groups + * and requests permission to receive presence information from them. +**/ +- (void)addUser:(XMPPJID *)jid withNickname:(NSString *)optionalName groups:(NSArray *)groups; + +/** + * Adds the given user to the roster with an optional nickname, + * adds the given user to groups + * and optionally requests permission to receive presence information from them. +**/ +- (void)addUser:(XMPPJID *)jid withNickname:(NSString *)optionalName groups:(NSArray *)groups subscribeToPresence:(BOOL)subscribe; + +/** + * Sets/modifies the nickname for the given user. +**/ +- (void)setNickname:(NSString *)nickname forUser:(XMPPJID *)jid; + +/** + * Remove the user from the roster, unsubscribe from their presence, AND + * revoke given user's permission to receive our presence (if they have such permission). + * + * This is similar to removing a buddy in a traditional IM model. + * + * @see unsubscribePresenceFromUser: + * @see revokePresencePermissionFromUser: +**/ +- (void)removeUser:(XMPPJID *)jid; + +/** + * If we don't currently receive presence from the given user, + * this method requests a subscription to start receiving presence updates from the given user. + * + * This is similar to following in the twitter model. + * + * Note: If the given user isn't already in the roster, it is recommended to instead use addUser:withNickname:. + * + * @see addUser:withNickname: +**/ +- (void)subscribePresenceToUser:(XMPPJID *)jid; + +/** + * If we currently have a presence subscription to the given user, + * this method then removes the subscription. + * + * This is similar to unfollowing in the twitter model. + * + * If the given user has a presence subscription to us (they're following our presence), + * then their presence subscription to us is left intact. + * + * @see removeUser: + * @see revokePresencePermissionFromUser: +**/ +- (void)unsubscribePresenceFromUser:(XMPPJID *)jid; + +/** + * If we have previously accepted a presence subscription request from the given user, + * this method revokes the previously granted permission. + * + * This is similar to forcing the given user to unfollow us in the twitter model. + * + * If we have a presence subscription to the given user (we're following their presence), + * then our presence subscription to them is left intact. + * + * @see removeUser: + * @see unsubscribePresenceFromUser: +**/ +- (void)revokePresencePermissionFromUser:(XMPPJID *)jid; + +/** + * Accepts the presence subscription request the given user. + * + * If you also choose, you can add the user to your roster. + * Doing so is similar to the traditional IM model. +**/ +- (void)acceptPresenceSubscriptionRequestFrom:(XMPPJID *)jid andAddToRoster:(BOOL)flag; + +/** + * Rejects the presence subscription request from the given user. + * + * If you are already subscribed to the given user's presence, + * rejecting they subscription request will not affect your subscription to their presence. +**/ +- (void)rejectPresenceSubscriptionRequestFrom:(XMPPJID *)jid; + +// +// +// You can access/enumerate the users & resources via the roster storage class (xmppRosterStorage property). +// +// Rember, XMPPRoster is just the scaffolding for a complete and customizable roster solution. +// The roster storage classes hold the majority of the magic. +// +// And since you're free to plug-n-play storage classes, and customize them as much as you want. +// This is where you can really tailor the xmpp stack to meet the needs of your application. +// +// + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPRosterStorage +@required + +// +// +// -- PUBLIC METHODS -- +// +// There are no public methods required by this protocol. +// +// Each individual roster storage class will provide a proper way to access/enumerate the +// users/resources according to the underlying storage mechanism. +// + + +// +// +// -- PRIVATE METHODS -- +// +// These methods are designed to be used ONLY by the XMPPRoster class. +// +// + +/** + * Configures the storage class, passing its parent and parent's dispatch queue. + * + * This method is called by the init method of the XMPPRoster class. + * This method is designed to inform the storage class of its parent + * and of the dispatch queue the parent will be operating on. + * + * The storage class may choose to operate on the same queue as its parent, + * or it may operate on its own internal dispatch queue. + * + * This method should return YES if it was configured properly. + * If a storage class is designed to be used with a single parent at a time, this method may return NO. + * The XMPPRoster class is configured to ignore the passed + * storage class in its init method if this method returns NO. +**/ +- (BOOL)configureWithParent:(XMPPRoster *)aParent queue:(dispatch_queue_t)queue; + +- (void)beginRosterPopulationForXMPPStream:(XMPPStream *)stream withVersion:(NSString *)version; +- (void)endRosterPopulationForXMPPStream:(XMPPStream *)stream; + +- (void)handleRosterItem:(NSXMLElement *)item xmppStream:(XMPPStream *)stream; +- (void)handlePresence:(XMPPPresence *)presence xmppStream:(XMPPStream *)stream; + +- (BOOL)userExistsWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream; + +- (void)clearAllResourcesForXMPPStream:(XMPPStream *)stream; +- (void)clearAllUsersAndResourcesForXMPPStream:(XMPPStream *)stream; + +- (NSArray *)jidsForXMPPStream:(XMPPStream *)stream; + +- (void)getSubscription:(NSString **)subscription + ask:(NSString **)ask + nickname:(NSString **)nickname + groups:(NSArray **)groups + forJID:(XMPPJID *)jid + xmppStream:(XMPPStream *)stream; + +@optional + +/** + * When XMPPvCardAvatarModule is included in the framework, the roster will integrate with it. + * Implement this method to provide support for storing the downloaded user photos. +**/ +#if TARGET_OS_IPHONE +- (void)setPhoto:(UIImage *)image forUserWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream; +#else +- (void)setPhoto:(NSImage *)image forUserWithJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream; +#endif + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPRosterDelegate +@optional + +/** + * Sent when a presence subscription request is received. + * That is, another user has added you to their roster, + * and is requesting permission to receive presence broadcasts that you send. + * + * The entire presence packet is provided for proper extensibility. + * You can use [presence from] to get the JID of the user who sent the request. + * + * The methods acceptPresenceSubscriptionRequestFrom: and rejectPresenceSubscriptionRequestFrom: can + * be used to respond to the request. +**/ +- (void)xmppRoster:(XMPPRoster *)sender didReceivePresenceSubscriptionRequest:(XMPPPresence *)presence; + +/** + * Sent when a Roster Push is received as specified in Section 2.1.6 of RFC 6121. +**/ +- (void)xmppRoster:(XMPPRoster *)sender didReceiveRosterPush:(XMPPIQ *)iq; + +/** + * Sent when the initial roster is received. +**/ +- (void)xmppRosterDidBeginPopulating:(XMPPRoster *)sender withVersion:(NSString *)version; + +/** + * Sent when the initial roster has been populated into storage. +**/ +- (void)xmppRosterDidEndPopulating:(XMPPRoster *)sender; + +/** + * Sent when the roster receives a roster item. + * + * Example: + * + * + * Friends + * +**/ +- (void)xmppRoster:(XMPPRoster *)sender didReceiveRosterItem:(NSXMLElement *)item; + +@end diff --git a/Extensions/Roster/XMPPRoster.m b/Extensions/Roster/XMPPRoster.m new file mode 100644 index 0000000..65c3d89 --- /dev/null +++ b/Extensions/Roster/XMPPRoster.m @@ -0,0 +1,1006 @@ +#import "XMPPRoster.h" +#import "XMPP.h" +#import "XMPPIDTracker.h" +#import "XMPPLogging.h" +#import "XMPPFramework.h" +#import "DDList.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +enum XMPPRosterConfig +{ + kAutoFetchRoster = 1 << 0, // If set, we automatically fetch roster after authentication + kAutoAcceptKnownPresenceSubscriptionRequests = 1 << 1, // See big description in header file... :D + kRosterlessOperation = 1 << 2, + kAutoClearAllUsersAndResources = 1 << 3, +}; +enum XMPPRosterFlags +{ + kRequestedRoster = 1 << 0, // If set, we have requested the roster + kHasRoster = 1 << 1, // If set, we have received the roster + kPopulatingRoster = 1 << 2, // If set, we are populating the roster +}; + +@interface XMPPRoster (PrivateAPI) + +- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPRoster + +- (id)init +{ + return [self initWithRosterStorage:nil dispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + return [self initWithRosterStorage:nil dispatchQueue:queue]; +} + +- (id)initWithRosterStorage:(id )storage +{ + return [self initWithRosterStorage:storage dispatchQueue:NULL]; +} + +- (id)initWithRosterStorage:(id )storage dispatchQueue:(dispatch_queue_t)queue +{ + NSParameterAssert(storage != nil); + + if ((self = [super initWithDispatchQueue:queue])) + { + if ([storage configureWithParent:self queue:moduleQueue]) + { + xmppRosterStorage = storage; + } + else + { + XMPPLogError(@"%@: %@ - Unable to configure storage!", THIS_FILE, THIS_METHOD); + } + + config = kAutoFetchRoster | kAutoAcceptKnownPresenceSubscriptionRequests | kAutoClearAllUsersAndResources; + flags = 0; + + earlyPresenceElements = [[NSMutableArray alloc] initWithCapacity:2]; + + mucModules = [[DDList alloc] init]; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + XMPPLogTrace(); + + if ([super activate:aXmppStream]) + { + XMPPLogVerbose(@"%@: Activated", THIS_FILE); + + xmppIDTracker = [[XMPPIDTracker alloc] initWithStream:xmppStream dispatchQueue:moduleQueue]; + + #ifdef _XMPP_VCARD_AVATAR_MODULE_H + { + // Automatically tie into the vCard system so we can store user photos. + + [xmppStream autoAddDelegate:self + delegateQueue:moduleQueue + toModulesOfClass:[XMPPvCardAvatarModule class]]; + } + #endif + + #ifdef _XMPP_MUC_H + { + // Automatically tie into the MUC system so we can ignore non-roster presence stanzas. + + [xmppStream enumerateModulesWithBlock:^(XMPPModule *module, NSUInteger idx, BOOL *stop) { + + if ([module isKindOfClass:[XMPPMUC class]]) + { + [mucModules add:(__bridge void *)module]; + } + }]; + } + #endif + + return YES; + } + + return NO; +} + +- (void)deactivate +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool { + + [xmppIDTracker removeAllIDs]; + xmppIDTracker = nil; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + #ifdef _XMPP_VCARD_AVATAR_MODULE_H + { + [xmppStream removeAutoDelegate:self delegateQueue:moduleQueue fromModulesOfClass:[XMPPvCardAvatarModule class]]; + } + #endif + + [super deactivate]; +} + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Internal +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method may optionally be used by XMPPRosterStorage classes (declared in XMPPRosterPrivate.h). +**/ +- (GCDMulticastDelegate *)multicastDelegate +{ + return multicastDelegate; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id )xmppRosterStorage +{ + // Note: The xmppRosterStorage variable is read-only (set in the init method) + + return xmppRosterStorage; +} + +- (BOOL)autoFetchRoster +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (config & kAutoFetchRoster) ? YES : NO; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setAutoFetchRoster:(BOOL)flag +{ + dispatch_block_t block = ^{ + + if (flag) + config |= kAutoFetchRoster; + else + config &= ~kAutoFetchRoster; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (BOOL)autoClearAllUsersAndResources +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (config & kAutoClearAllUsersAndResources) ? YES : NO; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setAutoClearAllUsersAndResources:(BOOL)flag +{ + dispatch_block_t block = ^{ + + if (flag) + config |= kAutoClearAllUsersAndResources; + else + config &= ~kAutoClearAllUsersAndResources; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (BOOL)autoAcceptKnownPresenceSubscriptionRequests +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (config & kAutoAcceptKnownPresenceSubscriptionRequests) ? YES : NO; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setAutoAcceptKnownPresenceSubscriptionRequests:(BOOL)flag +{ + dispatch_block_t block = ^{ + + if (flag) + config |= kAutoAcceptKnownPresenceSubscriptionRequests; + else + config &= ~kAutoAcceptKnownPresenceSubscriptionRequests; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (BOOL)allowRosterlessOperation +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (config & kRosterlessOperation) ? YES : NO; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setAllowRosterlessOperation:(BOOL)flag +{ + dispatch_block_t block = ^{ + + if (flag) + config |= kRosterlessOperation; + else + config &= ~kRosterlessOperation; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + + +- (BOOL)hasRequestedRoster +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (flags & kRequestedRoster) ? YES : NO; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (BOOL)isPopulating{ + + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (flags & kPopulatingRoster) ? YES : NO; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (BOOL)hasRoster +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (flags & kHasRoster) ? YES : NO; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)_requestedRoster +{ + NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue"); + + return (flags & kRequestedRoster) ? YES : NO; +} + +- (void)_setRequestedRoster:(BOOL)flag +{ + NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue"); + + if (flag) + flags |= kRequestedRoster; + else + flags &= ~kRequestedRoster; +} + +- (void)_setHasRoster:(BOOL)flag +{ + NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue"); + + if (flag) + flags |= kHasRoster; + else + flags &= ~kHasRoster; +} + +- (BOOL)_populatingRoster +{ + NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue"); + + return (flags & kPopulatingRoster) ? YES : NO; +} + +- (void)_setPopulatingRoster:(BOOL)flag +{ + NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue"); + + if (flag) + flags |= kPopulatingRoster; + else + flags &= ~kPopulatingRoster; +} + +- (void)_addRosterItems:(NSArray *)rosterItems +{ + NSAssert(dispatch_get_specific(moduleQueueTag) , @"Invoked on incorrect queue"); + + BOOL hasRoster = [self hasRoster]; + + for (NSXMLElement *item in rosterItems) + { + // During roster population, we need to filter out items for users who aren't actually in our roster. + // That is, those users who have requested to be our buddy, but we haven't approved yet. + // This is described in more detail in the method isRosterItem above. + + [multicastDelegate xmppRoster:self didReceiveRosterItem:item]; + + if (hasRoster || [self isRosterItem:item]) + { + [xmppRosterStorage handleRosterItem:item xmppStream:xmppStream]; + } + } +} + +/** + * Some server's include in our roster the JID's of user's NOT in our roster. + * This happens when another user adds us to their roster, and requests permission to receive our presence. + * + * As discussed in RFC 3921, the state of the other user is "None + Pending In", + * and the server "SHOULD NOT" include these JID's in the roster it sends us. + * + * Nonetheless, some servers do anyway. + * This method filters out such rogue entries in our roster. + * + * Note that the server will automatically send us the proper presence subscription request, + * and it will continue to do so everytime we sign in. + * From the RFC: + * the user's server MUST keep a record of the subscription request and deliver the request when the + * user next creates an available resource, until the user either approves or denies the request. + * + * So there is absolutely NO reason to process these entries, or include them in the roster's storage. + * Furthermore, it isn't reliable to depend on these entires being there. + * The RFC has clearly defined recommendations on the matter, and servers that currently send these rogue items + * may very likely stop doing so in future versions. +**/ +- (BOOL)isRosterItem:(NSXMLElement *)item +{ + NSString *subscription = [item attributeStringValueForName:@"subscription"]; + if ([subscription isEqualToString:@"none"]) + { + NSString *ask = [item attributeStringValueForName:@"ask"]; + if ([ask isEqualToString:@"subscribe"]) + { + return YES; + } + else + { + return NO; + } + } + + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Roster Management +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)addUser:(XMPPJID *)jid withNickname:(NSString *)optionalName{ + [self addUser:jid withNickname:optionalName groups:nil subscribeToPresence:YES]; +} + +- (void)addUser:(XMPPJID *)jid withNickname:(NSString *)optionalName groups:(NSArray *)groups{ + [self addUser:jid withNickname:optionalName groups:groups subscribeToPresence:YES]; +} + +- (void)addUser:(XMPPJID *)jid withNickname:(NSString *)optionalName groups:(NSArray *)groups subscribeToPresence:(BOOL)subscribe{ + + if (jid == nil) return; + + XMPPJID *myJID = xmppStream.myJID; + + if ([myJID isEqualToJID:jid options:XMPPJIDCompareBare]) + { + // You don't need to add yourself to the roster. + // XMPP will automatically send you presence from all resources signed in under your username. + // + // E.g. If you sign in with robbiehanson@deusty.com/home you'll automatically + // receive presence from robbiehanson@deusty.com/work + + XMPPLogInfo(@"%@: %@ - Ignoring request to add myself to my own roster", [self class], THIS_METHOD); + return; + } + + // Add the buddy to our roster + // + // + // + // + // family + // + // + // + + NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; + [item addAttributeWithName:@"jid" stringValue:[jid bare]]; + + if(optionalName) + { + [item addAttributeWithName:@"name" stringValue:optionalName]; + } + + for (NSString *group in groups) { + NSXMLElement *groupElement = [NSXMLElement elementWithName:@"group"]; + [groupElement setStringValue:group]; + [item addChild:groupElement]; + } + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:roster"]; + [query addChild:item]; + + NSXMLElement *iq = [NSXMLElement elementWithName:@"iq"]; + [iq addAttributeWithName:@"type" stringValue:@"set"]; + [iq addChild:query]; + + [xmppStream sendElement:iq]; + + if(subscribe) + { + [self subscribePresenceToUser:jid]; + } +} + +- (void)setNickname:(NSString *)nickname forUser:(XMPPJID *)jid +{ + // This is a public method, so it may be invoked on any thread/queue. + + if (jid == nil) return; + + // + // + // + // + // + + NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; + [item addAttributeWithName:@"jid" stringValue:[jid bare]]; + [item addAttributeWithName:@"name" stringValue:nickname]; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:roster"]; + [query addChild:item]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set"]; + [iq addChild:query]; + + [xmppStream sendElement:iq]; +} + +- (void)subscribePresenceToUser:(XMPPJID *)jid +{ + // This is a public method, so it may be invoked on any thread/queue. + + if (jid == nil) return; + + XMPPJID *myJID = xmppStream.myJID; + + if ([myJID isEqualToJID:jid options:XMPPJIDCompareBare]) + { + XMPPLogInfo(@"%@: %@ - Ignoring request to subscribe presence to myself", [self class], THIS_METHOD); + return; + } + + // + + XMPPPresence *presence = [XMPPPresence presenceWithType:@"subscribe" to:[jid bareJID]]; + [xmppStream sendElement:presence]; +} + +- (void)unsubscribePresenceFromUser:(XMPPJID *)jid +{ + // This is a public method, so it may be invoked on any thread/queue. + + if (jid == nil) return; + + XMPPJID *myJID = xmppStream.myJID; + + if ([myJID isEqualToJID:jid options:XMPPJIDCompareBare]) + { + XMPPLogInfo(@"%@: %@ - Ignoring request to unsubscribe presence from myself", [self class], THIS_METHOD); + return; + } + + // + + XMPPPresence *presence = [XMPPPresence presenceWithType:@"unsubscribe" to:[jid bareJID]]; + [xmppStream sendElement:presence]; +} + +- (void)revokePresencePermissionFromUser:(XMPPJID *)jid +{ + // This is a public method, so it may be invoked on any thread/queue. + + if (jid == nil) return; + + XMPPJID *myJID = xmppStream.myJID; + + if ([myJID isEqualToJID:jid options:XMPPJIDCompareBare]) + { + XMPPLogInfo(@"%@: %@ - Ignoring request to revoke presence from myself", [self class], THIS_METHOD); + return; + } + + // + + XMPPPresence *presence = [XMPPPresence presenceWithType:@"unsubscribed" to:[jid bareJID]]; + [xmppStream sendElement:presence]; +} + +- (void)removeUser:(XMPPJID *)jid +{ + // This is a public method, so it may be invoked on any thread/queue. + + if (jid == nil) return; + + XMPPJID *myJID = xmppStream.myJID; + + if ([myJID isEqualToJID:jid options:XMPPJIDCompareBare]) + { + XMPPLogInfo(@"%@: %@ - Ignoring request to remove myself from my own roster", [self class], THIS_METHOD); + return; + } + + // Remove the user from our roster. + // And unsubscribe from presence. + // And revoke contact's subscription to our presence. + // ...all in one step + + // + // + // + // + // + + NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; + [item addAttributeWithName:@"jid" stringValue:[jid bare]]; + [item addAttributeWithName:@"subscription" stringValue:@"remove"]; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:roster"]; + [query addChild:item]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set"]; + [iq addChild:query]; + + [xmppStream sendElement:iq]; +} + +- (void)acceptPresenceSubscriptionRequestFrom:(XMPPJID *)jid andAddToRoster:(BOOL)flag +{ + // This is a public method, so it may be invoked on any thread/queue. + + // Send presence response + // + // + + XMPPPresence *presence = [XMPPPresence presenceWithType:@"subscribed" to:[jid bareJID]]; + [xmppStream sendElement:presence]; + + // Add optionally add user to our roster + + if (flag) + { + [self addUser:jid withNickname:nil]; + } +} + +- (void)rejectPresenceSubscriptionRequestFrom:(XMPPJID *)jid +{ + // This is a public method, so it may be invoked on any thread/queue. + + // Send presence response + // + // + + XMPPPresence *presence = [XMPPPresence presenceWithType:@"unsubscribed" to:[jid bareJID]]; + [xmppStream sendElement:presence]; +} +- (void)fetchRoster +{ + // This is a public method, so it may be invoked on any thread/queue. + + dispatch_block_t block = ^{ @autoreleasepool { + + [self fetchRosterVersion:nil]; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} +- (void)fetchRosterVersion:(NSString *)version +{ + // This is a public method, so it may be invoked on any thread/queue. + + dispatch_block_t block = ^{ @autoreleasepool { + + if ([self _requestedRoster]) + { + // We've already requested the roster from the server. + return; + } + + // + // + // + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:roster"]; + if (version) + [query addAttributeWithName:@"ver" stringValue:version]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" elementID:[xmppStream generateUUID]]; + [iq addChild:query]; + + [xmppIDTracker addElement:iq + target:self + selector:@selector(handleFetchRosterQueryIQ:withInfo:) + timeout:60]; + + [xmppStream sendElement:iq]; + + [self _setRequestedRoster:YES]; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPIDTracker +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)handleFetchRosterQueryIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)basicTrackingInfo{ + + dispatch_block_t block = ^{ @autoreleasepool { + + NSXMLElement *query = [iq elementForName:@"query" xmlns:@"jabber:iq:roster"]; + NSString * version = [query attributeStringValueForName:@"ver"]; + + BOOL hasRoster = [self hasRoster]; + + if (!hasRoster) + { + [xmppRosterStorage clearAllUsersAndResourcesForXMPPStream:xmppStream]; + [self _setPopulatingRoster:YES]; + [multicastDelegate xmppRosterDidBeginPopulating:self withVersion:version]; + [xmppRosterStorage beginRosterPopulationForXMPPStream:xmppStream withVersion:version]; + } + + NSArray *items = [query elementsForName:@"item"]; + [self _addRosterItems:items]; + + if (!hasRoster) + { + // We should have our roster now + + [self _setHasRoster:YES]; + [self _setPopulatingRoster:NO]; + [multicastDelegate xmppRosterDidEndPopulating:self]; + [xmppRosterStorage endRosterPopulationForXMPPStream:xmppStream]; + + // Process any premature presence elements we received. + + for (XMPPPresence *presence in earlyPresenceElements) + { + [self xmppStream:xmppStream didReceivePresence:presence]; + } + + [earlyPresenceElements removeAllObjects]; + } + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender +{ + // This method is invoked on the moduleQueue. + + XMPPLogTrace(); + + if ([self autoFetchRoster]) + { + [self fetchRoster]; + } +} + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + // This method is invoked on the moduleQueue. + + XMPPLogTrace(); + + // Note: Some jabber servers send an iq element with an xmlns. + // Because of the bug in Apple's NSXML (documented in our elementForName method), + // it is important we specify the xmlns for the query. + + NSXMLElement *query = [iq elementForName:@"query" xmlns:@"jabber:iq:roster"]; + + if (query) + { + if([iq isSetIQ]) + { + [multicastDelegate xmppRoster:self didReceiveRosterPush:iq]; + + NSArray *items = [query elementsForName:@"item"]; + [self _addRosterItems:items]; + } + else if([iq isResultIQ]) + { + [xmppIDTracker invokeForElement:iq withObject:iq]; + } + + return YES; + } + + return NO; +} + +- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence +{ + // This method is invoked on the moduleQueue. + + XMPPLogTrace(); + + if (![self hasRoster] && ![self allowRosterlessOperation]) + { + // We received a presence notification, + // but we don't have a roster to apply it to yet. + // + // This is possible if we send our presence before we've received our roster. + // It's even possible if we send our presence after we've requested our roster. + // There is no guarantee the server will process our requests serially, + // and the server may start sending presence elements before it sends our roster. + // + // However, if we've requested the roster, + // then it shouldn't be too long before we receive it. + // So we should be able to simply queue the presence elements for later processing. + + if ([self _requestedRoster]) + { + // We store the presence element until we get our roster. + [earlyPresenceElements addObject:presence]; + } + else + { + // The user has not requested the roster. + // This is a rogue presence element, or the user is simply not using our roster management. + } + + return; + } + + if ([[presence type] isEqualToString:@"subscribe"]) + { + XMPPJID *userJID = [[presence from] bareJID]; + + BOOL knownUser = [xmppRosterStorage userExistsWithJID:userJID xmppStream:xmppStream]; + + if (knownUser && [self autoAcceptKnownPresenceSubscriptionRequests]) + { + // Presence subscription request from someone who's already in our roster. + // Automatically approve. + // + // + + XMPPPresence *response = [XMPPPresence presenceWithType:@"subscribed" to:userJID]; + [xmppStream sendElement:response]; + } + else + { + // Presence subscription request from someone who's NOT in our roster + + [multicastDelegate xmppRoster:self didReceivePresenceSubscriptionRequest:presence]; + } + } + else + { + #ifdef _XMPP_MUC_H + + // Ignore MUC related presence items + + for (XMPPMUC *muc in mucModules) + { + if ([muc isMUCRoomPresence:presence]) + { + return; + } + } + + #endif + + [xmppRosterStorage handlePresence:presence xmppStream:xmppStream]; + } +} + +- (void)xmppStream:(XMPPStream *)sender didSendPresence:(XMPPPresence *)presence +{ + // This method is invoked on the moduleQueue. + + XMPPLogTrace(); + + // We check the toStr, so we don't dump the resources when a user leaves a MUC room. + + if ([[presence type] isEqualToString:@"unavailable"] && [presence toStr] == nil) + { + // We don't receive presence notifications when we're offline. + // So we need to remove all resources from our roster when we're offline. + // When we become available again, we'll automatically receive the + // presence from every available user in our roster. + // + // We will receive general roster updates as long as we're still connected though. + // So there's no need to refetch the roster. + + [xmppRosterStorage clearAllResourcesForXMPPStream:xmppStream]; + + [earlyPresenceElements removeAllObjects]; + } +} + +- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error +{ + // This method is invoked on the moduleQueue. + + XMPPLogTrace(); + + if([self autoClearAllUsersAndResources]) + { + [xmppRosterStorage clearAllUsersAndResourcesForXMPPStream:xmppStream]; + } + else + { + [xmppRosterStorage clearAllResourcesForXMPPStream:xmppStream]; + } + + [self _setRequestedRoster:NO]; + [self _setHasRoster:NO]; + + [earlyPresenceElements removeAllObjects]; +} + +#ifdef _XMPP_MUC_H + +- (void)xmppStream:(XMPPStream *)sender didRegisterModule:(id)module +{ + if ([module isKindOfClass:[XMPPMUC class]]) + { + if (![mucModules contains:(__bridge void *)module]) + { + [mucModules add:(__bridge void *)module]; + } + } +} + +- (void)xmppStream:(XMPPStream *)sender willUnregisterModule:(id)module +{ + if ([module isKindOfClass:[XMPPMUC class]]) + { + [mucModules remove:(__bridge void *)module]; + } +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPvCardAvatarDelegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifdef _XMPP_VCARD_AVATAR_MODULE_H + +#if TARGET_OS_IPHONE +- (void)xmppvCardAvatarModule:(XMPPvCardAvatarModule *)vCardTempModule + didReceivePhoto:(UIImage *)photo + forJID:(XMPPJID *)jid +#else +- (void)xmppvCardAvatarModule:(XMPPvCardAvatarModule *)vCardTempModule + didReceivePhoto:(NSImage *)photo + forJID:(XMPPJID *)jid +#endif +{ + if ([xmppRosterStorage respondsToSelector:@selector(setPhoto:forUserWithJID:xmppStream:)]) + { + [xmppRosterStorage setPhoto:photo forUserWithJID:[jid bareJID] xmppStream:xmppStream]; + } +} + +#endif + +@end diff --git a/Extensions/Roster/XMPPRosterPrivate.h b/Extensions/Roster/XMPPRosterPrivate.h new file mode 100644 index 0000000..88111df --- /dev/null +++ b/Extensions/Roster/XMPPRosterPrivate.h @@ -0,0 +1,16 @@ +#import +#import "XMPPRoster.h" + +@interface XMPPRoster (PrivateInternalAPI) + +/** + * XMPPRosterStorage classes may optionally use the same delegate(s) as their parent XMPPRoster. + * This method allows such storage classes to access the delegate(s). + * + * Note: If the storage class operates on a different queue than its parent, + * it MUST dispatch all calls to the multicastDelegate onto its parent's queue. + * The parent's dispatch queue is passed in the configureWithParent:queue: method. +**/ +- (GCDMulticastDelegate *)multicastDelegate; + +@end diff --git a/Extensions/Roster/XMPPUser.h b/Extensions/Roster/XMPPUser.h new file mode 100644 index 0000000..1a2d692 --- /dev/null +++ b/Extensions/Roster/XMPPUser.h @@ -0,0 +1,21 @@ +#import +#import "XMPP.h" + +@protocol XMPPResource; + + +@protocol XMPPUser +@required + +- (XMPPJID *)jid; +- (NSString *)nickname; + +- (BOOL)isOnline; +- (BOOL)isPendingApproval; + +- (id )primaryResource; +- (id )resourceForJID:(XMPPJID *)jid; + +- (NSArray *)allResources; + +@end diff --git a/Extensions/SystemInputActivityMonitor/XMPPSystemInputActivityMonitor.h b/Extensions/SystemInputActivityMonitor/XMPPSystemInputActivityMonitor.h new file mode 100644 index 0000000..6b1213c --- /dev/null +++ b/Extensions/SystemInputActivityMonitor/XMPPSystemInputActivityMonitor.h @@ -0,0 +1,56 @@ +#import "XMPPModule.h" + +extern const NSTimeInterval XMPPSystemInputActivityMonitorInactivityTimeIntervalNone; + +#define _XMPP_SYSTEM_INPUT_ACTIVITY_MONITOR_H + +/** + * XMPPSystemInputActivityMonitor is used to keep track of system input activity. + * This module could be used to add features such as "auto away" when the user is inactive for 5 mins. +**/ + +@interface XMPPSystemInputActivityMonitor : XMPPModule +{ + dispatch_source_t timer; + + BOOL active; + NSDate *lastActivityDate; + NSTimeInterval inactivityTimeInterval; +} + +/** + * Returns wether the system is active based on the lastActivityDate and inactivityTimeInterval. +**/ +@property (assign, getter = isActive, readonly) BOOL active; + +/** + * The last time any input activity was detected. +**/ +@property (assign, readonly) NSDate *lastActivityDate; + +/** + * The minimum time interval after the last input activity, that assumes the system is idle. + * + * To disable activity checking set this value to XMPPSystemInputActivityMonitorInactivityTimeIntervalNone + * If set to XMPPSystemInputActivityMonitorInactivityTimeIntervalNone none of delegate methods will be called. + * Setting this value doesn't cause the delegate methods to be called immediately. + * + * Default 300 Seconds +**/ +@property (assign) NSTimeInterval inactivityTimeInterval; + +@end + +@protocol XMPPSystemInputActivityMonitorDelegate + +/** + * The system did become active after being inactive. +**/ +- (void)xmppSystemInputActivityMonitorDidBecomeActive:(XMPPSystemInputActivityMonitor *)xmppSystemInputActivityMonitor; + +/** + * The system did become inactive after being active. +**/ +- (void)xmppSystemInputActivityMonitorDidBecomeInactive:(XMPPSystemInputActivityMonitor *)xmppSystemInputActivityMonitor; + +@end \ No newline at end of file diff --git a/Extensions/SystemInputActivityMonitor/XMPPSystemInputActivityMonitor.m b/Extensions/SystemInputActivityMonitor/XMPPSystemInputActivityMonitor.m new file mode 100644 index 0000000..d32a54b --- /dev/null +++ b/Extensions/SystemInputActivityMonitor/XMPPSystemInputActivityMonitor.m @@ -0,0 +1,202 @@ +#import "XMPPSystemInputActivityMonitor.h" +#import "XMPP.h" +#import "XMPPLogging.h" +#import "XMPPFramework.h" + + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#if TARGET_OS_IPHONE +#warning This file does not work on TARGET_OS_IPHONE. +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG +static const int xmppLogLevel = XMPP_LOG_LEVEL_VERBOSE; +#else +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +const NSTimeInterval XMPPSystemInputActivityMonitorInactivityTimeIntervalNone = -1; + +#define INACTIVITY_TIME_INTERVAL 300.0 +#define TIMER_TIME_INTERVAL 1.0 + +@implementation XMPPSystemInputActivityMonitor + +- (id)init +{ + return [self initWithDispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super initWithDispatchQueue:queue])) + { + inactivityTimeInterval = INACTIVITY_TIME_INTERVAL; + active = YES; + } + return self; +} + + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + XMPPLogTrace(); + + if ([super activate:aXmppStream]) + { + XMPPLogVerbose(@"%@: Activated", THIS_FILE); + +#if !TARGET_OS_IPHONE + + CFTimeInterval secondsSinceLastUserInteraction = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateHIDSystemState, kCGAnyInputEventType); + + NSDate *date = [NSDate dateWithTimeIntervalSinceNow:(secondsSinceLastUserInteraction * -1)]; + [self _setLastActivityDate:date]; + + timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue); + + if (timer) + { + dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), (TIMER_TIME_INTERVAL * NSEC_PER_SEC), 0); + dispatch_source_set_event_handler(timer, ^{ + CFTimeInterval secondsSinceLastUserInteraction = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateHIDSystemState, kCGAnyInputEventType); + + NSDate *date = [NSDate dateWithTimeIntervalSinceNow:(secondsSinceLastUserInteraction * -1)]; + [self _setLastActivityDate:date]; + }); + dispatch_resume(timer); + } +#endif + + return YES; + } + + return NO; +} + +- (void)deactivate +{ + XMPPLogTrace(); + + if(timer) + { + dispatch_source_cancel(timer); + timer = NULL; + } + + [super deactivate]; +} + +- (void)dealloc{ + + if(timer) + { + dispatch_source_cancel(timer); + timer = NULL; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma Internal +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)_setLastActivityDate:(NSDate *)flag{ + + dispatch_block_t block = ^{ + lastActivityDate = flag; + + if(inactivityTimeInterval > XMPPSystemInputActivityMonitorInactivityTimeIntervalNone) + { + if(active && -[lastActivityDate timeIntervalSinceNow] > inactivityTimeInterval) + { + active = NO; + [multicastDelegate xmppSystemInputActivityMonitorDidBecomeInactive:self]; + } + else if(!active && -[lastActivityDate timeIntervalSinceNow] < inactivityTimeInterval) + { + active = YES; + [multicastDelegate xmppSystemInputActivityMonitorDidBecomeActive:self]; + } + + }else{ + active = YES; + } + + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isActive +{ + __block BOOL result = YES;; + + dispatch_block_t block = ^{ + result = active; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (NSDate *)lastActivityDate +{ + __block NSDate *result = nil;; + + dispatch_block_t block = ^{ + result = lastActivityDate; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (NSTimeInterval)inactivityTimeInterval +{ + __block NSTimeInterval result = XMPPSystemInputActivityMonitorInactivityTimeIntervalNone; + + dispatch_block_t block = ^{ + result = inactivityTimeInterval; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setInactivityTimeInterval:(NSTimeInterval)flag +{ + dispatch_block_t block = ^{ + inactivityTimeInterval = flag; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +@end diff --git a/Extensions/XEP-0009/XMPPIQ+JabberRPC.h b/Extensions/XEP-0009/XMPPIQ+JabberRPC.h new file mode 100644 index 0000000..21b6c89 --- /dev/null +++ b/Extensions/XEP-0009/XMPPIQ+JabberRPC.h @@ -0,0 +1,77 @@ +// +// XMPPIQ+JabberRPC.h +// XEP-0009 +// +// Created by Eric Chamberlain on 5/16/10. +// + +#import + +#import "XMPPIQ.h" + + +@interface XMPPIQ(JabberRPC) + +/** + * Creates and returns a new autoreleased XMPPIQ. + * This is the only method you normally need to call. + **/ ++ (XMPPIQ *)rpcTo:(XMPPJID *)jid methodName:(NSString *)method parameters:(NSArray *)parameters; + +#pragma mark - +#pragma mark Element helper methods + +// returns a Jabber-RPC query elelement +// ++(NSXMLElement *)elementRpcQuery; + +// returns a Jabber-RPC methodCall element +// ++(NSXMLElement *)elementMethodCall; + +// returns a Jabber-RPC methodName element +// method ++(NSXMLElement *)elementMethodName:(NSString *)method; + +// returns a Jabber-RPC params element +// ++(NSXMLElement *)elementParams; + +#pragma mark - +#pragma mark Disco elements + +// returns the Disco query identity element +// ++(NSXMLElement *)elementRpcIdentity; + +// returns the Disco query feature element +// ++(NSXMLElement *)elementRpcFeature; + +#pragma mark - +#pragma mark Conversion methods + +// encode any object into JabberRPC formatted element +// this method calls the others ++(NSXMLElement *)paramElementFromObject:(id)object; + ++(NSXMLElement *)valueElementFromObject:(id)object; + ++(NSXMLElement *)valueElementFromArray:(NSArray *)array; ++(NSXMLElement *)valueElementFromDictionary:(NSDictionary *)dictionary; + ++(NSXMLElement *)valueElementFromBoolean:(CFBooleanRef)boolean; ++(NSXMLElement *)valueElementFromNumber:(NSNumber *)number; ++(NSXMLElement *)valueElementFromString:(NSString *)string; ++(NSXMLElement *)valueElementFromDate:(NSDate *)date; ++(NSXMLElement *)valueElementFromData:(NSData *)data; + ++(NSXMLElement *)valueElementFromElementWithName:(NSString *)elementName value:(NSString *)value; + + +#pragma mark Wrapper methods + ++(NSXMLElement *)wrapElement:(NSString *)elementName aroundElement:(NSXMLElement *)element; ++(NSXMLElement *)wrapValueElementAroundElement:(NSXMLElement *)element; + +@end diff --git a/Extensions/XEP-0009/XMPPIQ+JabberRPC.m b/Extensions/XEP-0009/XMPPIQ+JabberRPC.m new file mode 100644 index 0000000..c170d7e --- /dev/null +++ b/Extensions/XEP-0009/XMPPIQ+JabberRPC.m @@ -0,0 +1,219 @@ +// +// XMPPIQ+JabberRPC.m +// XEP-0009 +// +// Created by Eric Chamberlain on 5/16/10. +// + + +#import "XMPPIQ+JabberRPC.h" +#import "XMPP.h" +#import "NSData+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation XMPPIQ(JabberRPC) + ++(XMPPIQ *)rpcTo:(XMPPJID *)jid methodName:(NSString *)method parameters:(NSArray *)parameters { + // Send JabberRPC element + // + // + // + // + // method + // + // + // example + // + // ... + // + // + // + // + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:jid elementID:[XMPPStream generateUUID]]; + + NSXMLElement *jabberRPC = [self elementRpcQuery]; + NSXMLElement *methodCall = [self elementMethodCall]; + NSXMLElement *methodName = [self elementMethodName:method]; + NSXMLElement *params = [self elementParams]; + + for (id parameter in parameters) { + [params addChild:[self paramElementFromObject:parameter]]; + } + + [methodCall addChild:methodName]; + [methodCall addChild:params]; + [jabberRPC addChild:methodCall]; + [iq addChild:jabberRPC]; + return iq; +} + +#pragma mark Element helper methods + ++(NSXMLElement *)elementRpcQuery { + return [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:rpc"]; +} + ++(NSXMLElement *)elementMethodCall { + return [NSXMLElement elementWithName:@"methodCall"]; +} + ++(NSXMLElement *)elementMethodName:(NSString *)method { + return [NSXMLElement elementWithName:@"methodName" stringValue:method]; +} + ++(NSXMLElement *)elementParams { + return [NSXMLElement elementWithName:@"params"]; +} + + +#pragma mark - +#pragma mark Disco elements + ++(NSXMLElement *)elementRpcIdentity { + NSXMLElement *identity = [NSXMLElement elementWithName:@"identity"]; + [identity addAttributeWithName:@"category" stringValue:@"automation"]; + [identity addAttributeWithName:@"type" stringValue:@"rpc"]; + return identity; +} + +// returns the Disco query feature element +// ++(NSXMLElement *)elementRpcFeature { + NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"]; + [feature addAttributeWithName:@"var" stringValue:@"jabber:iq:rpc"]; + return feature; +} +#pragma mark Conversion methods + ++(NSXMLElement *)paramElementFromObject:(id)object { + if (!object) { + return nil; + } + return [self wrapElement:@"param" aroundElement:[self valueElementFromObject:object]]; +} + ++(NSXMLElement *)valueElementFromObject:(id)object { + if (!object) { + return nil; + } + + if ([object isKindOfClass: [NSArray class]]) { + return [self valueElementFromArray: object]; + } else if ([object isKindOfClass: [NSDictionary class]]) { + return [self valueElementFromDictionary: object]; + } else if (((__bridge CFBooleanRef)object == kCFBooleanTrue) || ((__bridge CFBooleanRef)object == kCFBooleanFalse)) { + return [self valueElementFromBoolean: (__bridge CFBooleanRef)object]; + } else if ([object isKindOfClass: [NSNumber class]]) { + return [self valueElementFromNumber: object]; + } else if ([object isKindOfClass: [NSString class]]) { + return [self valueElementFromString: object]; + } else if ([object isKindOfClass: [NSDate class]]) { + return [self valueElementFromDate: object]; + } else if ([object isKindOfClass: [NSData class]]) { + return [self valueElementFromData: object]; + } else { + return [self valueElementFromString: object]; + } + +} + + ++(NSXMLElement *)valueElementFromArray:(NSArray *)array { + NSXMLElement *data = [NSXMLElement elementWithName:@"data"]; + + for (id object in array) { + [data addChild:[self valueElementFromObject:object]]; + } + return [self wrapValueElementAroundElement:data]; +} + + ++(NSXMLElement *)valueElementFromDictionary:(NSDictionary *)dictionary { + NSXMLElement *structElement = [NSXMLElement elementWithName:@"struct"]; + + NSXMLElement *member; + NSXMLElement *name; + + for (NSString *key in dictionary) { + member = [NSXMLElement elementWithName:@"member"]; + name = [NSXMLElement elementWithName:@"name" stringValue:key]; + [member addChild:name]; + [member addChild:[self valueElementFromObject:dictionary[key]]]; + } + + return [self wrapValueElementAroundElement:structElement]; +} + + ++(NSXMLElement *)valueElementFromBoolean:(CFBooleanRef)boolean { + if (boolean == kCFBooleanTrue) { + return [self valueElementFromElementWithName:@"boolean" value:@"1"]; + } else { + return [self valueElementFromElementWithName:@"boolean" value:@"0"]; + } +} + + ++(NSXMLElement *)valueElementFromNumber:(NSNumber *)number { + // what type of NSNumber is this? + if ([[NSString stringWithCString: [number objCType] + encoding: NSUTF8StringEncoding] isEqualToString: @"d"]) { + return [self valueElementFromElementWithName:@"double" value:[number stringValue]]; + } else { + return [self valueElementFromElementWithName:@"i4" value:[number stringValue]]; + } +} + + ++(NSXMLElement *)valueElementFromString:(NSString *)string { + return [self valueElementFromElementWithName:@"string" value:string]; +} + + ++(NSXMLElement *)valueElementFromDate:(NSDate *)date { + unsigned calendarComponents = kCFCalendarUnitYear | + kCFCalendarUnitMonth | + kCFCalendarUnitDay | + kCFCalendarUnitHour | + kCFCalendarUnitMinute | + kCFCalendarUnitSecond; + NSDateComponents *dateComponents = [[NSCalendar currentCalendar] components:calendarComponents fromDate:date]; + NSString *dateString = [NSString stringWithFormat: @"%.4ld%.2ld%.2ldT%.2ld:%.2ld:%.2ld", + (long)[dateComponents year], + (long)[dateComponents month], + (long)[dateComponents day], + (long)[dateComponents hour], + (long)[dateComponents minute], + (long)[dateComponents second], nil]; + + return [self valueElementFromElementWithName:@"dateTime.iso8601" value: dateString]; +} + + ++(NSXMLElement *)valueElementFromData:(NSData *)data { + return [self valueElementFromElementWithName:@"base64" value:[data xmpp_base64Encoded]]; +} + ++(NSXMLElement *)valueElementFromElementWithName:(NSString *)elementName value:(NSString *)value { + return [self wrapValueElementAroundElement:[NSXMLElement elementWithName:elementName stringValue:value]]; +} + + +#pragma mark Wrapper methods + ++(NSXMLElement *)wrapElement:(NSString *)elementName aroundElement:(NSXMLElement *)element { + NSXMLElement *wrapper = [NSXMLElement elementWithName:elementName]; + [wrapper addChild:element]; + return wrapper; +} + ++(NSXMLElement *)wrapValueElementAroundElement:(NSXMLElement *)element { + return [self wrapElement:@"value" aroundElement:element]; +} + +@end diff --git a/Extensions/XEP-0009/XMPPIQ+JabberRPCResonse.h b/Extensions/XEP-0009/XMPPIQ+JabberRPCResonse.h new file mode 100644 index 0000000..6347d38 --- /dev/null +++ b/Extensions/XEP-0009/XMPPIQ+JabberRPCResonse.h @@ -0,0 +1,66 @@ +// +// XMPPIQ+JabberRPCResonse.h +// XEP-0009 +// +// Created by Eric Chamberlain on 5/25/10. +// + +#import "XMPPIQ.h" + +typedef enum { + JabberRPCElementTypeArray, + JabberRPCElementTypeDictionary, + JabberRPCElementTypeMember, + JabberRPCElementTypeName, + JabberRPCElementTypeInteger, + JabberRPCElementTypeDouble, + JabberRPCElementTypeBoolean, + JabberRPCElementTypeString, + JabberRPCElementTypeDate, + JabberRPCElementTypeData +} JabberRPCElementType; + + +@interface XMPPIQ(JabberRPCResonse) + +-(NSXMLElement *)methodResponseElement; + +// is this a Jabber RPC method response +-(BOOL)isMethodResponse; + +-(BOOL)isFault; + +-(BOOL)isJabberRPC; + +-(id)methodResponse:(NSError **)error; + +-(id)objectFromElement:(NSXMLElement *)param; + + +#pragma mark - + +-(NSArray *)parseArray:(NSXMLElement *)arrayElement; + +-(NSDictionary *)parseStruct:(NSXMLElement *)structElement; + +-(NSDictionary *)parseMember:(NSXMLElement *)memberElement; + +#pragma mark - + +- (NSDate *)parseDateString: (NSString *)dateString withFormat: (NSString *)format; + +#pragma mark - + +- (NSNumber *)parseInteger: (NSString *)value; + +- (NSNumber *)parseDouble: (NSString *)value; + +- (NSNumber *)parseBoolean: (NSString *)value; + +- (NSString *)parseString: (NSString *)value; + +- (NSDate *)parseDate: (NSString *)value; + +- (NSData *)parseData: (NSString *)value; + +@end diff --git a/Extensions/XEP-0009/XMPPIQ+JabberRPCResonse.m b/Extensions/XEP-0009/XMPPIQ+JabberRPCResonse.m new file mode 100644 index 0000000..3048170 --- /dev/null +++ b/Extensions/XEP-0009/XMPPIQ+JabberRPCResonse.m @@ -0,0 +1,290 @@ +// +// XMPPIQ+JabberRPCResonse.m +// XEP-0009 +// +// Created by Eric Chamberlain on 5/25/10. +// + +#import "XMPPIQ+JabberRPCResonse.h" +#import "XMPPJabberRPCModule.h" +#import "XMPPLogging.h" +#import "NSData+XMPP.h" +#import "NSXMLElement+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +@implementation XMPPIQ(JabberRPCResonse) + +-(NSXMLElement *)methodResponseElement { + NSXMLElement *query = [self elementForName:@"query"]; + return [query elementForName:@"methodResponse"]; +} + +// is this a Jabber RPC method response +-(BOOL)isMethodResponse { + /* + + + + South Dakota + + + + */ + NSXMLElement *methodResponse = [self methodResponseElement]; + return methodResponse != nil; +} + +-(BOOL)isFault { + /* + + + + + + faultCode + 4 + + + faultString + Too many parameters. + + + + + + */ + NSXMLElement *methodResponse = [self methodResponseElement]; + + return [methodResponse elementForName:@"fault"] != nil; +} + +-(BOOL)isJabberRPC { + /* + + ... + + */ + NSXMLElement *rpcQuery = [self elementForName:@"query" xmlns:@"jabber:iq:rpc"]; + return rpcQuery != nil; +} + +-(id)methodResponse:(NSError **)error { + id response = nil; + + /* + + + + South Dakota + + + + */ + + // or + + /* + + + + + + faultCode + 4 + + + faultString + Too many parameters. + + + + + + */ + NSXMLElement *methodResponse = [self methodResponseElement]; + + // parse the methodResponse + + response = [self objectFromElement:(NSXMLElement *)[methodResponse childAtIndex:0]]; + + if ([self isFault]) { + // we should produce an error + // response should be a dict + if (error) { + *error = [NSError errorWithDomain:XMPPJabberRPCErrorDomain + code:[[response objectForKey:@"faultCode"] intValue] + userInfo:(NSDictionary *)response]; + } + response = nil; + } else { + if (error) { + *error = nil; + } + } + + return response; +} + +-(id)objectFromElement:(NSXMLElement *)param { + NSString *element = [param name]; + + if ([element isEqualToString:@"params"] || + [element isEqualToString:@"param"] || + [element isEqualToString:@"fault"] || + [element isEqualToString:@"value"]) { + + NSXMLElement *firstChild = (NSXMLElement *)[param childAtIndex:0]; + if (firstChild) { + return [self objectFromElement:firstChild]; + } else { + // no child element, treat it like a string + return [self parseString:[param stringValue]]; + } + } else if ([element isEqualToString:@"string"] || + [element isEqualToString:@"name"]) { + return [self parseString:[param stringValue]]; + } else if ([element isEqualToString:@"member"]) { + return [self parseMember:param]; + } else if ([element isEqualToString:@"array"]) { + return [self parseArray:param]; + } else if ([element isEqualToString:@"struct"]) { + return [self parseStruct:param]; + } else if ([element isEqualToString:@"int"]) { + return [self parseInteger:[param stringValue]]; + } else if ([element isEqualToString:@"double"]) { + return [self parseDouble:[param stringValue]]; + } else if ([element isEqualToString:@"boolean"]) { + return [self parseBoolean:[param stringValue]]; + } else if ([element isEqualToString:@"dateTime.iso8601"]) { + return [self parseDate:[param stringValue]]; + } else if ([element isEqualToString:@"base64"]) { + return [self parseData:[param stringValue]]; + } else { + // bad element + XMPPLogWarn(@"%@: %@ - bad element: %@", THIS_FILE, THIS_METHOD, [param stringValue]); + } + return nil; +} + + +#pragma mark - + +-(NSArray *)parseArray:(NSXMLElement *)arrayElement { + /* + + + 12 + Egypt + 0 + -31 + + + */ + NSXMLElement *data = (NSXMLElement *)[arrayElement childAtIndex:0]; + NSArray *children = [data children]; + + NSMutableArray *array = [NSMutableArray arrayWithCapacity:[children count]]; + + for (NSXMLElement *child in children) { + [array addObject:[self objectFromElement:child]]; + } + + return array; +} + +-(NSDictionary *)parseStruct:(NSXMLElement *)structElement { + /* + + + lowerBound + 18 + + + upperBound + 139 + + + */ + NSArray *children = [structElement children]; + NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:[children count]]; + + for (NSXMLElement *child in children) { + [dict addEntriesFromDictionary:[self parseMember:child]]; + } + + return dict; +} + +-(NSDictionary *)parseMember:(NSXMLElement *)memberElement { + NSString *key = [self objectFromElement:[memberElement elementForName:@"name"]]; + id value = [self objectFromElement:[memberElement elementForName:@"value"]]; + + return @{key : value}; +} + +#pragma mark - + +- (NSDate *)parseDateString: (NSString *)dateString withFormat: (NSString *)format { + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]; + [dateFormatter setDateFormat: format]; + + NSDate *result = [dateFormatter dateFromString: dateString]; + + return result; +} + +#pragma mark - + +- (NSNumber *)parseInteger: (NSString *)value { + return @([value integerValue]); +} + +- (NSNumber *)parseDouble: (NSString *)value { + return @([value doubleValue]); +} + +- (NSNumber *)parseBoolean: (NSString *)value { + if ([value isEqualToString: @"1"]) { + return @YES; + } + + return @NO; +} + +- (NSString *)parseString: (NSString *)value { + return [value stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]; +} + +- (NSDate *)parseDate: (NSString *)value { + NSDate *result = nil; + + result = [self parseDateString: value withFormat: @"yyyyMMdd'T'HH:mm:ss"]; + + if (!result) { + result = [self parseDateString: value withFormat: @"yyyy'-'MM'-'dd'T'HH:mm:ss"]; + } + + return result; +} + +- (NSData *)parseData: (NSString *)value { + // Convert the base 64 encoded data into a string + NSData *base64Data = [value dataUsingEncoding:NSASCIIStringEncoding]; + NSData *decodedData = [base64Data xmpp_base64Decoded]; + + return decodedData; +} + + +@end diff --git a/Extensions/XEP-0009/XMPPJabberRPCModule.h b/Extensions/XEP-0009/XMPPJabberRPCModule.h new file mode 100644 index 0000000..8c3274f --- /dev/null +++ b/Extensions/XEP-0009/XMPPJabberRPCModule.h @@ -0,0 +1,50 @@ +// +// XMPPJabberRPCModule.h +// XEP-0009 +// +// Originally created by Eric Chamberlain on 5/16/10. +// + +#import +#import "XMPPModule.h" + +#define _XMPP_JABBER_RPC_MODULE_H + +extern NSString *const XMPPJabberRPCErrorDomain; + +@class XMPPJID; +@class XMPPStream; +@class XMPPIQ; +@protocol XMPPJabberRPCModuleDelegate; + + +@interface XMPPJabberRPCModule : XMPPModule +{ + NSMutableDictionary *rpcIDs; + NSTimeInterval defaultTimeout; +} + +@property (nonatomic, assign) NSTimeInterval defaultTimeout; + +- (NSString *)sendRpcIQ:(XMPPIQ *)iq; +- (NSString *)sendRpcIQ:(XMPPIQ *)iq withTimeout:(NSTimeInterval)timeout; + +// caller knows best when a request has timed out +// it should remove the rpcID, on timeout. +- (void)timeoutRemoveRpcID:(NSString *)rpcID; + +@end + + +@protocol XMPPJabberRPCModuleDelegate +@optional + +// sent when transport error is received +-(void)jabberRPC:(XMPPJabberRPCModule *)sender elementID:(NSString *)elementID didReceiveError:(NSError *)error; + +// sent when a methodResponse comes back +-(void)jabberRPC:(XMPPJabberRPCModule *)sender elementID:(NSString *)elementID didReceiveMethodResponse:(id)response; + +// sent when a Jabber-RPC server request is received +-(void)jabberRPC:(XMPPJabberRPCModule *)sender didReceiveSetIQ:(XMPPIQ *)iq; +@end diff --git a/Extensions/XEP-0009/XMPPJabberRPCModule.m b/Extensions/XEP-0009/XMPPJabberRPCModule.m new file mode 100644 index 0000000..1177ebc --- /dev/null +++ b/Extensions/XEP-0009/XMPPJabberRPCModule.m @@ -0,0 +1,341 @@ +// +// XMPPJabberRPCModule.m +// XEP-0009 +// +// Originally created by Eric Chamberlain on 5/16/10. +// + +#import "XMPPJabberRPCModule.h" +#import "XMPP.h" +#import "XMPPIQ+JabberRPC.h" +#import "XMPPIQ+JabberRPCResonse.h" +#import "XMPPLogging.h" +#import "XMPPFramework.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +NSString *const XMPPJabberRPCErrorDomain = @"XMPPJabberRPCErrorDomain"; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface RPCID : NSObject +{ + NSString *rpcID; + dispatch_source_t timer; +} + +@property (nonatomic, readonly) NSString *rpcID; +@property (nonatomic, readonly) dispatch_source_t timer; + +- (id)initWithRpcID:(NSString *)rpcID timer:(dispatch_source_t)timer; + +- (void)cancelTimer; + +@end + +@implementation RPCID + +@synthesize rpcID; +@synthesize timer; + +- (id)initWithRpcID:(NSString *)aRpcID timer:(dispatch_source_t)aTimer +{ + if ((self = [super init])) + { + rpcID = [aRpcID copy]; + + timer = aTimer; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(timer); + #endif + } + return self; +} + +- (BOOL)isEqual:(id)anObject +{ + return [rpcID isEqual:anObject]; +} + +- (NSUInteger)hash +{ + return [rpcID hash]; +} + +- (void)cancelTimer +{ + if (timer) + { + dispatch_source_cancel(timer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(timer); + #endif + timer = NULL; + } +} + +- (void)dealloc +{ + [self cancelTimer]; + +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPJabberRPCModule + +@dynamic defaultTimeout; + +- (NSTimeInterval)defaultTimeout +{ + __block NSTimeInterval result; + + dispatch_block_t block = ^{ + result = defaultTimeout; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setDefaultTimeout:(NSTimeInterval)newDefaultTimeout +{ + dispatch_block_t block = ^{ + XMPPLogTrace(); + defaultTimeout = newDefaultTimeout; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Module Management +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)init +{ + return [self initWithDispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super initWithDispatchQueue:queue])) + { + XMPPLogTrace(); + + rpcIDs = [[NSMutableDictionary alloc] initWithCapacity:5]; + defaultTimeout = 5.0; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + XMPPLogTrace(); + + if ([super activate:aXmppStream]) + { + #ifdef _XMPP_CAPABILITIES_H + [xmppStream autoAddDelegate:self delegateQueue:moduleQueue toModulesOfClass:[XMPPCapabilities class]]; + #endif + + return YES; + } + + return NO; +} + +- (void)deactivate +{ + XMPPLogTrace(); + +#ifdef _XMPP_CAPABILITIES_H + [xmppStream removeAutoDelegate:self delegateQueue:moduleQueue fromModulesOfClass:[XMPPCapabilities class]]; +#endif + + [super deactivate]; +} + +- (void)dealloc +{ + XMPPLogTrace(); + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Send RPC +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSString *)sendRpcIQ:(XMPPIQ *)iq +{ + return [self sendRpcIQ:iq withTimeout:[self defaultTimeout]]; +} + +- (NSString *)sendRpcIQ:(XMPPIQ *)iq withTimeout:(NSTimeInterval)timeout +{ + XMPPLogTrace(); + + NSString *elementID = [iq elementID]; + + dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue); + + dispatch_source_set_event_handler(timer, ^{ @autoreleasepool { + + [self timeoutRemoveRpcID:elementID]; + }}); + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(timer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(timer); + + RPCID *rpcID = [[RPCID alloc] initWithRpcID:elementID timer:timer]; + + rpcIDs[elementID] = rpcID; + + [xmppStream sendElement:iq]; + + return elementID; +} + +- (void)timeoutRemoveRpcID:(NSString *)elementID +{ + XMPPLogTrace(); + + RPCID *rpcID = rpcIDs[elementID]; + if (rpcID) + { + [rpcID cancelTimer]; + [rpcIDs removeObjectForKey:elementID]; + + NSError *error = [NSError errorWithDomain:XMPPJabberRPCErrorDomain + code:1400 + userInfo:@{@"error" : @"Request timed out"}]; + + [multicastDelegate jabberRPC:self elementID:elementID didReceiveError:error]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + XMPPLogTrace(); + + if ([iq isJabberRPC]) + { + if ([iq isResultIQ] || [iq isErrorIQ]) + { + // Example: + // + // + + NSString *elementID = [iq elementID]; + + // check if this is a JabberRPC query + // we could check the query element, but we should be able to do a lookup based on the unique elementID + // because we send an ID, we should get one back + + RPCID *rpcID = rpcIDs[elementID]; + if (rpcID == nil) + { + return NO; + } + + XMPPLogVerbose(@"%@: Received RPC response!", THIS_FILE); + + if ([iq isResultIQ]) + { + id response; + NSError *error = nil; + + //TODO: parse iq and generate response. + response = [iq methodResponse:&error]; + + if (error == nil) { + [multicastDelegate jabberRPC:self elementID:elementID didReceiveMethodResponse:response]; + } else { + [multicastDelegate jabberRPC:self elementID:elementID didReceiveError:error]; + } + + } + else + { + // TODO: implement error parsing + // not much specified in XEP, only 403 forbidden error + NSXMLElement *errorElement = [iq childErrorElement]; + NSError *error = [NSError errorWithDomain:XMPPJabberRPCErrorDomain + code:[errorElement attributeIntValueForName:@"code"] + userInfo:@{ + @"error" : [errorElement attributesAsDictionary], + @"condition" : [[errorElement childAtIndex:0] name], + @"iq" : iq}]; + + [multicastDelegate jabberRPC:self elementID:elementID didReceiveError:error]; + } + + [rpcID cancelTimer]; + [rpcIDs removeObjectForKey:elementID]; + +#ifdef _XMPP_CAPABILITIES_H + } else if ([iq isSetIQ]) { + // we would receive set when implementing Jabber-RPC server + + [multicastDelegate jabberRPC:self didReceiveSetIQ:iq]; +#endif + } + + // Jabber-RPC doesn't use get iq type + } + return NO; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPCapabilities delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifdef _XMPP_CAPABILITIES_H +/** + * If an XMPPCapabilites instance is used we want to advertise our support for JabberRPC. +**/ +- (void)xmppCapabilities:(XMPPCapabilities *)sender collectingMyCapabilities:(NSXMLElement *)query +{ + XMPPLogTrace(); + + // + // ... + // + // + // ... + // + [query addChild:[XMPPIQ elementRpcIdentity]]; + [query addChild:[XMPPIQ elementRpcFeature]]; +} +#endif + +@end diff --git a/Extensions/XEP-0012/XMPPIQ+LastActivity.h b/Extensions/XEP-0012/XMPPIQ+LastActivity.h new file mode 100644 index 0000000..884119e --- /dev/null +++ b/Extensions/XEP-0012/XMPPIQ+LastActivity.h @@ -0,0 +1,94 @@ +// +// XMPPIQ+LastActivity.h +// XEP-0012 +// +// Created by Daniel Rodríguez Troitiño on 1/26/2013. +// + +#import + +#import "XMPPIQ.h" + +@class XMPPJID; + +/** + * Last Activity XEP-0012 namespace: "jabber:iq:last". + */ +extern NSString *const XMPPLastActivityNamespace; + +/** + * Helper methods to create last activity queries inside IQ stanzas or to + * inspect result IQ answering last activity queries. + */ +@interface XMPPIQ (LastActivity) + +/** + * Returns an get XMPPIQ with a last activity query child element addressed to + * the given JID. + */ ++ (XMPPIQ *)lastActivityQueryTo:(XMPPJID *)jid; + +/** + * Returns a result XMPPIQ answering the given XMPPIQ request with the given + * number of seconds. + * + * The from and to attributes of the returned XMPPIQ will be switched from the + * from and to attributes of the given request. The element ID will be the same + * of the given request. No status message will be included. + */ ++ (XMPPIQ *)lastActivityResponseToIQ:(XMPPIQ *)request withSeconds:(NSUInteger)seconds; + +/** + * Returns a result XMPPIQ answering the given XMPPIQ request with the given + * number of seconds and the given unavailable status. + * + * The from and to attributes of the returned XMPPIQ will be switched from the + * from and to attributes of the given request. The element ID will be the same + * of the given request. + */ ++ (XMPPIQ *)lastActivityResponseToIQ:(XMPPIQ *)request withSeconds:(NSUInteger)seconds status:(NSString *)status; + +/** + * Returns an error XMPPIQ answering the given XMPPIQ request. + * + * The from and to attributes of the returned XMPPIQ will be switched from the + * from and to attributes of the given request. The element ID will be the same + * of the given request. + * + * An error element with a forbidden element will be included as child element: + * + * + * + * + * + * + */ ++ (XMPPIQ *)lastActivityResponseForbiddenToIQ:(XMPPIQ *)request; + +/** + * Returns YES if the child element of this XMPPIQ is a query element from the + * XMPPLastActivityNamespace. Otherwise returns NO. + */ +- (BOOL)isLastActivityQuery; + +/** + * Returns the parsed number from the seconds attribute of the query subelement. + * + * Returns NSNotFound in case this XMPPIQ has no query element, or that query + * element does not have a seconds attribute. Returns 0 in case the seconds + * attribute cannot be parsed as a integer. + */ +- (NSUInteger)lastActivitySeconds; + +/** + * Returns the contents of the query subelement. + * + * Returns nil in case this XMPPIQ has no query element, or the query element + * does not have contents. + */ +- (NSString *)lastActivityUnavailableStatus; + +@end diff --git a/Extensions/XEP-0012/XMPPIQ+LastActivity.m b/Extensions/XEP-0012/XMPPIQ+LastActivity.m new file mode 100644 index 0000000..b0ec5b5 --- /dev/null +++ b/Extensions/XEP-0012/XMPPIQ+LastActivity.m @@ -0,0 +1,97 @@ +// +// XMPPIQ+LastActivity.m +// XEP-0012 +// +// Created by Daniel Rodríguez Troitiño on 1/26/2013. +// + +#import "XMPPIQ+LastActivity.h" + +#import "XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +NSString *const XMPPLastActivityNamespace = @"jabber:iq:last"; + +@implementation XMPPIQ (LastActivity) + ++ (XMPPIQ *)lastActivityQueryTo:(XMPPJID *)jid +{ + NSXMLElement *query = [[NSXMLElement alloc] initWithName:@"query" xmlns:XMPPLastActivityNamespace]; + return [[self alloc] initWithType:@"get" to:jid elementID:[XMPPStream generateUUID] child:query]; +} + ++ (XMPPIQ *)lastActivityResponseToIQ:(XMPPIQ *)request withSeconds:(NSUInteger)seconds +{ + return [self lastActivityResponseToIQ:request withSeconds:seconds status:nil]; +} + ++ (XMPPIQ *)lastActivityResponseToIQ:(XMPPIQ *)request withSeconds:(NSUInteger)seconds status:(NSString *)status +{ + NSXMLElement *query = [[NSXMLElement alloc] initWithName:@"query" xmlns:XMPPLastActivityNamespace]; + [query addAttributeWithName:@"seconds" stringValue:[NSString stringWithFormat:@"%lu", (unsigned long)seconds]]; + if (status && [status length] > 0) + { + [query setStringValue:status]; + } + + return [[self alloc] initWithType:@"result" to:request.from elementID:request.elementID child:query]; +} + ++ (XMPPIQ *)lastActivityResponseForbiddenToIQ:(XMPPIQ *)request +{ + NSXMLElement *reason = [[NSXMLElement alloc] initWithName:@"forbidden" xmlns:@"urn:ietf:params:xml:ns:xmpp-stanzas"]; + NSXMLElement *error = [[NSXMLElement alloc] initWithName:@"error"]; + [error addAttributeWithName:@"type" stringValue:@"auth"]; + [error addChild:reason]; + + return [[self alloc] initWithType:@"error" to:request.from elementID:request.elementID child:error]; +} + +- (BOOL)isLastActivityQuery +{ + return nil != [self lastActivityQueryElement]; +} + +- (NSUInteger)lastActivitySeconds +{ + NSUInteger seconds = NSNotFound; + NSXMLElement *query = [self lastActivityQueryElement]; + if (query) + { + NSXMLNode *attribute = [query attributeForName:@"seconds"]; + if (attribute) + { + // Some Servers Return -1 to indicate no activity, so we need to ignore these + if([query attributeIntegerValueForName:@"seconds"] >= 0) + { + seconds = [query attributeUnsignedIntegerValueForName:@"seconds"]; + } + } + } + + return seconds; +} + +- (NSString *)lastActivityUnavailableStatus +{ + NSXMLElement *query = [self lastActivityQueryElement]; + if (query) + { + return [query stringValue]; + } + else + { + return nil; + } +} + +- (NSXMLElement *)lastActivityQueryElement +{ + return [self elementForName:@"query" xmlns:XMPPLastActivityNamespace]; +} + +@end + diff --git a/Extensions/XEP-0012/XMPPLastActivity.h b/Extensions/XEP-0012/XMPPLastActivity.h new file mode 100644 index 0000000..f266445 --- /dev/null +++ b/Extensions/XEP-0012/XMPPLastActivity.h @@ -0,0 +1,119 @@ +// +// XMPPLastActivity.h +// XEP-0012 +// +// Created by Daniel Rodríguez Troitiño on 1/26/2013. +// +// + +#import "XMPP.h" +#import "XMPPIQ+LastActivity.h" + +#define _XMPP_LAST_ACTIVITY_H + +/** + * Provides support to both sending last activity queries and answering those + * last activity queries done by other entities, as documented in the XEP-0012. + * + * The automatic responses can be disabled setting respondsToQueries to NO. + * + * If no delegate of this class responds to + * numberOfIdleTimeSecondsForXMPPLastActivity:currentIdleTimeSeconds: a default + * value of 0 will be send to the other entities. You are encouraged to provide + * at least one delegate with this method implemented. + */ +@interface XMPPLastActivity : XMPPModule + +/** + * Whether or not the module should respond to incoming last activity queries. + * + * If you create multiple instances of this module, only one instance should + * respond to queries. + * + * It is recommended you set this (if needed) before you activate the module. + * + * The default value is YES. + */ +@property (atomic, assign) BOOL respondsToQueries; + +/** + * Send a last activity query to an specific JID. + * + * XEP-0012 specifies that last activity queries can be send to offline + * entities (bare JID), to online resources (full JID) or servers (domain only + * JID). The answers of each of those kind of JIDs are a little different and + * has different meaning. It is responsability of the consumer to interpretate + * those answers. + * + * The module will wait for an answer from the server or the entity for at most + * 30 seconds before considering this request a timeout failure. + * + * The delegates of the module should implement + * xmppLastActivity:didReceiveResponse: and + * xmppLastActivity:didNotReceiveResponse:dueToTimeout: to be informed when the + * result of the IQ arrives (or the timeout happens). + * + * The returning string is the element ID of the IQ that was sent including the + * last activity query. + */ +- (NSString *)sendLastActivityQueryToJID:(XMPPJID *)jid; + +/** + * Send a last activity query to an specific JID with an specific timeout. + * + * XEP-0012 specifies that last activity queries can be send to offline + * entities (bare JID), to online resources (full JID) or servers (domain only + * JID). The answers of each of those kind of JIDs are a little different and + * has different meaning. It is responsability of the consumer to interpretate + * those answers. + * + * The module will wait for an answer from the server or the entity for at most + * the number seconds given by tiemout before considering this request a timeout + * failure. + * + * The delegates of the module should implement + * xmppLastActivity:didReceiveResponse: and + * xmppLastActivity:didNotReceiveResponse:dueToTimeout: to be informed when the + * result of the IQ arrives (or the timeout happens). + * + * The returning string is the element ID of the IQ that was sent including the + * last activity query. + */ +- (NSString *)sendLastActivityQueryToJID:(XMPPJID *)jid withTimeout:(NSTimeInterval)timeout; + +@end + +@protocol XMPPLastActivityDelegate + +/** + * Callback when the XMPPLastActivity sender receives a result IQ or an error IQ + * for a previous get IQ. + * + * The XMPPIQ response can be queried using the methods in the category + * XMPPIQ (LastActivity) for the seconds and status value. + */ +- (void)xmppLastActivity:(XMPPLastActivity *)sender didReceiveResponse:(XMPPIQ *)response; + +/** + * Callback when the XMPPLastActivity sender does not receive neither a result + * IQ nor a error IQ after timeout seconds have passed. + * + * The queryID is the element ID used to send the query, so the consumer can + * compare it with the element ID returned by the sendLastActivityQueryToJID: + * methods and act accordingly. + */ +- (void)xmppLastActivity:(XMPPLastActivity *)sender didNotReceiveResponse:(NSString *)queryID dueToTimeout:(NSTimeInterval)timeout; + +/** + * Callback to obtain the number of idle seconds that the XMPPLastActivity + * sender should use as answer to a last activity query iq. + * + * Each delegate will be asked in turn (the order is not guaranteed) and each of + * then should decide to return the given idleSeconds value, or a new value as + * the number of idleSeconds. The first delegate will receive NSNotFound as the + * value of idleSecond. If the last delegate returns NSNotFound as result, the + * answer will be 0 seconds. + */ +- (NSUInteger)numberOfIdleTimeSecondsForXMPPLastActivity:(XMPPLastActivity *)sender queryIQ:(XMPPIQ *)iq currentIdleTimeSeconds:(NSUInteger)idleSeconds; + +@end diff --git a/Extensions/XEP-0012/XMPPLastActivity.m b/Extensions/XEP-0012/XMPPLastActivity.m new file mode 100644 index 0000000..a5acb82 --- /dev/null +++ b/Extensions/XEP-0012/XMPPLastActivity.m @@ -0,0 +1,222 @@ +// +// XMPPLastActivity.m +// XEP-0012 +// +// Created by Daniel Rodríguez Troitiño on 1/26/2013. +// +// + +#import "XMPPLastActivity.h" +#import "XMPPIDTracker.h" +#import "XMPPIQ+LastActivity.h" +#import "XMPPFramework.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +static const NSTimeInterval XMPPLastActivityDefaultTimeout = 30.0; + + +@interface XMPPLastActivity () + +@property (atomic, strong) XMPPIDTracker *queryTracker; + +@end + + +@implementation XMPPLastActivity + +@synthesize respondsToQueries = _respondsToQueries; +@synthesize queryTracker = _queryTracker; + +- (id)init +{ + return [self initWithDispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super initWithDispatchQueue:queue])) + { + _respondsToQueries = YES; + } + + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + if ([super activate:aXmppStream]) + { +#ifdef _XMPP_CAPABILITIES_H + [xmppStream autoAddDelegate:self delegateQueue:moduleQueue toModulesOfClass:[XMPPCapabilities class]]; +#endif + + _queryTracker = [[XMPPIDTracker alloc] initWithDispatchQueue:moduleQueue]; + + return YES; + } + + return NO; +} + +- (void)deactivate +{ +#ifdef _XMPP_CAPABILITIES_H + [xmppStream removeAutoDelegate:self delegateQueue:moduleQueue fromModulesOfClass:[XMPPCapabilities class]]; +#endif + + dispatch_block_t block = ^{ @autoreleasepool { + [_queryTracker removeAllIDs]; + _queryTracker = nil; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + [super deactivate]; +} + +- (BOOL)respondsToQueries +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return _respondsToQueries; + } + else + { + __block BOOL result; + + dispatch_sync(moduleQueue, ^{ + result = _respondsToQueries; + }); + + return result; + } +} + +- (void)setRespondsToQueries:(BOOL)respondsToQueries +{ + dispatch_block_t block = ^{ + if (_respondsToQueries != respondsToQueries) + { + _respondsToQueries = respondsToQueries; + +#ifdef _XMPP_CAPABILITIES_H + @autoreleasepool { + // Capabilities may have changed, need to notify others + [xmppStream resendMyPresence]; + } +#endif + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); +} + +- (NSString *)sendLastActivityQueryToJID:(XMPPJID *)jid +{ + return [self sendLastActivityQueryToJID:jid withTimeout:XMPPLastActivityDefaultTimeout]; +} + +- (NSString *)sendLastActivityQueryToJID:(XMPPJID *)jid withTimeout:(NSTimeInterval)timeout +{ + XMPPIQ *query = [XMPPIQ lastActivityQueryTo:jid]; + NSString *queryID = query.elementID; + + dispatch_async(moduleQueue, ^{ + __weak __typeof__(self) self_weak_ = self; + [_queryTracker addID:queryID block:^(XMPPIQ *iq, id info) { + __strong __typeof__(self) self = self_weak_; + if (iq) + { + [self delegateDidReceiveResponse:iq]; + } + else + { + [self delegateDidNotReceiveResponse:info.elementID dueToTimeout:info.timeout]; + } + } timeout:timeout]; + + [xmppStream sendElement:query]; + }); + + return queryID; +} + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + NSString *type = [iq type]; + + if (([type isEqualToString:@"result"] || [type isEqualToString:@"error"])) + { + return [_queryTracker invokeForID:iq.elementID withObject:iq]; + } + else if (_respondsToQueries && [type isEqualToString:@"get"] && [iq isLastActivityQuery]) + { + NSUInteger seconds = [self delegateNumberOfIdleTimeSecondsWithIQ:iq]; + XMPPIQ *response = [XMPPIQ lastActivityResponseToIQ:iq withSeconds:seconds]; + [sender sendElement:response]; + + return YES; + } + + return NO; +} + +- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error +{ + [_queryTracker removeAllIDs]; +} + +#ifdef _XMPP_CAPABILITIES_H +// If an XMPPCapabilities instance is used we want to advertise our support for last activity. +- (void)xmppCapabilities:(XMPPCapabilities *)sender collectingMyCapabilities:(NSXMLElement *)query +{ + if (_respondsToQueries) + { + NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"]; + [feature addAttributeWithName:@"var" stringValue:XMPPLastActivityNamespace]; + + [query addChild:feature]; + } +} +#endif + +- (void)delegateDidReceiveResponse:(XMPPIQ *)response +{ + [multicastDelegate xmppLastActivity:self didReceiveResponse:response]; +} + +- (void)delegateDidNotReceiveResponse:(NSString *)queryID dueToTimeout:(NSTimeInterval)timeout +{ + [multicastDelegate xmppLastActivity:self didNotReceiveResponse:queryID dueToTimeout:timeout]; +} + +- (NSUInteger)delegateNumberOfIdleTimeSecondsWithIQ:(XMPPIQ *)iq +{ + __block NSUInteger idleSeconds = NSNotFound; + + GCDMulticastDelegateEnumerator *delegateEnumerator = [multicastDelegate delegateEnumerator]; + + id delegate; + dispatch_queue_t delegateQueue; + SEL selector = @selector(numberOfIdleTimeSecondsForXMPPLastActivity:queryIQ:currentIdleTimeSeconds:); + + while ([delegateEnumerator getNextDelegate:&delegate delegateQueue:&delegateQueue forSelector:selector]) + { + dispatch_sync(delegateQueue, ^{ @autoreleasepool { + idleSeconds = [delegate numberOfIdleTimeSecondsForXMPPLastActivity:self queryIQ:iq currentIdleTimeSeconds:idleSeconds]; + }}); + } + + return idleSeconds == NSNotFound ? 0 : idleSeconds; +} + +@end diff --git a/Extensions/XEP-0016/XMPPPrivacy.h b/Extensions/XEP-0016/XMPPPrivacy.h new file mode 100644 index 0000000..e6c68c5 --- /dev/null +++ b/Extensions/XEP-0016/XMPPPrivacy.h @@ -0,0 +1,231 @@ +#import +#import "XMPPModule.h" + +#if TARGET_OS_IPHONE + #import "DDXML.h" +#endif + +#define _XMPP_PRIVACY_H + +@class XMPPIQ; + +extern NSString *const XMPPPrivacyErrorDomain; + +typedef enum XMPPPrivacyErrorCode +{ + XMPPPrivacyQueryTimeout, // No response from server + XMPPPrivacyDisconnect, // XMPP disconnection + +} XMPPPrivacyErrorCode; + + + +@interface XMPPPrivacy : XMPPModule +{ + BOOL autoRetrievePrivacyListNames; + BOOL autoRetrievePrivacyListItems; + BOOL autoClearPrivacyListInfo; + + NSMutableDictionary *privacyDict; + NSString *activeListName; + NSString *defaultListName; + + NSMutableDictionary *pendingQueries; +} + +/** + * Whether or not the module should automatically retrieve the privacy list names. + * If this property is enabled, then the list is automatically fetched when the client signs in. + * + * The default value is YES. +**/ +@property (readwrite, assign) BOOL autoRetrievePrivacyListNames; + +/** + * Whether or not the module should automatically retrieve the privacy list rules. + * If this property is enabled, then the rules for each privacy list are automatically fetched. + * + * In other words, if the privacy list names are fetched (either automatically, or via retrieveListNames), + * then the module will automatically fetch the associated rules. + * It will also update the set of rules if we receive a "privacy list push" + * from the server that another resource has changed one of the privacy lists. + * + * The default value is YES. +**/ +@property (readwrite, assign) BOOL autoRetrievePrivacyListItems; + +/** + * Whether the module should automatically clear the privacy list info when the client disconnects. + * + * As per the XEP, if there are multiple resources signed in for the user, + * and one resource makes changes to a privacy list, all other resources are "pushed" a notification. + * However, if our client is disconnected when another resource makes the changes, + * then the only way we can find out about the changes are to redownload the privacy lists. + * + * It is recommended to clear the privacy list to assure we have the correct info. + * However, there may be specific situations in which an xmpp client can be sure the privacy list won't change. + * + * The default value is YES. +**/ +@property (readwrite, assign) BOOL autoClearPrivacyListInfo; + +/** + * Manual fetch of list names and rules, and manual control over when to clear stored info. +**/ +- (void)retrieveListNames; +- (void)retrieveListWithName:(NSString *)privacyListName; +- (void)clearPrivacyListInfo; + +/** + * Returns the privacy list names. + * This is an array of strings. +**/ +- (NSArray *)listNames; + +/** + * Returns the privacy list rules/items for the given list name. + * + * The result is an array or privacy items (NSXMLElement's). + * The array is sorted according to order, where the item with the smallest 'order' is first in the array. +**/ +- (NSArray *)listWithName:(NSString *)privacyListName; + +/** + * Returns information about the active list. + * If there is no active list, the methods return nil. + * + * The activeList method is a convenience method for [modPriv listWithName:[modPriv activeListName]] +**/ +- (NSString *)activeListName; +- (NSArray *)activeList; + +/** + * Returns information about the default list. + * If there is no default list, the methods return nil. + * + * The defaultList method is a convenience method for [modPriv listWithName:[modPriv defaultListName]] +**/ +- (NSString *)defaultListName; +- (NSArray *)defaultList; + +/** + * Changes the client's active privacy list to the list with the given name. + * The privacy list name must match the name of an existing privacy list. + * + * To decline the use of an active list simply pass nil to this method. + * + * Once the server has acknowledged the change, + * the delegate method xmppPrivacy:didSetActiveListName: will be invoked. + * If the server is unable to process the change (e.g. invalid list name), + * the delegate method xmppPrivacy:didNotSetActiveListName:error: will be invoked. + * + * The methods activeListName and activeList will update after the server acknowledges the change. +**/ +- (void)setActiveListName:(NSString *)privacyListName; + +/** + * Changes the client's default privacy list to the list with the given name. + * The privacy list name must match the name of an existing privacy list. + * + * To decline the use of a default list simply pass nil to this method. + * + * Once the server has acknowledged the change, + * the delegate method xmppPrivacy:didSetDefaultListName: will be invoked. + * If the server is unable to process the change (e.g. invalid list name, in use by another resource), + * the delegate method xmppPrivacy:didNotSetDefaultListName:error: will be invoked. + * + * The methods defaultListName and defaultList will update after the server acknowledges the change. +**/ +- (void)setDefaultListName:(NSString *)privacyListName; + +/** + * Adds/Edits/Removes a privacy list with the given name. + * The given array should contain only privacy items (NSXMLElement's). + * + * To remove a privacy list, invoke this method will a nil or empty items parameter. +**/ +- (void)setListWithName:(NSString *)privacyListName items:(NSArray *)items; + +/** + * The following are convenience methods to create privacy item rules. + * A quick refresher from the XEP-0016 documentation is provided below. + * + * The 'type' attribute is OPTIONAL, and must be one of: jid|group|subscription + * + * If the 'type' is 'jid', then the 'value' must contain a valid JID. + * JIDs are to be matched in the following order: + * + * - (only that resource matches) + * - (any resource matches) + * - (only that resource matches) + * - (the domain itself matches, as does any user@domain or domain/resource) + * + * If the 'type' is 'group', then the 'value' should contain the name of a group in the user's roster. + * + * If the 'type' is 'subscription', then the 'value' must be one of: both|to|from|none + * + * The 'action' attribute is MANDATORY and must be one of: allow|deny + * + * The 'order' attribute is MANDATORY and must be a non-negative integer that is unique among all items in the list. + * List items are processed by the server according to the 'order' attribute in ascending order. (0 before 1, etc) + * Once the server matches a privacy item in the list, it obeys the item and ceases processing. + * + * The privacy item may contain one or more child elements that specify more granular blocking control: + * + * - (blocks incoming message stanzas) + * - (blocks incoming IQ stanzas) + * - (blocks incoming presence notifications) + * - (blocks outgoing presence notifications) +**/ ++ (NSXMLElement *)privacyItemWithAction:(NSString *)action order:(NSUInteger)order; ++ (NSXMLElement *)privacyItemWithType:(NSString *)type + value:(NSString *)value + action:(NSString *)action + order:(NSUInteger)order; + ++ (void)blockIQs:(NSXMLElement *)privacyItem; ++ (void)blockMessages:(NSXMLElement *)privacyItem; ++ (void)blockPresenceIn:(NSXMLElement *)privacyItem; ++ (void)blockPresenceOut:(NSXMLElement *)privacyItem; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPPrivacyDelegate +@optional + +/** + * The following delegate methods correspond almost exactly with the action methods of the class. + * There are a few possible ways in which an action could fail: + * + * 1. We receive an error response from the server. + * 2. We receive no response from the server, and the query times out. + * 3. We get disconnected before we receive the response. + * + * In case number 1, the error will be an XMPPIQ of type='error'. + * + * In case number 2 or 3, the error will be an NSError + * with domain=XMPPPrivacyErrorDomain and code from the XMPPPrivacyErrorCode enumeration. +**/ + +- (void)xmppPrivacy:(XMPPPrivacy *)sender didReceiveListNames:(NSArray *)listNames; +- (void)xmppPrivacy:(XMPPPrivacy *)sender didNotReceiveListNamesDueToError:(id)error; + +- (void)xmppPrivacy:(XMPPPrivacy *)sender didReceiveListWithName:(NSString *)name items:(NSArray *)items; +- (void)xmppPrivacy:(XMPPPrivacy *)sender didNotReceiveListWithName:(NSString *)name error:(id)error; + +- (void)xmppPrivacy:(XMPPPrivacy *)sender didReceivePushWithListName:(NSString *)name; + +- (void)xmppPrivacy:(XMPPPrivacy *)sender didSetActiveListName:(NSString *)name; +- (void)xmppPrivacy:(XMPPPrivacy *)sender didNotSetActiveListName:(NSString *)name error:(id)error; + +- (void)xmppPrivacy:(XMPPPrivacy *)sender didSetDefaultListName:(NSString *)name; +- (void)xmppPrivacy:(XMPPPrivacy *)sender didNotSetDefaultListName:(NSString *)name error:(id)error; + +- (void)xmppPrivacy:(XMPPPrivacy *)sender didSetListWithName:(NSString *)name; +- (void)xmppPrivacy:(XMPPPrivacy *)sender didNotSetListWithName:(NSString *)name error:(id)error; + +@end diff --git a/Extensions/XEP-0016/XMPPPrivacy.m b/Extensions/XEP-0016/XMPPPrivacy.m new file mode 100644 index 0000000..cdf6880 --- /dev/null +++ b/Extensions/XEP-0016/XMPPPrivacy.m @@ -0,0 +1,1001 @@ +#import "XMPP.h" +#import "XMPPLogging.h" +#import "XMPPPrivacy.h" +#import "NSNumber+XMPP.h" + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#ifdef DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_VERBOSE; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +#define QUERY_TIMEOUT 30.0 // NSTimeInterval (double) = seconds + +NSString *const XMPPPrivacyErrorDomain = @"XMPPPrivacyErrorDomain"; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +typedef enum XMPPPrivacyQueryInfoType { + FetchList, + FetchRules, + EditList, + SetActiveList, + SetDefaultList + +} XMPPPrivacyQueryInfoType; + +@interface XMPPPrivacyQueryInfo : NSObject +{ + XMPPPrivacyQueryInfoType type; + NSString *privacyListName; + NSArray *privacyListItems; + + dispatch_source_t timer; +} + +@property (nonatomic, readonly) XMPPPrivacyQueryInfoType type; +@property (nonatomic, readonly) NSString *privacyListName; +@property (nonatomic, readonly) NSArray *privacyListItems; + +@property (nonatomic, readwrite) dispatch_source_t timer; + +- (void)cancel; + ++ (XMPPPrivacyQueryInfo *)queryInfoWithType:(XMPPPrivacyQueryInfoType)type; ++ (XMPPPrivacyQueryInfo *)queryInfoWithType:(XMPPPrivacyQueryInfoType)type name:(NSString *)name; ++ (XMPPPrivacyQueryInfo *)queryInfoWithType:(XMPPPrivacyQueryInfoType)type name:(NSString *)name items:(NSArray *)items; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPPrivacy (/* Must be nameless for properties */) + +- (void)addQueryInfo:(XMPPPrivacyQueryInfo *)qi withKey:(NSString *)uuid; +- (void)queryTimeout:(NSString *)uuid; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPPrivacy + +- (id)init +{ + return [self initWithDispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super initWithDispatchQueue:queue])) + { + autoRetrievePrivacyListNames = YES; + autoRetrievePrivacyListItems = YES; + autoClearPrivacyListInfo = YES; + + privacyDict = [[NSMutableDictionary alloc] init]; + activeListName = nil; + defaultListName = nil; + + pendingQueries = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + if ([super activate:aXmppStream]) + { + // Reserved for possible future use. + + return YES; + } + + return NO; +} + +- (void)deactivate +{ + // Reserved for possible future use. + + [super deactivate]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)autoRetrievePrivacyListNames +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return autoRetrievePrivacyListNames; + } + else + { + __block BOOL result; + + dispatch_sync(moduleQueue, ^{ + result = autoRetrievePrivacyListNames; + }); + + return result; + } +} + +- (void)setAutoRetrievePrivacyListNames:(BOOL)flag +{ + dispatch_block_t block = ^{ + + autoRetrievePrivacyListNames = flag; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (BOOL)autoRetrievePrivacyListItems +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return autoRetrievePrivacyListItems; + } + else + { + __block BOOL result; + + dispatch_sync(moduleQueue, ^{ + result = autoRetrievePrivacyListItems; + }); + + return result; + } +} + +- (void)setAutoRetrievePrivacyListItems:(BOOL)flag +{ + dispatch_block_t block = ^{ + + autoRetrievePrivacyListItems = flag; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (BOOL)autoClearPrivacyListInfo +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return autoClearPrivacyListInfo; + } + else + { + __block BOOL result; + + dispatch_sync(moduleQueue, ^{ + result = autoClearPrivacyListInfo; + }); + + return result; + } +} + +- (void)setAutoClearPrivacyListInfo:(BOOL)flag +{ + dispatch_block_t block = ^{ + + autoClearPrivacyListInfo = flag; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Public API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)retrieveListNames +{ + XMPPLogTrace(); + + // + // + // + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:privacy"]; + + NSString *uuid = [xmppStream generateUUID]; + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:nil elementID:uuid child:query]; + + [xmppStream sendElement:iq]; + + XMPPPrivacyQueryInfo *qi = [XMPPPrivacyQueryInfo queryInfoWithType:FetchList]; + [self addQueryInfo:qi withKey:uuid]; +} + +- (void)retrieveListWithName:(NSString *)privacyListName +{ + XMPPLogTrace(); + + if (privacyListName == nil) return; + + // + // + // + // + // + + NSXMLElement *list = [NSXMLElement elementWithName:@"list"]; + [list addAttributeWithName:@"name" stringValue:privacyListName]; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:privacy"]; + [query addChild:list]; + + NSString *uuid = [xmppStream generateUUID]; + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:nil elementID:uuid child:query]; + + [xmppStream sendElement:iq]; + + XMPPPrivacyQueryInfo *qi = [XMPPPrivacyQueryInfo queryInfoWithType:FetchRules name:privacyListName]; + [self addQueryInfo:qi withKey:uuid]; +} + +- (void)clearPrivacyListInfo +{ + XMPPLogTrace(); + + if (dispatch_get_specific(moduleQueueTag)) + { + [privacyDict removeAllObjects]; + } + else + { + dispatch_async(moduleQueue, ^{ @autoreleasepool { + + [privacyDict removeAllObjects]; + }}); + } +} + +- (NSArray *)listNames +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return [privacyDict allKeys]; + } + else + { + __block NSArray *result; + + dispatch_sync(moduleQueue, ^{ @autoreleasepool { + + result = [[privacyDict allKeys] copy]; + }}); + + return result; + } +} + +- (NSArray *)listWithName:(NSString *)privacyListName +{ + NSArray* (^block)() = ^ NSArray* () { + + id result = privacyDict[privacyListName]; + + if (result == [NSNull null]) // Not fetched yet + return nil; + else + return (NSArray *)result; + }; + + // ExecuteVoidBlock(moduleQueue, block); + // ExecuteNonVoidBlock(moduleQueue, block, NSArray*) + + if (dispatch_get_specific(moduleQueueTag)) + { + return block(); + } + else + { + __block NSArray *result; + + dispatch_sync(moduleQueue, ^{ @autoreleasepool { + + result = block(); + }}); + + return result; + } +} + +- (NSString *)activeListName +{ + return activeListName; +} + +- (NSArray *)activeList +{ + return [self listWithName:activeListName]; +} + +- (void)setActiveListName:(NSString *)privacyListName +{ + // Setting active list: + // + // + // + // + // + // + // + // Decline the use of active lists: + // + // + // + // + // + // + + NSXMLElement *active = [NSXMLElement elementWithName:@"active"]; + if (privacyListName) + { + [active addAttributeWithName:@"name" stringValue:privacyListName]; + } + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:privacy"]; + [query addChild:active]; + + NSString *uuid = [xmppStream generateUUID]; + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:nil elementID:uuid child:query]; + + [xmppStream sendElement:iq]; + + XMPPPrivacyQueryInfo *qi = [XMPPPrivacyQueryInfo queryInfoWithType:SetActiveList name:privacyListName]; + [self addQueryInfo:qi withKey:uuid]; +} + +- (NSString *)defaultListName +{ + return defaultListName; +} + +- (NSArray *)defaultList +{ + return [self listWithName:defaultListName]; +} + +- (void)setDefaultListName:(NSString *)privacyListName +{ + // Setting default list: + // + // + // + // + // + // + // + // Decline the use of default list: + // + // + // + // + // + // + + NSXMLElement *dfault = [NSXMLElement elementWithName:@"default"]; + if (privacyListName) + { + [dfault addAttributeWithName:@"name" stringValue:privacyListName]; + } + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:privacy"]; + [query addChild:dfault]; + + NSString *uuid = [xmppStream generateUUID]; + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:nil elementID:uuid child:query]; + + [xmppStream sendElement:iq]; + + XMPPPrivacyQueryInfo *qi = [XMPPPrivacyQueryInfo queryInfoWithType:SetDefaultList name:privacyListName]; + [self addQueryInfo:qi withKey:uuid]; +} + +- (void)setListWithName:(NSString *)privacyListName items:(NSArray *)items +{ + // Edit a privacy list: + // + // + // + // + // + // + // + // + // + // + // + // + // Remove a privacy list: + // + // + // + // + // + // + + NSXMLElement *list = [NSXMLElement elementWithName:@"list"]; + [list addAttributeWithName:@"name" stringValue:privacyListName]; + + if (items && ([items count] > 0)) + { + [list setChildren:items]; + } + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:privacy"]; + [query addChild:list]; + + NSString *uuid = [xmppStream generateUUID]; + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:nil elementID:uuid child:query]; + + [xmppStream sendElement:iq]; + + XMPPPrivacyQueryInfo *qi = [XMPPPrivacyQueryInfo queryInfoWithType:EditList name:privacyListName items:items]; + [self addQueryInfo:qi withKey:uuid]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Query Processing +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)addQueryInfo:(XMPPPrivacyQueryInfo *)queryInfo withKey:(NSString *)uuid +{ + // Setup timer + + dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue); + + dispatch_source_set_event_handler(timer, ^{ @autoreleasepool { + + [self queryTimeout:uuid]; + }}); + + dispatch_time_t fireTime = dispatch_time(DISPATCH_TIME_NOW, (QUERY_TIMEOUT * NSEC_PER_SEC)); + + dispatch_source_set_timer(timer, fireTime, DISPATCH_TIME_FOREVER, 1.0); + dispatch_resume(timer); + + queryInfo.timer = timer; + + // Add to dictionary + pendingQueries[uuid] = queryInfo; +} + +- (void)removeQueryInfo:(XMPPPrivacyQueryInfo *)queryInfo withKey:(NSString *)uuid +{ + // Invalidate timer + [queryInfo cancel]; + + // Remove from dictionary + [pendingQueries removeObjectForKey:uuid]; +} + +- (void)processQuery:(XMPPPrivacyQueryInfo *)queryInfo withFailureCode:(XMPPPrivacyErrorCode)errorCode +{ + NSError *error = [NSError errorWithDomain:XMPPPrivacyErrorDomain code:errorCode userInfo:nil]; + + if (queryInfo.type == FetchList) + { + [multicastDelegate xmppPrivacy:self didNotReceiveListNamesDueToError:error]; + } + else if (queryInfo.type == FetchRules) + { + [multicastDelegate xmppPrivacy:self didNotReceiveListWithName:queryInfo.privacyListName error:error]; + } + else if (queryInfo.type == EditList) + { + [multicastDelegate xmppPrivacy:self didNotSetListWithName:queryInfo.privacyListName error:error]; + } + else if (queryInfo.type == SetActiveList) + { + [multicastDelegate xmppPrivacy:self didNotSetActiveListName:queryInfo.privacyListName error:error]; + } + else if (queryInfo.type == SetDefaultList) + { + [multicastDelegate xmppPrivacy:self didNotSetDefaultListName:queryInfo.privacyListName error:error]; + } +} + +- (void)queryTimeout:(NSString *)uuid +{ + XMPPPrivacyQueryInfo *queryInfo = privacyDict[uuid]; + if (queryInfo) + { + [self processQuery:queryInfo withFailureCode:XMPPPrivacyQueryTimeout]; + [self removeQueryInfo:queryInfo withKey:uuid]; + } +} + +NSInteger sortItems(id itemOne, id itemTwo, void *context) +{ + NSXMLElement *item1 = (NSXMLElement *)itemOne; + NSXMLElement *item2 = (NSXMLElement *)itemTwo; + + NSString *orderStr1 = [item1 attributeStringValueForName:@"order"]; + NSString *orderStr2 = [item2 attributeStringValueForName:@"order"]; + + NSUInteger order1; + BOOL parse1 = [NSNumber xmpp_parseString:orderStr1 intoNSUInteger:&order1]; + + NSUInteger order2; + BOOL parse2 = [NSNumber xmpp_parseString:orderStr2 intoNSUInteger:&order2]; + + if (parse1) + { + if (parse2) + { + // item1 = valid + // item2 = valid + + if (order1 < order2) + return NSOrderedAscending; + if (order1 > order2) + return NSOrderedDescending; + + return NSOrderedSame; + } + else + { + // item1 = valid + // item2 = invalid + + return NSOrderedAscending; + } + } + else if (parse2) + { + // item1 = invalid + // item2 = valid + + return NSOrderedDescending; + } + else + { + // item1 = invalid + // item2 = invalid + + return NSOrderedSame; + } +} + +- (void)processQueryResponse:(XMPPIQ *)iq withInfo:(XMPPPrivacyQueryInfo *)queryInfo +{ + if (queryInfo.type == FetchList) + { + // Privacy List Names Query Response: + // + // + // + // + // + // + // + // + // + // + + if ([[iq type] isEqualToString:@"result"]) + { + NSXMLElement *query = [iq elementForName:@"query" xmlns:@"jabber:iq:privacy"]; + if (query == nil) return; + + NSXMLElement *active = [query elementForName:@"active"]; + activeListName = [[active attributeStringValueForName:@"name"] copy]; + + NSXMLElement *dfault = [query elementForName:@"default"]; + defaultListName = [[dfault attributeStringValueForName:@"name"] copy]; + + NSArray *listNames = [query elementsForName:@"list"]; + for (NSXMLElement *listName in listNames) + { + NSString *name = [listName attributeStringValueForName:@"name"]; + if (name) + { + id value = privacyDict[name]; + if (value == nil) + { + privacyDict[name] = [NSNull null]; + } + } + } + + [multicastDelegate xmppPrivacy:self didReceiveListNames:[self listNames]]; + } + else + { + [multicastDelegate xmppPrivacy:self didNotReceiveListNamesDueToError:iq]; + } + } + else if (queryInfo.type == FetchRules) + { + // Privacy List Rules Query Response (success): + // + // + // + // + // + // + // + // + // + // + // + // Privacy List Rules Query Response (error): + // + // + // + // + // + // + // + // + // + + if ([[iq type] isEqualToString:@"result"]) + { + NSXMLElement *query = [iq elementForName:@"query" xmlns:@"jabber:iq:privacy"]; + if (query == nil) return; + + NSXMLElement *list = [query elementForName:@"list"]; + if (list == nil) return; + + NSArray *items = [[list elementsForName:@"item"] sortedArrayUsingFunction:sortItems context:NULL]; + + if (items == nil) + { + items = [NSArray array]; + } + + privacyDict[queryInfo.privacyListName] = items; + + [multicastDelegate xmppPrivacy:self didReceiveListWithName:queryInfo.privacyListName items:items]; + } + else + { + [multicastDelegate xmppPrivacy:self didNotReceiveListWithName:queryInfo.privacyListName error:iq]; + } + } + else if (queryInfo.type == EditList) + { + // Privacy List Add/Edit/Remove Response (success): + // + // + // + // Note: The result iq does NOT have a query child. + + if ([[iq type] isEqualToString:@"result"]) + { + NSArray *items = [[queryInfo privacyListItems] sortedArrayUsingFunction:sortItems context:NULL]; + + if (items == nil) + { + items = [NSArray array]; + } + + privacyDict[queryInfo.privacyListName] = items; + + [multicastDelegate xmppPrivacy:self didSetListWithName:queryInfo.privacyListName]; + } + else + { + [multicastDelegate xmppPrivacy:self didNotSetListWithName:queryInfo.privacyListName error:iq]; + } + } + else if (queryInfo.type == SetActiveList) + { + // Change of active list (success): + // + // + // + // + // Change of active list (error): + // + // + // + // + // + // + // + // + // + // + // + // Note: The result iq does NOT have a query child. + // Note: The success could also mean we declined the use of an active list. + + if ([[iq type] isEqualToString:@"result"]) + { + activeListName = [[queryInfo privacyListName] copy]; + + [multicastDelegate xmppPrivacy:self didSetActiveListName:queryInfo.privacyListName]; + } + else + { + [multicastDelegate xmppPrivacy:self didNotSetActiveListName:queryInfo.privacyListName error:iq]; + } + } + else if (queryInfo.type == SetDefaultList) + { + // Change of default list (success): + // + // + // + // + // Change of default list (error): + // + // + // + // + // + // + // + // + // + // + // + // Note: The result iq does NOT have a query child. + // Note: The success could also mean we declined the use of a default list. + + if ([[iq type] isEqualToString:@"result"]) + { + defaultListName = [[queryInfo privacyListName] copy]; + + [multicastDelegate xmppPrivacy:self didSetDefaultListName:queryInfo.privacyListName]; + } + else + { + [multicastDelegate xmppPrivacy:self didNotSetDefaultListName:queryInfo.privacyListName error:iq]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender +{ + if (self.autoRetrievePrivacyListNames) + { + [self retrieveListNames]; + } +} + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + NSString *type = [iq type]; + + if ([type isEqualToString:@"set"]) + { + NSXMLElement *query = [iq elementForName:@"query" xmlns:@"jabber:iq:privacy"]; + if (query) + { + // Privacy List Push: + // + // + // + // + // + // + // + // + // Push response: + // + // + + NSXMLElement *list = [query elementForName:@"list"]; + + NSString *listName = [list attributeStringValueForName:@"name"]; + if (listName == nil) + { + return NO; + } + + [multicastDelegate xmppPrivacy:self didReceivePushWithListName:listName]; + + XMPPIQ *iqResponse = [XMPPIQ iqWithType:@"result" to:[iq from] elementID:[iq elementID]]; + [xmppStream sendElement:iqResponse]; + + if (self.autoRetrievePrivacyListItems) + { + [self retrieveListWithName:listName]; + } + + return YES; + } + } + else + { + // This may be a response to a query we sent + + XMPPPrivacyQueryInfo *queryInfo = pendingQueries[[iq elementID]]; + if (queryInfo) + { + [self processQueryResponse:iq withInfo:queryInfo]; + + if (queryInfo.type == FetchList && self.autoRetrievePrivacyListItems) + { + for (NSString *privacyListName in privacyDict) + { + id privacyListItems = privacyDict[privacyListName]; + + if (privacyListItems == [NSNull null]) + { + [self retrieveListWithName:privacyListName]; + } + } + } + + return YES; + } + } + + return NO; +} + +-(void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error +{ + // If there are any pending queries, + // they just failed due to the disconnection. + + for (NSString *uuid in pendingQueries) + { + XMPPPrivacyQueryInfo *queryInfo = privacyDict[uuid]; + + [self processQuery:queryInfo withFailureCode:XMPPPrivacyDisconnect]; + } + + // Clear the list of pending queries + + [pendingQueries removeAllObjects]; + + // Maybe clear all stored privacy info + + if (self.autoClearPrivacyListInfo) + { + [self clearPrivacyListInfo]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Privacy Items +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (NSXMLElement *)privacyItemWithAction:(NSString *)action order:(NSUInteger)orderValue +{ + return [self privacyItemWithType:nil value:nil action:action order:orderValue]; +} + ++ (NSXMLElement *)privacyItemWithType:(NSString *)type + value:(NSString *)value + action:(NSString *)action + order:(NSUInteger)orderValue +{ + NSString *order = [[NSString alloc] initWithFormat:@"%lu", (unsigned long)orderValue]; + + // + // [] + // [] + // [] + // [] + // + + NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; + + if (type) + [item addAttributeWithName:@"type" stringValue:type]; + + if (value) + [item addAttributeWithName:@"value" stringValue:value]; + + [item addAttributeWithName:@"action" stringValue:action]; + [item addAttributeWithName:@"order" stringValue:order]; + + return item; +} + ++ (void)blockIQs:(NSXMLElement *)privacyItem +{ + [privacyItem addChild:[NSXMLElement elementWithName:@"iq"]]; +} + ++ (void)blockMessages:(NSXMLElement *)privacyItem +{ + [privacyItem addChild:[NSXMLElement elementWithName:@"message"]]; +} + ++ (void)blockPresenceIn:(NSXMLElement *)privacyItem +{ + [privacyItem addChild:[NSXMLElement elementWithName:@"presence-in"]]; +} + ++ (void)blockPresenceOut:(NSXMLElement *)privacyItem +{ + [privacyItem addChild:[NSXMLElement elementWithName:@"presence-out"]]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPPrivacyQueryInfo + +@synthesize type; +@synthesize privacyListName; +@synthesize privacyListItems; +@synthesize timer; + + +- (id)initWithType:(XMPPPrivacyQueryInfoType)aType name:(NSString *)name items:(NSArray *)items +{ + if ((self = [super init])) + { + type = aType; + privacyListName = [name copy]; + privacyListItems = [items copy]; + } + return self; +} + +- (void)cancel +{ + if (timer) + { + dispatch_source_cancel(timer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(timer); + #endif + timer = NULL; + } +} + +- (void)dealloc +{ + [self cancel]; +} + ++ (XMPPPrivacyQueryInfo *)queryInfoWithType:(XMPPPrivacyQueryInfoType)type +{ + return [self queryInfoWithType:type name:nil items:nil]; +} + ++ (XMPPPrivacyQueryInfo *)queryInfoWithType:(XMPPPrivacyQueryInfoType)type name:(NSString *)name +{ + return [self queryInfoWithType:type name:name items:nil]; +} + ++ (XMPPPrivacyQueryInfo *)queryInfoWithType:(XMPPPrivacyQueryInfoType)type name:(NSString *)name items:(NSArray *)items +{ + return [[XMPPPrivacyQueryInfo alloc] initWithType:type name:name items:items]; +} + +@end diff --git a/Extensions/XEP-0045/CoreDataStorage/XMPPRoom.xcdatamodeld/.xccurrentversion b/Extensions/XEP-0045/CoreDataStorage/XMPPRoom.xcdatamodeld/.xccurrentversion new file mode 100644 index 0000000..723fb60 --- /dev/null +++ b/Extensions/XEP-0045/CoreDataStorage/XMPPRoom.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + XMPPRoom.xcdatamodel + + diff --git a/Extensions/XEP-0045/CoreDataStorage/XMPPRoom.xcdatamodeld/XMPPRoom.xcdatamodel/contents b/Extensions/XEP-0045/CoreDataStorage/XMPPRoom.xcdatamodeld/XMPPRoom.xcdatamodel/contents new file mode 100644 index 0000000..aae0819 --- /dev/null +++ b/Extensions/XEP-0045/CoreDataStorage/XMPPRoom.xcdatamodeld/XMPPRoom.xcdatamodel/contents @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Extensions/XEP-0045/CoreDataStorage/XMPPRoomCoreDataStorage.h b/Extensions/XEP-0045/CoreDataStorage/XMPPRoomCoreDataStorage.h new file mode 100644 index 0000000..b3195f0 --- /dev/null +++ b/Extensions/XEP-0045/CoreDataStorage/XMPPRoomCoreDataStorage.h @@ -0,0 +1,125 @@ +#import +#import + +#import "XMPP.h" +#import "XMPPRoom.h" +#import "XMPPRoomMessageCoreDataStorageObject.h" +#import "XMPPRoomOccupantCoreDataStorageObject.h" +#import "XMPPCoreDataStorage.h" + + +@interface XMPPRoomCoreDataStorage : XMPPCoreDataStorage + +/** + * Convenience method to get an instance with the default database name. + * + * IMPORTANT: + * You are NOT required to use the sharedInstance. + * + * If your application makes extensive use of MUC, and you use a sharedInstance of this class, + * then all of your MUC rooms share the same database store. You might get better performance if you create + * multiple instances of this class instead (using different database filenames), as this way you can have + * concurrent writes to multiple databases. +**/ ++ (instancetype)sharedInstance; + + +/* Inherited from XMPPCoreDataStorage + * Please see the XMPPCoreDataStorage header file for extensive documentation. + +- (id)initWithDatabaseFilename:(NSString *)databaseFileName storeOptions:(NSDictionary *)storeOptions; +- (id)initWithInMemoryStore; + +@property (readonly) NSString *databaseFileName; + +@property (readwrite) NSUInteger saveThreshold; + +@property (readonly) NSManagedObjectModel *managedObjectModel; +@property (readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator; + +*/ + +/** + * You may choose to extend this class, and/or the message/occupant classes for customized functionality. + * These properties allow for such customization. + * + * You must set your desired entity names, if different from default, before you begin using the storage class. +**/ +@property (strong, readwrite) NSString * messageEntityName; +@property (strong, readwrite) NSString * occupantEntityName; + +/** + * It is likely you don't want the message history to persist forever. + * Doing so would allow the database to grow infinitely large over time. + * + * The maxMessageAge property provides a way to specify how old a message can get + * before it should get deleted from the database. + * + * The deleteInterval specifies how often to sweep for old messages. + * Since deleting is an expensive operation (disk io) it is done on a fixed interval. + * + * You can optionally disable the maxMessageAge by setting it to zero (or a negative value). + * If you disable the maxMessageAge then old messages are not deleted. + * + * You can optionally disable the deleteInterval by setting it to zero (or a negative value). + * + * The default maxAge is 7 days. + * The default deleteInterval is 5 minutes. +**/ +@property (assign, readwrite) NSTimeInterval maxMessageAge; +@property (assign, readwrite) NSTimeInterval deleteInterval; + +/** + * You may optionally prevent old message deletion for particular rooms. +**/ +- (void)pauseOldMessageDeletionForRoom:(XMPPJID *)roomJID; +- (void)resumeOldMessageDeletionForRoom:(XMPPJID *)roomJID; + +/** + * Convenience method to get the message/occupant entity description. + * + * @see messageEntityName + * @see occupantEntityName +**/ +- (NSEntityDescription *)messageEntity:(NSManagedObjectContext *)moc; +- (NSEntityDescription *)occupantEntity:(NSManagedObjectContext *)moc; + +/** + * Returns the timestamp of the most recent message stored in the database for the given room. + * This may be used when requesting the message history from the server, + * to prevent redownloading messages you already have. + * + * @param roomJID - The JID of the room (a bare JID) + * + * @param xmppStream - This class can support multiple concurrent xmppStreams. + * Optionally pass the xmppStream the room applies to. + * If you're using this claass with a single xmppStream, you can pass nil. + * + * @param moc - The managedObjectContext to use when doing the lookups. + * If non-nil, this should match the thread you're currently using. + * If nil, the operation is dispatch_sync'd onto the internal queue, + * and uses the internal managedObjectContext. + * + * The moc may optionally be nil strictly because this method does not return a NSManagedObject. +**/ +- (NSDate *)mostRecentMessageTimestampForRoom:(XMPPJID *)roomJID + stream:(XMPPStream *)xmppStream + inContext:(NSManagedObjectContext *)moc; + +/** + * Returns the occupant for the given full jid. + * + * @param jid - The full jid of the room occupant (including resource). + * + * @param xmppStream - This class can support multiple concurrent xmppStreams. + * Optionally pass the xmppStream the room applies to. + * If you're using this claass with a single xmppStream, you can pass nil. + * + * @param moc - The managedObjectContext to use when doing the lookups. + * This must not be nil, and should match the thread you're currently using. +**/ +- (XMPPRoomOccupantCoreDataStorageObject *)occupantForJID:(XMPPJID *)jid + stream:(XMPPStream *)xmppStream + inContext:(NSManagedObjectContext *)moc; + +@end diff --git a/Extensions/XEP-0045/CoreDataStorage/XMPPRoomCoreDataStorage.m b/Extensions/XEP-0045/CoreDataStorage/XMPPRoomCoreDataStorage.m new file mode 100644 index 0000000..b69b379 --- /dev/null +++ b/Extensions/XEP-0045/CoreDataStorage/XMPPRoomCoreDataStorage.m @@ -0,0 +1,999 @@ +#import "XMPPRoomCoreDataStorage.h" +#import "XMPPCoreDataStorageProtected.h" +#import "NSXMLElement+XEP_0203.h" +#import "XMPPLogging.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +#define AssertPrivateQueue() \ + NSAssert(dispatch_get_specific(storageQueueTag), @"Private method: MUST run on storageQueue"); + +@interface XMPPRoomCoreDataStorage () +{ + /* Inherited from XMPPCoreDataStorage + + NSString *databaseFileName; + NSUInteger saveThreshold; + + dispatch_queue_t storageQueue; + + */ + + NSString *messageEntityName; + NSString *occupantEntityName; + + NSTimeInterval maxMessageAge; + NSTimeInterval deleteInterval; + + NSMutableSet *pausedMessageDeletion; + + dispatch_time_t lastDeleteTime; + dispatch_source_t deleteTimer; +} + +- (NSEntityDescription *)messageEntity:(NSManagedObjectContext *)moc; +- (NSEntityDescription *)occupantEntity:(NSManagedObjectContext *)moc; + +- (void)performDelete; +- (void)destroyDeleteTimer; +- (void)updateDeleteTimer; +- (void)createAndStartDeleteTimer; + +- (void)clearAllOccupantsFromRoom:(XMPPJID *)roomJID; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPRoomCoreDataStorage + +static XMPPRoomCoreDataStorage *sharedInstance; + ++ (instancetype)sharedInstance +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + sharedInstance = [[XMPPRoomCoreDataStorage alloc] initWithDatabaseFilename:nil storeOptions:nil]; + }); + + return sharedInstance; +} + +- (void)commonInit +{ + XMPPLogTrace(); + [super commonInit]; + + // This method is invoked by all public init methods of the superclass + + messageEntityName = NSStringFromClass([XMPPRoomMessageCoreDataStorageObject class]); + occupantEntityName = NSStringFromClass([XMPPRoomOccupantCoreDataStorageObject class]); + + maxMessageAge = (60 * 60 * 24 * 7); // 7 days + deleteInterval = (60 * 5); // 5 days + + pausedMessageDeletion = [[NSMutableSet alloc] init]; +} + +- (void)dealloc +{ + [self destroyDeleteTimer]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSString *)messageEntityName +{ + __block NSString *result = nil; + + dispatch_block_t block = ^{ + result = messageEntityName; + }; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_sync(storageQueue, block); + + return result; +} + +- (void)setMessageEntityName:(NSString *)newMessageEntityName +{ + dispatch_block_t block = ^{ + messageEntityName = newMessageEntityName; + }; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_async(storageQueue, block); +} + +- (NSString *)occupantEntityName +{ + __block NSString *result = nil; + + dispatch_block_t block = ^{ + result = occupantEntityName; + }; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_sync(storageQueue, block); + + return result; +} + +- (void)setOccupantEntityName:(NSString *)newOccupantEntityName +{ + dispatch_block_t block = ^{ + occupantEntityName = newOccupantEntityName; + }; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_async(storageQueue, block); +} + +- (NSTimeInterval)maxMessageAge +{ + __block NSTimeInterval result = 0; + + dispatch_block_t block = ^{ + result = maxMessageAge; + }; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_sync(storageQueue, block); + + return result; +} + +- (void)setMaxMessageAge:(NSTimeInterval)age +{ + dispatch_block_t block = ^{ @autoreleasepool { + + NSTimeInterval oldMaxMessageAge = maxMessageAge; + NSTimeInterval newMaxMessageAge = age; + + maxMessageAge = age; + + // There are several cases we need to handle here. + // + // 1. If the maxAge was previously enabled and it just got disabled, + // then we need to stop the deleteTimer. (And we might as well release it.) + // + // 2. If the maxAge was previously disabled and it just got enabled, + // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) + // + // 3. If the maxAge was increased, + // then we don't need to do anything. + // + // 4. If the maxAge was decreased, + // then we should do an immediate delete. + + BOOL shouldDeleteNow = NO; + + if (oldMaxMessageAge > 0.0) + { + if (newMaxMessageAge <= 0.0) + { + // Handles #1 + [self destroyDeleteTimer]; + } + else if (oldMaxMessageAge > newMaxMessageAge) + { + // Handles #4 + shouldDeleteNow = YES; + } + else + { + // Handles #3 + // Nothing to do now + } + } + else if (newMaxMessageAge > 0.0) + { + // Handles #2 + shouldDeleteNow = YES; + } + + if (shouldDeleteNow) + { + [self performDelete]; + + if (deleteTimer) + [self updateDeleteTimer]; + else + [self createAndStartDeleteTimer]; + } + }}; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_async(storageQueue, block); +} + +- (NSTimeInterval)deleteInterval +{ + __block NSTimeInterval result = 0; + + dispatch_block_t block = ^{ + result = deleteInterval; + }; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_sync(storageQueue, block); + + return result; +} + +- (void)setDeleteInterval:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ @autoreleasepool { + + deleteInterval = interval; + + // There are several cases we need to handle here. + // + // 1. If the deleteInterval was previously enabled and it just got disabled, + // then we need to stop the deleteTimer. (And we might as well release it.) + // + // 2. If the deleteInterval was previously disabled and it just got enabled, + // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) + // + // 3. If the deleteInterval increased, then we need to reset the timer so that it fires at the later date. + // + // 4. If the deleteInterval decreased, then we need to reset the timer so that it fires at an earlier date. + // (Plus we might need to do an immediate delete.) + + if (deleteInterval > 0.0) + { + if (deleteTimer == NULL) + { + // Handles #2 + // + // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, + // if a delete is needed the timer will fire immediately. + + [self createAndStartDeleteTimer]; + } + else + { + // Handles #3 + // Handles #4 + // + // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self updateDeleteTimer]; + } + } + else if (deleteTimer) + { + // Handles #1 + + [self destroyDeleteTimer]; + } + }}; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_async(storageQueue, block); +} + +- (void)pauseOldMessageDeletionForRoom:(XMPPJID *)roomJID +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [pausedMessageDeletion addObject:[roomJID bareJID]]; + }}; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_async(storageQueue, block); +} + +- (void)resumeOldMessageDeletionForRoom:(XMPPJID *)roomJID +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [pausedMessageDeletion removeObject:[roomJID bareJID]]; + [self performDelete]; + }}; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_async(storageQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Overrides +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)didCreateManagedObjectContext +{ + XMPPLogTrace(); + + [self clearAllOccupantsFromRoom:nil]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Internal API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)performDelete +{ + if (maxMessageAge <= 0.0) return; + + NSDate *minLocalTimestamp = [NSDate dateWithTimeIntervalSinceNow:(maxMessageAge * -1.0)]; + + NSManagedObjectContext *moc = [self managedObjectContext]; + NSEntityDescription *messageEntity = [self messageEntity:moc]; + + NSPredicate *predicate; + if ([pausedMessageDeletion count] > 0) + { + predicate = [NSPredicate predicateWithFormat:@"localTimestamp <= %@ AND NOT roomJIDStr IN %@", + minLocalTimestamp, pausedMessageDeletion]; + } + else + { + predicate = [NSPredicate predicateWithFormat:@"localTimestamp <= %@", minLocalTimestamp]; + } + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:messageEntity]; + [fetchRequest setPredicate:predicate]; + [fetchRequest setFetchBatchSize:saveThreshold]; + + NSError *error = nil; + NSArray *oldMessages = [moc executeFetchRequest:fetchRequest error:&error]; + + if (error) + { + XMPPLogWarn(@"%@: %@ - fetch error: %@", THIS_FILE, THIS_METHOD, error); + } + + NSUInteger unsavedCount = [self numberOfUnsavedChanges]; + + for (XMPPRoomMessageCoreDataStorageObject *oldMessage in oldMessages) + { + [moc deleteObject:oldMessage]; + + if (++unsavedCount >= saveThreshold) + { + [self save]; + unsavedCount = 0; + } + } + + lastDeleteTime = dispatch_time(DISPATCH_TIME_NOW, 0); +} + +- (void)destroyDeleteTimer +{ + if (deleteTimer) + { + dispatch_source_cancel(deleteTimer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(deleteTimer); + #endif + deleteTimer = NULL; + } +} + +- (void)updateDeleteTimer +{ + if ((deleteTimer != NULL) && (deleteInterval > 0.0) && (maxMessageAge > 0.0)) + { + uint64_t interval = deleteInterval * NSEC_PER_SEC; + dispatch_time_t startTime; + + if (lastDeleteTime > 0) + startTime = dispatch_time(lastDeleteTime, interval); + else + startTime = dispatch_time(DISPATCH_TIME_NOW, interval); + + dispatch_source_set_timer(deleteTimer, startTime, interval, 1.0); + } +} + +- (void)createAndStartDeleteTimer +{ + if ((deleteTimer == NULL) && (deleteInterval > 0.0) && (maxMessageAge > 0.0)) + { + deleteTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, storageQueue); + + dispatch_source_set_event_handler(deleteTimer, ^{ @autoreleasepool { + + [self performDelete]; + + }}); + + [self updateDeleteTimer]; + + if(deleteTimer != NULL) + { + dispatch_resume(deleteTimer); + } + } +} + +- (void)clearAllOccupantsFromRoom:(XMPPJID *)roomJID +{ + XMPPLogTrace(); + AssertPrivateQueue(); + + NSManagedObjectContext *moc = [self managedObjectContext]; + NSEntityDescription *entity = [self occupantEntity:moc]; + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:entity]; + [fetchRequest setFetchBatchSize:saveThreshold]; + + if (roomJID) + { + NSPredicate *predicate; + predicate = [NSPredicate predicateWithFormat:@"roomJIDStr == %@", [roomJID bare]]; + + [fetchRequest setPredicate:predicate]; + } + + NSArray *allOccupants = [moc executeFetchRequest:fetchRequest error:nil]; + + NSUInteger unsavedCount = [self numberOfUnsavedChanges]; + + for (XMPPRoomOccupantCoreDataStorageObject *occupant in allOccupants) + { + [moc deleteObject:occupant]; + + if (++unsavedCount >= saveThreshold) + { + [self save]; + unsavedCount = 0; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Protected API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Optional override hook. +**/ +- (BOOL)existsMessage:(XMPPMessage *)message forRoom:(XMPPRoom *)room stream:(XMPPStream *)xmppStream +{ + NSDate *remoteTimestamp = [message delayedDeliveryDate]; + + if (remoteTimestamp == nil) + { + // When the xmpp server sends us a room message, it will always timestamp delayed messages. + // For example, when retrieving the discussion history, all messages will include the original timestamp. + // If a message doesn't include such timestamp, then we know we're getting it in "real time". + + return NO; + } + + // Does this message already exist in the database? + // How can we tell if two XMPPRoomMessages are the same? + // + // 1. Same streamBareJidStr + // 2. Same jid + // 3. Same text + // 4. Approximately the same timestamps + // + // This is actually a rather difficult question. + // What if the same user sends the exact same message multiple times? + // + // If we first received the message while already in the room, it won't contain a remoteTimestamp. + // Returning to the room later and downloading the discussion history will return the same message, + // this time with a remote timestamp. + // + // So if the message doesn't have a remoteTimestamp, + // but it's localTimestamp is approximately the same as the remoteTimestamp, + // then this is enough evidence to consider the messages the same. + // + // Note: Predicate order matters. Most unique key should be first, least unique should be last. + + NSManagedObjectContext *moc = [self managedObjectContext]; + NSEntityDescription *messageEntity = [self messageEntity:moc]; + + NSString *streamBareJidStr = [[self myJIDForXMPPStream:xmppStream] bare]; + + XMPPJID *messageJID = [message from]; + NSString *messageBody = [[message elementForName:@"body"] stringValue]; + + NSDate *minLocalTimestamp = [remoteTimestamp dateByAddingTimeInterval:-60]; + NSDate *maxLocalTimestamp = [remoteTimestamp dateByAddingTimeInterval: 60]; + + NSString *predicateFormat = @" body == %@ " + @"AND jidStr == %@ " + @"AND streamBareJidStr == %@ " + @"AND " + @"(" + @" (remoteTimestamp == %@) " + @" OR (remoteTimestamp == NIL && localTimestamp BETWEEN {%@, %@})" + @")"; + + NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateFormat, + messageBody, messageJID, streamBareJidStr, + remoteTimestamp, minLocalTimestamp, maxLocalTimestamp]; + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:messageEntity]; + [fetchRequest setPredicate:predicate]; + [fetchRequest setFetchLimit:1]; + + NSError *error = nil; + NSArray *results = [moc executeFetchRequest:fetchRequest error:&error]; + + if (error) + { + XMPPLogError(@"%@: %@ - Fetch error: %@", THIS_FILE, THIS_METHOD, error); + } + + return ([results count] > 0); +} + +/** + * Optional override hook for general extensions. + * + * @see insertMessage:outgoing:forRoom:stream: +**/ +- (void)didInsertMessage:(XMPPRoomMessageCoreDataStorageObject *)message +{ + // Override me if you're extending the XMPPRoomMessageCoreDataStorageObject class to add additional properties. + // You can update your additional properties here. + // + // At this point the standard properties have already been set. + // So you can, for example, access the XMPPMessage via message.message. +} + +/** + * Optional override hook for complete customization. + * Override me if you need to do specific custom work when inserting a message in a room. + * + * @see didInsertMessage: +**/ +- (void)insertMessage:(XMPPMessage *)message + outgoing:(BOOL)isOutgoing + forRoom:(XMPPRoom *)room + stream:(XMPPStream *)xmppStream +{ + // Extract needed information + + XMPPJID *roomJID = room.roomJID; + XMPPJID *messageJID = isOutgoing ? room.myRoomJID : [message from]; + + NSDate *localTimestamp; + NSDate *remoteTimestamp; + + if (isOutgoing) + { + localTimestamp = [[NSDate alloc] init]; + remoteTimestamp = nil; + } + else + { + remoteTimestamp = [message delayedDeliveryDate]; + if (remoteTimestamp) { + localTimestamp = remoteTimestamp; + } + else { + localTimestamp = [[NSDate alloc] init]; + } + } + + NSString *messageBody = [[message elementForName:@"body"] stringValue]; + + NSManagedObjectContext *moc = [self managedObjectContext]; + NSString *streamBareJidStr = [[self myJIDForXMPPStream:xmppStream] bare]; + + NSEntityDescription *messageEntity = [self messageEntity:moc]; + + // Add to database + + XMPPRoomMessageCoreDataStorageObject *roomMessage = (XMPPRoomMessageCoreDataStorageObject *) + [[NSManagedObject alloc] initWithEntity:messageEntity insertIntoManagedObjectContext:nil]; + + roomMessage.message = message; + roomMessage.roomJID = roomJID; + roomMessage.jid = messageJID; + roomMessage.nickname = [messageJID resource]; + roomMessage.body = messageBody; + roomMessage.localTimestamp = localTimestamp; + roomMessage.remoteTimestamp = remoteTimestamp; + roomMessage.isFromMe = isOutgoing; + roomMessage.streamBareJidStr = streamBareJidStr; + + [moc insertObject:roomMessage]; // Hook if subclassing XMPPRoomMessageCoreDataStorageObject (awakeFromInsert) + [self didInsertMessage:roomMessage]; // Hook if subclassing XMPPRoomCoreDataStorage +} + +/** + * Optional override hook for general extensions. + * + * @see insertOccupantWithPresence:room:stream: +**/ +- (void)didInsertOccupant:(XMPPRoomOccupantCoreDataStorageObject *)occupant +{ + // Override me if you're extending the XMPPRoomOccupantCoreDataStorageObject class to add additional properties. + // You can update your additional properties here. + // + // At this point the standard XMPPRoomOccupantCDSO properties have already been set. + // So you can, for example, access the XMPPPresence via occupant.presence. +} + +/** + * Optional override hook for general extensions. + * + * @see updateOccupant:withPresence:room:stream: +**/ +- (void)didUpdateOccupant:(XMPPRoomOccupantCoreDataStorageObject *)occupant +{ + // Override me if you're extending the XMPPRoomOccupantCoreDataStorageObject class to add additional properties. + // You can update your additional properties here. + // + // At this point the standard XMPPRoomOccupantCDSO properties have already been updated. + // So you can, for example, access the XMPPPresence via occupant.presence. +} + +/** + * Optional override hook for complete customization. + * Override me if you need to do specific custom work when inserting an occupant in a room. + * + * @see didInsertOccupant: +**/ +- (void)insertOccupantWithPresence:(XMPPPresence *)presence + room:(XMPPRoom *)room + stream:(XMPPStream *)xmppStream +{ + // Extract needed information + + XMPPJID *roomJID = room.roomJID; + XMPPJID *presenceJID = [presence from]; + + NSString *role = nil; + NSString *affiliation = nil; + XMPPJID *realJID = nil; + + NSXMLElement *x = [presence elementForName:@"x" xmlns:@"http://jabber.org/protocol/muc#user"]; + NSXMLElement *item = [x elementForName:@"item"]; + if (item) + { + role = [[item attributeStringValueForName:@"role"] lowercaseString]; + affiliation = [[item attributeStringValueForName:@"affiliation"] lowercaseString]; + + NSString *realJIDStr = [item attributeStringValueForName:@"jid"]; + if (realJIDStr) + { + realJID = [XMPPJID jidWithString:realJIDStr]; + } + } + + // Add to database + + NSManagedObjectContext *moc = [self managedObjectContext]; + NSString *streamBareJidStr = [[self myJIDForXMPPStream:xmppStream] bare]; + + NSEntityDescription *occupantEntity = [self occupantEntity:moc]; + + XMPPRoomOccupantCoreDataStorageObject *occupant = (XMPPRoomOccupantCoreDataStorageObject *) + [[NSManagedObject alloc] initWithEntity:occupantEntity insertIntoManagedObjectContext:nil]; + + occupant.presence = presence; + occupant.roomJID = roomJID; + occupant.jid = presenceJID; + occupant.nickname = [presenceJID resource]; + occupant.role = role; + occupant.affiliation = affiliation; + occupant.realJID = realJID; + occupant.createdAt = [NSDate date]; + occupant.streamBareJidStr = streamBareJidStr; + + [moc insertObject:occupant]; // Hook if subclassing XMPPRoomOccupantCoreDataStorageObject (awakeFromInsert) + [self didInsertOccupant:occupant]; // Hook if subclassing XMPPRoomCoreDataStorage +} + +/** + * Optional override hook for complete customization. + * Override me if you need to do specific custom work when updating an occupant in a room. + * + * @see didUpdateOccupant: +**/ +- (void)updateOccupant:(XMPPRoomOccupantCoreDataStorageObject *)occupant + withPresence:(XMPPPresence *)presence + room:(XMPPRoom *)room + stream:(XMPPStream *)stream +{ + // Extract needed information + + NSString *role = nil; + NSString *affiliation = nil; + XMPPJID *realJID = nil; + + NSXMLElement *x = [presence elementForName:@"x" xmlns:@"http://jabber.org/protocol/muc#user"]; + NSXMLElement *item = [x elementForName:@"item"]; + if (item) + { + role = [[item attributeStringValueForName:@"role"] lowercaseString]; + affiliation = [[item attributeStringValueForName:@"affiliation"] lowercaseString]; + + NSString *realJIDStr = [item attributeStringValueForName:@"jid"]; + if (realJIDStr) + { + realJID = [XMPPJID jidWithString:realJIDStr]; + } + } + + // Update database + + occupant.presence = presence; + occupant.role = role; + occupant.affiliation = affiliation; + occupant.realJID = realJID; + + [self didUpdateOccupant:occupant]; // Hook if subclassing XMPPRoomCoreDataStorage +} + +/** + * Optional override hook. +**/ +- (void)removeOccupant:(XMPPRoomOccupantCoreDataStorageObject *)occupant + withPresence:(XMPPPresence *)presence + room:(XMPPRoom *)room + stream:(XMPPStream *)stream +{ + // Delete from database + + [[occupant managedObjectContext] deleteObject:occupant]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Public API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSEntityDescription *)messageEntity:(NSManagedObjectContext *)moc +{ + // This method should be thread-safe. + // So be sure to access the entity name through the property accessor. + + return [NSEntityDescription entityForName:[self messageEntityName] inManagedObjectContext:moc]; +} + +- (NSEntityDescription *)occupantEntity:(NSManagedObjectContext *)moc +{ + // This method should be thread-safe. + // So be sure to access the entity name through the property accessor. + + return [NSEntityDescription entityForName:[self occupantEntityName] inManagedObjectContext:moc]; +} + +- (NSDate *)mostRecentMessageTimestampForRoom:(XMPPJID *)roomJID + stream:(XMPPStream *)xmppStream + inContext:(NSManagedObjectContext *)inMoc +{ + if (roomJID == nil) return nil; + + // It's possible to use our internal managedObjectContext only because we're not returning a NSManagedObject. + + __block NSDate *result = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + NSManagedObjectContext *moc = inMoc ? : [self managedObjectContext]; + + NSEntityDescription *entity = [self messageEntity:moc]; + + NSPredicate *predicate; + if (xmppStream) + { + NSString *streamBareJidStr = [[self myJIDForXMPPStream:xmppStream] bare]; + + NSString *predicateFormat = @"roomJIDStr == %@ AND streamBareJidStr == %@"; + predicate = [NSPredicate predicateWithFormat:predicateFormat, roomJID.bare, streamBareJidStr]; + } + else + { + predicate = [NSPredicate predicateWithFormat:@"roomJIDStr == %@", roomJID.bare]; + } + + NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"localTimestamp" ascending:NO]; + NSArray *sortDescriptors = @[sortDescriptor]; + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:entity]; + [fetchRequest setPredicate:predicate]; + [fetchRequest setSortDescriptors:sortDescriptors]; + [fetchRequest setFetchLimit:1]; + + NSError *error = nil; + XMPPRoomMessageCoreDataStorageObject *message = [[moc executeFetchRequest:fetchRequest error:&error] lastObject]; + + if (error) + { + XMPPLogError(@"%@: %@ - fetchRequest error: %@", THIS_FILE, THIS_METHOD, error); + } + else + { + result = [message.localTimestamp copy]; + } + }}; + + if (inMoc == nil) + dispatch_sync(storageQueue, block); + else + block(); + + return result; +} + +- (XMPPRoomOccupantCoreDataStorageObject *)occupantForJID:(XMPPJID *)jid + stream:(XMPPStream *)xmppStream + inContext:(NSManagedObjectContext *)moc +{ + if (jid == nil) return nil; + if (moc == nil) return nil; + + NSEntityDescription *entity = [self occupantEntity:moc]; + + NSPredicate *predicate; + if (xmppStream) + { + NSString *streamBareJidStr = [[self myJIDForXMPPStream:xmppStream] bare]; + + NSString *predicateFormat = @"jidStr == %@ AND streamBareJidStr == %@"; + predicate = [NSPredicate predicateWithFormat:predicateFormat, jid, streamBareJidStr]; + } + else + { + predicate = [NSPredicate predicateWithFormat:@"jidStr == %@", jid]; + } + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:entity]; + [fetchRequest setPredicate:predicate]; + [fetchRequest setFetchBatchSize:1]; + + NSError *error = nil; + NSArray *results = [moc executeFetchRequest:fetchRequest error:&error]; + + if (error) + { + XMPPLogWarn(@"%@: %@ - fetch error: %@", THIS_FILE, THIS_METHOD, error); + } + + return (XMPPRoomOccupantCoreDataStorageObject *)[results lastObject]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPRoomStorage Protocol +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)handlePresence:(XMPPPresence *)presence room:(XMPPRoom *)room +{ + XMPPLogTrace(); + + + XMPPJID *presenceJID = [presence from]; + XMPPStream *xmppStream = room.xmppStream; + + [self scheduleBlock:^{ + + NSManagedObjectContext *moc = [self managedObjectContext]; + + // Is occupant already in database? + + XMPPRoomOccupantCoreDataStorageObject *occupant = + [self occupantForJID:presenceJID stream:xmppStream inContext:moc]; + + // Is occupant available or unavailable? + + if ([[presence type] isEqualToString:@"unavailable"]) + { + // Remove occupant record from database + + if (occupant) + { + [self removeOccupant:occupant withPresence:presence room:room stream:xmppStream]; + } + } + else + { + // Insert or update occupant in database + + if (occupant) + { + [self updateOccupant:occupant withPresence:presence room:room stream:xmppStream]; + } + else + { + [self insertOccupantWithPresence:presence room:room stream:xmppStream]; + } + } + }]; +} + +- (void)handleOutgoingMessage:(XMPPMessage *)message room:(XMPPRoom *)room +{ + XMPPLogTrace(); + + XMPPStream *xmppStream = room.xmppStream; + + [self scheduleBlock:^{ + + [self insertMessage:message outgoing:YES forRoom:room stream:xmppStream]; + }]; +} + +- (void)handleIncomingMessage:(XMPPMessage *)message room:(XMPPRoom *)room +{ + XMPPLogTrace(); + + XMPPJID *myRoomJID = room.myRoomJID; + XMPPJID *messageJID = [message from]; + + if ([myRoomJID isEqualToJID:messageJID]) + { + if (![message wasDelayed]) + { + // Ignore - we already stored message in handleOutgoingMessage:room: + return; + } + } + + XMPPStream *xmppStream = room.xmppStream; + + [self scheduleBlock:^{ + + if ([self existsMessage:message forRoom:room stream:xmppStream]) + { + XMPPLogVerbose(@"%@: %@ - Duplicate message", THIS_FILE, THIS_METHOD); + } + else + { + [self insertMessage:message outgoing:NO forRoom:room stream:xmppStream]; + } + }]; +} + +- (void)handleDidLeaveRoom:(XMPPRoom *)room +{ + XMPPLogTrace(); + + XMPPJID *roomJID = room.roomJID; + + [self scheduleBlock:^{ + + [self clearAllOccupantsFromRoom:roomJID]; + }]; +} + +@end diff --git a/Extensions/XEP-0045/CoreDataStorage/XMPPRoomMessageCoreDataStorageObject.h b/Extensions/XEP-0045/CoreDataStorage/XMPPRoomMessageCoreDataStorageObject.h new file mode 100644 index 0000000..6988532 --- /dev/null +++ b/Extensions/XEP-0045/CoreDataStorage/XMPPRoomMessageCoreDataStorageObject.h @@ -0,0 +1,47 @@ +#import +#import + +#import "XMPP.h" +#import "XMPPRoom.h" + + +@interface XMPPRoomMessageCoreDataStorageObject : NSManagedObject + +/** + * The properties below are documented in the XMPPRoomMessage protocol. +**/ + +@property (nonatomic, retain) XMPPMessage * message; // Transient (proper type, not on disk) +@property (nonatomic, retain) NSString * messageStr; // Shadow (binary data, written to disk) + +@property (nonatomic, strong) XMPPJID * roomJID; // Transient (proper type, not on disk) +@property (nonatomic, strong) NSString * roomJIDStr; // Shadow (binary data, written to disk) + +@property (nonatomic, retain) XMPPJID * jid; // Transient (proper type, not on disk) +@property (nonatomic, retain) NSString * jidStr; // Shadow (binary data, written to disk) + +@property (nonatomic, retain) NSString * nickname; +@property (nonatomic, retain) NSString * body; + +@property (nonatomic, retain) NSDate * localTimestamp; +@property (nonatomic, strong) NSDate * remoteTimestamp; + +@property (nonatomic, assign) BOOL isFromMe; +@property (nonatomic, strong) NSNumber * fromMe; + +/** + * The 'type' property can be used to inject event messages. + * For example: "JohnDoe entered the room". + * + * You can define your own types to suit your needs. + * All normal messages will have a type of zero. +**/ +@property (nonatomic, strong) NSNumber * type; + +/** + * If a single instance of XMPPRoomCoreDataStorage is shared between multiple xmppStream's, + * this may be needed to distinguish between the streams. +**/ +@property (nonatomic, strong) NSString *streamBareJidStr; + +@end diff --git a/Extensions/XEP-0045/CoreDataStorage/XMPPRoomMessageCoreDataStorageObject.m b/Extensions/XEP-0045/CoreDataStorage/XMPPRoomMessageCoreDataStorageObject.m new file mode 100644 index 0000000..2d43565 --- /dev/null +++ b/Extensions/XEP-0045/CoreDataStorage/XMPPRoomMessageCoreDataStorageObject.m @@ -0,0 +1,205 @@ +#import "XMPPRoomMessageCoreDataStorageObject.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@interface XMPPRoomMessageCoreDataStorageObject () + +@property(nonatomic,strong) XMPPJID * primitiveRoomJID; +@property(nonatomic,strong) NSString * primitiveRoomJIDStr; + +@property(nonatomic,strong) XMPPJID * primitiveJid; +@property(nonatomic,strong) NSString * primitiveJidStr; + +@property(nonatomic,strong) XMPPMessage * primitiveMessage; +@property(nonatomic,strong) NSString * primitiveMessageStr; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPRoomMessageCoreDataStorageObject + +@dynamic message; +@dynamic messageStr; + +@dynamic roomJID, primitiveRoomJID; +@dynamic roomJIDStr, primitiveRoomJIDStr; + +@dynamic jid, primitiveJid; +@dynamic jidStr, primitiveJidStr; + +@dynamic nickname; +@dynamic body; +@dynamic localTimestamp; +@dynamic remoteTimestamp; +@dynamic isFromMe; +@dynamic fromMe; + +@dynamic type; + +@dynamic streamBareJidStr; + +@dynamic primitiveMessage; +@dynamic primitiveMessageStr; + +#pragma mark Transient roomJID + +- (XMPPJID *)roomJID +{ + // Create and cache on demand + + [self willAccessValueForKey:@"roomJID"]; + XMPPJID *tmp = self.primitiveRoomJID; + [self didAccessValueForKey:@"roomJID"]; + + if (tmp == nil) + { + NSString *roomJIDStr = self.roomJIDStr; + if (roomJIDStr) + { + tmp = [XMPPJID jidWithString:roomJIDStr]; + self.primitiveRoomJID = tmp; + } + } + + return tmp; +} + +- (void)setRoomJID:(XMPPJID *)roomJID +{ + [self willChangeValueForKey:@"roomJID"]; + [self willChangeValueForKey:@"roomJIDStr"]; + + self.primitiveRoomJID = roomJID; + self.primitiveRoomJIDStr = [roomJID full]; + + [self didChangeValueForKey:@"roomJID"]; + [self didChangeValueForKey:@"roomJIDStr"]; +} + +- (void)setRoomJIDStr:(NSString *)roomJIDStr +{ + [self willChangeValueForKey:@"roomJID"]; + [self willChangeValueForKey:@"roomJIDStr"]; + + self.primitiveRoomJID = [XMPPJID jidWithString:roomJIDStr]; + self.primitiveRoomJIDStr = roomJIDStr; + + [self didChangeValueForKey:@"roomJID"]; + [self didChangeValueForKey:@"roomJIDStr"]; +} + +#pragma mark Transient jid + +- (XMPPJID *)jid +{ + // Create and cache on demand + + [self willAccessValueForKey:@"jid"]; + XMPPJID *tmp = self.primitiveJid; + [self didAccessValueForKey:@"jid"]; + + if (tmp == nil) + { + NSString *jidStr = self.jidStr; + if (jidStr) + { + tmp = [XMPPJID jidWithString:jidStr]; + self.primitiveJid = tmp; + } + } + + return tmp; +} + +- (void)setJid:(XMPPJID *)jid +{ + [self willChangeValueForKey:@"jid"]; + [self willChangeValueForKey:@"jidStr"]; + + self.primitiveJid = jid; + self.primitiveJidStr = [jid full]; + + [self didChangeValueForKey:@"jid"]; + [self didChangeValueForKey:@"jidStr"]; +} + +- (void)setJidStr:(NSString *)jidStr +{ + [self willChangeValueForKey:@"jid"]; + [self willChangeValueForKey:@"jidStr"]; + + self.primitiveJid = [XMPPJID jidWithString:jidStr]; + self.primitiveJidStr = jidStr; + + [self didChangeValueForKey:@"jid"]; + [self didChangeValueForKey:@"jidStr"]; +} + +#pragma mark Scalar + +- (BOOL)isFromMe +{ + return [[self fromMe] boolValue]; +} + +- (void)setIsFromMe:(BOOL)value +{ + self.fromMe = @(value); +} + +#pragma mark - Message +- (XMPPMessage *)message +{ + // Create and cache on demand + [self willAccessValueForKey:@"message"]; + + XMPPMessage *message = self.primitiveMessage; + + [self didAccessValueForKey:@"message"]; + + if (message == nil) + { + NSString *messageStr = self.messageStr; + + if (messageStr) + { + NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString:messageStr error:nil]; + + message = [XMPPMessage messageFromElement:element]; + self.primitiveMessage = message; + } + } + + return message; +} + +- (void)setMessage:(XMPPMessage *)message +{ + [self willChangeValueForKey:@"message"]; + [self willChangeValueForKey:@"messageStr"]; + self.primitiveMessage = message; + self.primitiveMessageStr = [message compactXMLString]; + [self didChangeValueForKey:@"message"]; + [self didChangeValueForKey:@"messageStr"]; +} + +- (void)setMessageStr:(NSString *)messageStr +{ + [self willChangeValueForKey:@"message"]; + [self willChangeValueForKey:@"messageStr"]; + + NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString:messageStr error:nil]; + + self.primitiveMessage = [XMPPMessage messageFromElement:element]; + self.primitiveMessageStr = messageStr; + [self didChangeValueForKey:@"message"]; + [self didChangeValueForKey:@"messageStr"]; +} + +@end diff --git a/Extensions/XEP-0045/CoreDataStorage/XMPPRoomOccupantCoreDataStorageObject.h b/Extensions/XEP-0045/CoreDataStorage/XMPPRoomOccupantCoreDataStorageObject.h new file mode 100644 index 0000000..376995a --- /dev/null +++ b/Extensions/XEP-0045/CoreDataStorage/XMPPRoomOccupantCoreDataStorageObject.h @@ -0,0 +1,39 @@ +#import +#import + +#import "XMPP.h" +#import "XMPPRoom.h" + + +@interface XMPPRoomOccupantCoreDataStorageObject : NSManagedObject + +/** + * The properties below are documented in the XMPPRoomOccupant protocol. +**/ + +@property (nonatomic, strong) XMPPPresence * presence; // Transient (proper type, not on disk) +@property (nonatomic, strong) NSString * presenceStr; // Shadow (binary data, written to disk) + +@property (nonatomic, strong) XMPPJID * roomJID; // Transient (proper type, not on disk) +@property (nonatomic, strong) NSString * roomJIDStr; // Shadow (binary data, written to disk) + +@property (nonatomic, strong) XMPPJID * jid; // Transient (proper type, not on disk) +@property (nonatomic, strong) NSString * jidStr; // Shadow (binary data, written to disk) + +@property (nonatomic, strong) NSString * nickname; + +@property (nonatomic, strong) NSString * role; +@property (nonatomic, strong) NSString * affiliation; + +@property (nonatomic, strong) XMPPJID * realJID; // Transient (proper type, not on disk) +@property (nonatomic, strong) NSString * realJIDStr; // Shadow (binary data, written to disk) + +@property (nonatomic, strong) NSDate * createdAt; + +/** + * If a single instance of XMPPRoomCoreDataStorage is shared between multiple xmppStream's, + * this may be needed to distinguish between the streams. +**/ +@property (nonatomic, strong) NSString * streamBareJidStr; + +@end diff --git a/Extensions/XEP-0045/CoreDataStorage/XMPPRoomOccupantCoreDataStorageObject.m b/Extensions/XEP-0045/CoreDataStorage/XMPPRoomOccupantCoreDataStorageObject.m new file mode 100644 index 0000000..36e111c --- /dev/null +++ b/Extensions/XEP-0045/CoreDataStorage/XMPPRoomOccupantCoreDataStorageObject.m @@ -0,0 +1,234 @@ +#import "XMPPRoomOccupantCoreDataStorageObject.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@interface XMPPRoomOccupantCoreDataStorageObject () + +@property(nonatomic,strong) XMPPPresence * primitivePresence; +@property(nonatomic,strong) NSString * primitivePresenceStr; + +@property(nonatomic,strong) XMPPJID * primitiveRoomJID; +@property(nonatomic,strong) NSString * primitiveRoomJIDStr; + +@property(nonatomic,strong) XMPPJID * primitiveJid; +@property(nonatomic,strong) NSString * primitiveJidStr; + +@property(nonatomic,strong) XMPPJID * primitiveRealJID; +@property(nonatomic,strong) NSString * primitiveRealJIDStr; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPRoomOccupantCoreDataStorageObject + +@dynamic presence, primitivePresence; +@dynamic presenceStr, primitivePresenceStr; +@dynamic roomJID, primitiveRoomJID; +@dynamic roomJIDStr, primitiveRoomJIDStr; +@dynamic jid, primitiveJid; +@dynamic jidStr, primitiveJidStr; +@dynamic nickname; +@dynamic role; +@dynamic affiliation; +@dynamic realJID, primitiveRealJID; +@dynamic realJIDStr, primitiveRealJIDStr; +@dynamic createdAt; +@dynamic streamBareJidStr; + +#pragma mark Transient presence + +- (XMPPPresence *)presence +{ + // Create and cache on demand + + [self willAccessValueForKey:@"presence"]; + XMPPPresence *presence = self.primitivePresence; + [self didAccessValueForKey:@"presence"]; + + if (presence == nil) + { + NSString *presenceStr = self.presenceStr; + if (presenceStr) + { + NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString:presenceStr error:nil]; + presence = [XMPPPresence presenceFromElement:element]; + self.primitivePresence = presence; + } + } + + return presence; +} + +- (void)setPresence:(XMPPPresence *)newPresence +{ + [self willChangeValueForKey:@"presence"]; + [self willChangeValueForKey:@"presenceStr"]; + + self.primitivePresence = newPresence; + self.primitivePresenceStr = [newPresence compactXMLString]; + + [self didChangeValueForKey:@"presence"]; + [self didChangeValueForKey:@"presenceStr"]; +} + +- (void)setPresenceStr:(NSString *)presenceStr +{ + [self willChangeValueForKey:@"presence"]; + [self willChangeValueForKey:@"presenceStr"]; + + NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString:presenceStr error:nil]; + self.primitivePresence = [XMPPPresence presenceFromElement:element]; + self.primitivePresenceStr = presenceStr; + + [self didChangeValueForKey:@"presence"]; + [self didChangeValueForKey:@"presenceStr"]; +} + +#pragma mark Transient roomJID + +- (XMPPJID *)roomJID +{ + // Create and cache on demand + + [self willAccessValueForKey:@"roomJID"]; + XMPPJID *tmp = self.primitiveRoomJID; + [self didAccessValueForKey:@"roomJID"]; + + if (tmp == nil) + { + NSString *roomJIDStr = self.roomJIDStr; + if (roomJIDStr) + { + tmp = [XMPPJID jidWithString:roomJIDStr]; + self.primitiveRoomJID = tmp; + } + } + + return tmp; +} + +- (void)setRoomJID:(XMPPJID *)roomJID +{ + [self willChangeValueForKey:@"roomJID"]; + [self willChangeValueForKey:@"roomJIDStr"]; + + self.primitiveRoomJID = roomJID; + self.primitiveRoomJIDStr = [roomJID full]; + + [self didChangeValueForKey:@"roomJID"]; + [self didChangeValueForKey:@"roomJIDStr"]; +} + +- (void)setRoomJIDStr:(NSString *)roomJIDStr +{ + [self willChangeValueForKey:@"roomJID"]; + [self willChangeValueForKey:@"roomJIDStr"]; + + self.primitiveRoomJID = [XMPPJID jidWithString:roomJIDStr]; + self.primitiveRoomJIDStr = roomJIDStr; + + [self didChangeValueForKey:@"roomJID"]; + [self didChangeValueForKey:@"roomJIDStr"]; +} + +#pragma mark Transient jid + +- (XMPPJID *)jid +{ + // Create and cache on demand + + [self willAccessValueForKey:@"jid"]; + XMPPJID *tmp = self.primitiveJid; + [self didAccessValueForKey:@"jid"]; + + if (tmp == nil) + { + NSString *jidStr = self.jidStr; + if (jidStr) + { + tmp = [XMPPJID jidWithString:jidStr]; + self.primitiveJid = tmp; + } + } + + return tmp; +} + +- (void)setJid:(XMPPJID *)jid +{ + [self willChangeValueForKey:@"jid"]; + [self willChangeValueForKey:@"jidStr"]; + + self.primitiveJid = jid; + self.primitiveJidStr = [jid full]; + + [self didChangeValueForKey:@"jid"]; + [self didChangeValueForKey:@"jidStr"]; +} + +- (void)setJidStr:(NSString *)jidStr +{ + [self willChangeValueForKey:@"jid"]; + [self willChangeValueForKey:@"jidStr"]; + + self.primitiveJid = [XMPPJID jidWithString:jidStr]; + self.primitiveJidStr = jidStr; + + [self didChangeValueForKey:@"jid"]; + [self didChangeValueForKey:@"jidStr"]; +} + +#pragma mark Transient realJID + +- (XMPPJID *)realJID +{ + // Create and cache on demand + + [self willAccessValueForKey:@"realJID"]; + XMPPJID *tmp = self.primitiveRealJID; + [self didAccessValueForKey:@"realJID"]; + + if (tmp == nil) + { + NSString *realJIDStr = self.realJIDStr; + if (realJIDStr) + { + tmp = [XMPPJID jidWithString:realJIDStr]; + self.primitiveRealJID = tmp; + } + } + + return tmp; +} + +- (void)setRealJID:(XMPPJID *)realJID +{ + [self willChangeValueForKey:@"realJID"]; + [self willChangeValueForKey:@"realJIDStr"]; + + self.primitiveRealJID = realJID; + self.primitiveRealJIDStr = [realJID full]; + + [self didChangeValueForKey:@"realJID"]; + [self didChangeValueForKey:@"realJIDStr"]; +} + +- (void)setRealJIDStr:(NSString *)realJIDStr +{ + [self willChangeValueForKey:@"realJID"]; + [self willChangeValueForKey:@"realJIDStr"]; + + self.primitiveRealJID = [XMPPJID jidWithString:realJIDStr]; + self.primitiveRealJIDStr = realJIDStr; + + [self didChangeValueForKey:@"realJID"]; + [self didChangeValueForKey:@"realJIDStr"]; +} + +@end diff --git a/Extensions/XEP-0045/HybridStorage/XMPPRoomHybrid.xcdatamodeld/.xccurrentversion b/Extensions/XEP-0045/HybridStorage/XMPPRoomHybrid.xcdatamodeld/.xccurrentversion new file mode 100644 index 0000000..373fcc5 --- /dev/null +++ b/Extensions/XEP-0045/HybridStorage/XMPPRoomHybrid.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + XMPPRoomHybrid.xcdatamodel + + diff --git a/Extensions/XEP-0045/HybridStorage/XMPPRoomHybrid.xcdatamodeld/XMPPRoomHybrid.xcdatamodel/contents b/Extensions/XEP-0045/HybridStorage/XMPPRoomHybrid.xcdatamodeld/XMPPRoomHybrid.xcdatamodel/contents new file mode 100644 index 0000000..1687257 --- /dev/null +++ b/Extensions/XEP-0045/HybridStorage/XMPPRoomHybrid.xcdatamodeld/XMPPRoomHybrid.xcdatamodel/contents @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Extensions/XEP-0045/HybridStorage/XMPPRoomHybridStorage.h b/Extensions/XEP-0045/HybridStorage/XMPPRoomHybridStorage.h new file mode 100644 index 0000000..ea3c272 --- /dev/null +++ b/Extensions/XEP-0045/HybridStorage/XMPPRoomHybridStorage.h @@ -0,0 +1,197 @@ +#import +#import + +#import "XMPP.h" +#import "XMPPRoom.h" +#import "XMPPRoomMessageHybridCoreDataStorageObject.h" +#import "XMPPRoomOccupantHybridMemoryStorageObject.h" +#import "XMPPCoreDataStorage.h" + +/** + * This class is an example implementation of the XMPPRoomStorage protocol. + * It stores messages in a database using core data, and stores occupants in memory (as they are temporary). + * + * You are free to substitute your own storage class. +**/ + +@interface XMPPRoomHybridStorage : XMPPCoreDataStorage +{ + @protected + + /* Inherited from XMPPCoreDataStorage + + NSString *databaseFileName; + NSUInteger saveThreshold; + + dispatch_queue_t storageQueue; + + */ + + // The occupantsGlobalDict holds all occupants in a heirarchy. + // It is a dictionary of dictionaries of dictionaries. + + NSMutableDictionary * occupantsGlobalDict; // Key=xmppStream.myJid, Value=occupantsRoomsDict +// NSMutableDictionary * occupantsRoomsDict; // Key=xmppRoomJid, Value=occupantsRoomDict +// NSMutableDictionary * occupantsRoomDict; // Key=occupantJid, Value=XMPPRoomOccupantHybridMemoryStorageObject +} + +/** + * Convenience method to get an instance with the default database name. + * + * IMPORTANT: + * You are NOT required to use the sharedInstance. + * + * If your application makes extensive use of MUC, and you use a sharedInstance of this class, + * then all of your MUC rooms share the same database store. You might get better performance if you create + * multiple instances of this class instead (using different database filenames), as this way you can have + * concurrent writes to multiple databases. +**/ ++ (instancetype)sharedInstance; + +/* Inherited from XMPPCoreDataStorage + * Please see the XMPPCoreDataStorage header file for extensive documentation. + +- (id)initWithDatabaseFilename:(NSString *)databaseFileName; +- (id)initWithInMemoryStore; + +@property (readonly) NSString *databaseFileName; + +@property (readwrite) NSUInteger saveThreshold; + +@property (readonly) NSManagedObjectModel *managedObjectModel; +@property (readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator; + +*/ + +/** + * You may choose to extend this class, and/or the message class for customized functionality. + * These properties allow for such customization. + * + * You must set your desired entity name, if different from default, before you begin using the storage class. +**/ +@property (strong, readwrite) NSString * messageEntityName; + +/** + * You can optionally extend the XMPPRoomOccupantMemoryStorageObject class. + * Then just set the class here, and your subclass will automatically get used. + * + * You must set your desired class, if different from default, before you begin using the storage class. +**/ +@property (assign, readwrite) Class occupantClass; + +/** + * It is likely you don't want the message history to persist forever. + * Doing so would allow the database to grow infinitely large over time. + * + * The maxMessageAge property provides a way to specify how old a message can get + * before it should get deleted from the database. + * + * The deleteInterval specifies how often to sweep for old messages. + * Since deleting is an expensive operation (disk io) it is done on a fixed interval. + * + * You can optionally disable the maxMessageAge by setting it to zero (or a negative value). + * If you disable the maxMessageAge then old messages are not deleted. + * + * You can optionally disable the deleteInterval by setting it to zero (or a negative value). + * + * The default maxAge is 7 days. + * The default deleteInterval is 5 minutes. +**/ +@property (assign, readwrite) NSTimeInterval maxMessageAge; +@property (assign, readwrite) NSTimeInterval deleteInterval; + +/** + * You may optionally prevent old message deletion for particular rooms. +**/ +- (void)pauseOldMessageDeletionForRoom:(XMPPJID *)roomJID; +- (void)resumeOldMessageDeletionForRoom:(XMPPJID *)roomJID; + +/** + * Convenience method to get the message entity description. + * + * @see messageEntityName +**/ +- (NSEntityDescription *)messageEntity:(NSManagedObjectContext *)moc; + +/** + * Returns the timestamp of the most recent message stored in the database for the given room. + * This may be used when requesting the message history from the server, + * to prevent redownloading messages you already have. + * + * @param roomJID - The JID of the room (a bare JID) + * + * @param xmppStream - This class can support multiple concurrent xmppStreams. + * Optionally pass the xmppStream the room applies to. + * If you're using this claass with a single xmppStream, you can pass nil. + * + * @param moc - The managedObjectContext to use when doing the lookups. + * If non-nil, this should match the thread you're currently using. + * If nil, the operation is dispatch_sync'd onto the internal queue, + * and uses the internal managedObjectContext. + * + * The moc may optionally be nil strictly because this method does not return a NSManagedObject. +**/ +- (NSDate *)mostRecentMessageTimestampForRoom:(XMPPJID *)roomJID + stream:(XMPPStream *)xmppStream + inContext:(NSManagedObjectContext *)moc; + +/** + * Returns the occupant for the given full jid. + * + * @param jid - The full jid of the room occupant (including resource). + * E.g. xmppDevelopers@conf.xmpp.org/robbiehanson + * + * @param xmppStream - This class can support multiple concurrent xmppStreams. + * Optionally pass the xmppStream the room applies to. + * If you're using this claass with a single xmppStream, you can pass nil. +**/ +- (XMPPRoomOccupantHybridMemoryStorageObject *)occupantForJID:(XMPPJID *)jid stream:(XMPPStream *)xmppStream; + +/** + * Returns all occupants in the given room. + * Each occupant instance will be of kind XMPPRoomOccupantHybridMemoryStorageObject. + * + * @param roomJid - The JID of the room (a bare JID). + * E.g. xmppDevelopers@conf.xmpp.org + * + * @param xmppStream - This class can support multiple concurrent xmppStreams. + * Optionally pass the xmppStream the room applies to. + * If you're using this claass with a single xmppStream, you can pass nil. +**/ +- (NSArray *)occupantsForRoom:(XMPPJID *)roomJid stream:(XMPPStream *)xmppStream; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPRoomHybridStorageDelegate +@optional + +// +// XMPPRoomHybridStorage automatically uses the delegate(s) of its parent XMPPRoom. +// + +/** + * Similar to XMPPRoomDelegate's xmppRoom:occupantDidJoin:withPresence: method. + * This method provides the delegate with the occupant storage instance. +**/ +- (void)xmppRoomHybridStorage:(XMPPRoomHybridStorage *)sender + occupantDidJoin:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant; + +/** + * Similar to XMPPRoomDelegate's xmppRoom:occupantDidLeave:withPresence: method. + * This method provides the delegate with the occupant storage instance. +**/ +- (void)xmppRoomHybridStorage:(XMPPRoomHybridStorage *)sender + occupantDidLeave:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant; + +/** + * Similar to XMPPRoomDelegate's xmppRoom:occupantDidUpdate:withPresence: method. + * This method provides the delegate with the occupant storage instance. +**/ +- (void)xmppRoomHybridStorage:(XMPPRoomHybridStorage *)sender + occupantDidUpdate:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant; + +@end diff --git a/Extensions/XEP-0045/HybridStorage/XMPPRoomHybridStorage.m b/Extensions/XEP-0045/HybridStorage/XMPPRoomHybridStorage.m new file mode 100644 index 0000000..19c07f2 --- /dev/null +++ b/Extensions/XEP-0045/HybridStorage/XMPPRoomHybridStorage.m @@ -0,0 +1,990 @@ +#import "XMPPRoomHybridStorage.h" +#import "XMPPRoomPrivate.h" +#import "XMPPCoreDataStorageProtected.h" +#import "NSXMLElement+XEP_0203.h" +#import "XMPPLogging.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +#define AssertPrivateQueue() \ + NSAssert(dispatch_get_specific(storageQueueTag), @"Private method: MUST run on storageQueue"); + + +@interface XMPPRoomHybridStorage () +{ + // Protected variables are listed in the header file. + // These are the private variables. + + NSString *messageEntityName; + Class occupantClass; + + NSTimeInterval maxMessageAge; + NSTimeInterval deleteInterval; + + NSMutableSet *pausedMessageDeletion; + + dispatch_time_t lastDeleteTime; + dispatch_source_t deleteTimer; +} + +- (void)performDelete; +- (void)destroyDeleteTimer; +- (void)updateDeleteTimer; +- (void)createAndStartDeleteTimer; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPRoomHybridStorage + +static XMPPRoomHybridStorage *sharedInstance; + ++ (instancetype)sharedInstance +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + sharedInstance = [[XMPPRoomHybridStorage alloc] initWithDatabaseFilename:nil storeOptions:nil]; + }); + + return sharedInstance; +} + +- (void)commonInit +{ + XMPPLogTrace(); + [super commonInit]; + + // This method is invoked by all public init methods of the superclass + + occupantsGlobalDict = [[NSMutableDictionary alloc] init]; + + messageEntityName = NSStringFromClass([XMPPRoomMessageHybridCoreDataStorageObject class]); + occupantClass = [XMPPRoomOccupantHybridMemoryStorageObject class]; + + maxMessageAge = (60 * 60 * 24 * 7); // 7 days + deleteInterval = (60 * 5); // 5 days + + pausedMessageDeletion = [[NSMutableSet alloc] init]; + + autoRecreateDatabaseFile = YES; +} + +/** + * Documentation from the superclass (XMPPCoreDataStorage): + * + * Override me, if needed, to provide customized behavior. + * + * This method is queried to get the name of the ManagedObjectModel within the app bundle. + * It should return the name of the appropriate file (*.xdatamodel / *.mom / *.momd) sans file extension. + * + * The default implementation returns the name of the subclass, stripping any suffix of "CoreDataStorage". + * E.g., if your subclass was named "XMPPExtensionCoreDataStorage", then this method would return "XMPPExtension". + * + * Note that a file extension should NOT be included. +**/ +- (NSString *)managedObjectModelName +{ + // Optional hook + // + // The default implementation would return "XMPPPRoomHybridStorage". + // We prefer a slightly shorter version. + + return @"XMPPRoomHybrid"; +} + +- (void)dealloc +{ + [self destroyDeleteTimer]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@synthesize messageEntityName; +@synthesize occupantClass; + +- (NSTimeInterval)maxMessageAge +{ + __block NSTimeInterval result = 0; + + dispatch_block_t block = ^{ + result = maxMessageAge; + }; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_sync(storageQueue, block); + + return result; +} + +- (void)setMaxMessageAge:(NSTimeInterval)age +{ + dispatch_block_t block = ^{ @autoreleasepool { + + NSTimeInterval oldMaxMessageAge = maxMessageAge; + NSTimeInterval newMaxMessageAge = age; + + maxMessageAge = age; + + // There are several cases we need to handle here. + // + // 1. If the maxAge was previously enabled and it just got disabled, + // then we need to stop the deleteTimer. (And we might as well release it.) + // + // 2. If the maxAge was previously disabled and it just got enabled, + // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) + // + // 3. If the maxAge was increased, + // then we don't need to do anything. + // + // 4. If the maxAge was decreased, + // then we should do an immediate delete. + + BOOL shouldDeleteNow = NO; + + if (oldMaxMessageAge > 0.0) + { + if (newMaxMessageAge <= 0.0) + { + // Handles #1 + [self destroyDeleteTimer]; + } + else if (oldMaxMessageAge > newMaxMessageAge) + { + // Handles #4 + shouldDeleteNow = YES; + } + else + { + // Handles #3 + // Nothing to do now + } + } + else if (newMaxMessageAge > 0.0) + { + // Handles #2 + shouldDeleteNow = YES; + } + + if (shouldDeleteNow) + { + [self performDelete]; + + if (deleteTimer) + [self updateDeleteTimer]; + else + [self createAndStartDeleteTimer]; + } + }}; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_async(storageQueue, block); +} + +- (NSTimeInterval)deleteInterval +{ + __block NSTimeInterval result = 0; + + dispatch_block_t block = ^{ + result = deleteInterval; + }; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_sync(storageQueue, block); + + return result; +} + +- (void)setDeleteInterval:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ @autoreleasepool { + + deleteInterval = interval; + + // There are several cases we need to handle here. + // + // 1. If the deleteInterval was previously enabled and it just got disabled, + // then we need to stop the deleteTimer. (And we might as well release it.) + // + // 2. If the deleteInterval was previously disabled and it just got enabled, + // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) + // + // 3. If the deleteInterval increased, then we need to reset the timer so that it fires at the later date. + // + // 4. If the deleteInterval decreased, then we need to reset the timer so that it fires at an earlier date. + // (Plus we might need to do an immediate delete.) + + if (deleteInterval > 0.0) + { + if (deleteTimer == NULL) + { + // Handles #2 + // + // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, + // if a delete is needed the timer will fire immediately. + + [self createAndStartDeleteTimer]; + } + else + { + // Handles #3 + // Handles #4 + // + // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self updateDeleteTimer]; + } + } + else if (deleteTimer) + { + // Handles #1 + + [self destroyDeleteTimer]; + } + }}; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_async(storageQueue, block); +} + +- (void)pauseOldMessageDeletionForRoom:(XMPPJID *)roomJID +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [pausedMessageDeletion addObject:[roomJID bareJID]]; + }}; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_async(storageQueue, block); +} + +- (void)resumeOldMessageDeletionForRoom:(XMPPJID *)roomJID +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [pausedMessageDeletion removeObject:[roomJID bareJID]]; + [self performDelete]; + }}; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_async(storageQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Internal API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)performDelete +{ + if (maxMessageAge <= 0.0) return; + + NSDate *minLocalTimestamp = [NSDate dateWithTimeIntervalSinceNow:(maxMessageAge * -1.0)]; + + NSManagedObjectContext *moc = [self managedObjectContext]; + NSEntityDescription *messageEntity = [self messageEntity:moc]; + + NSPredicate *predicate; + if ([pausedMessageDeletion count] > 0) + { + predicate = [NSPredicate predicateWithFormat:@"localTimestamp <= %@ AND roomJIDStr NOT IN %@", + minLocalTimestamp, pausedMessageDeletion]; + } + else + { + predicate = [NSPredicate predicateWithFormat:@"localTimestamp <= %@", minLocalTimestamp]; + } + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:messageEntity]; + [fetchRequest setPredicate:predicate]; + [fetchRequest setFetchBatchSize:saveThreshold]; + + NSError *error = nil; + NSArray *oldMessages = [moc executeFetchRequest:fetchRequest error:&error]; + + if (error) + { + XMPPLogWarn(@"%@: %@ - fetch error: %@", THIS_FILE, THIS_METHOD, error); + } + + NSUInteger unsavedCount = [self numberOfUnsavedChanges]; + + for (XMPPRoomMessageHybridCoreDataStorageObject *oldMessage in oldMessages) + { + [moc deleteObject:oldMessage]; + + if (++unsavedCount >= saveThreshold) + { + [self save]; + unsavedCount = 0; + } + } + + lastDeleteTime = dispatch_time(DISPATCH_TIME_NOW, 0); +} + +- (void)destroyDeleteTimer +{ + if (deleteTimer) + { + dispatch_source_cancel(deleteTimer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(deleteTimer); + #endif + deleteTimer = NULL; + } +} + +- (void)updateDeleteTimer +{ + if ((deleteTimer != NULL) && (deleteInterval > 0.0) && (maxMessageAge > 0.0)) + { + uint64_t interval = deleteInterval * NSEC_PER_SEC; + dispatch_time_t startTime; + + if (lastDeleteTime > 0) + startTime = dispatch_time(lastDeleteTime, interval); + else + startTime = dispatch_time(DISPATCH_TIME_NOW, interval); + + dispatch_source_set_timer(deleteTimer, startTime, interval, 1.0); + } +} + +- (void)createAndStartDeleteTimer +{ + if ((deleteTimer == NULL) && (deleteInterval > 0.0) && (maxMessageAge > 0.0)) + { + deleteTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, storageQueue); + + dispatch_source_set_event_handler(deleteTimer, ^{ @autoreleasepool { + + [self performDelete]; + + }}); + + [self updateDeleteTimer]; + + if (deleteTimer) + dispatch_resume(deleteTimer); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Protected API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Optional override hook. +**/ +- (BOOL)existsMessage:(XMPPMessage *)message forRoom:(XMPPRoom *)room stream:(XMPPStream *)xmppStream +{ + NSDate *remoteTimestamp = [message delayedDeliveryDate]; + + if (remoteTimestamp == nil) + { + // When the xmpp server sends us a room message, it will always timestamp delayed messages. + // For example, when retrieving the discussion history, all messages will include the original timestamp. + // If a message doesn't include such timestamp, then we know we're getting it in "real time". + + return NO; + } + + // Does this message already exist in the database? + // How can we tell if two XMPPRoomMessages are the same? + // + // 1. Same streamBareJidStr + // 2. Same jid + // 3. Same text + // 4. Approximately the same timestamps + // + // This is actually a rather difficult question. + // What if the same user sends the exact same message multiple times? + // + // If we first received the message while already in the room, it won't contain a remoteTimestamp. + // Returning to the room later and downloading the discussion history will return the same message, + // this time with a remote timestamp. + // + // So if the message doesn't have a remoteTimestamp, + // but it's localTimestamp is approximately the same as the remoteTimestamp, + // then this is enough evidence to consider the messages the same. + // + // Note: Predicate order matters. Most unique key should be first, least unique should be last. + + NSManagedObjectContext *moc = [self managedObjectContext]; + NSEntityDescription *messageEntity = [self messageEntity:moc]; + + NSString *streamBareJidStr = [[self myJIDForXMPPStream:xmppStream] bare]; + + XMPPJID *messageJID = [message from]; + NSString *messageBody = [[message elementForName:@"body"] stringValue]; + + NSDate *minLocalTimestamp = [remoteTimestamp dateByAddingTimeInterval:-60]; + NSDate *maxLocalTimestamp = [remoteTimestamp dateByAddingTimeInterval: 60]; + + NSString *predicateFormat = @" body == %@ " + @"AND jidStr == %@ " + @"AND streamBareJidStr == %@ " + @"AND " + @"(" + @" (remoteTimestamp == %@) " + @" OR (remoteTimestamp == NIL && localTimestamp BETWEEN {%@, %@})" + @")"; + + NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateFormat, + messageBody, messageJID, streamBareJidStr, + remoteTimestamp, minLocalTimestamp, maxLocalTimestamp]; + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:messageEntity]; + [fetchRequest setPredicate:predicate]; + [fetchRequest setFetchLimit:1]; + + NSError *error = nil; + NSArray *results = [moc executeFetchRequest:fetchRequest error:&error]; + + if (error) + { + XMPPLogError(@"%@: %@ - Fetch error: %@", THIS_FILE, THIS_METHOD, error); + } + + return ([results count] > 0); +} + +/** + * Optional override hook for general extensions. + * + * @see insertMessage:outgoing:forRoom:stream: +**/ +- (void)didInsertMessage:(XMPPRoomMessageHybridCoreDataStorageObject *)message +{ + // Override me if you're extending the XMPPRoomMessageHybridCoreDataStorageObject class + // to add additional properties, which you can set here. + // + // At this point the standard properties have already been set. + // So you can, for example, access the XMPPMessage via message.message. +} + +/** + * Optional override hook for complete customization. + * Override me if you need to do specific custom work when inserting a message in a room. + * + * @see didInsertMessage: +**/ +- (void)insertMessage:(XMPPMessage *)message + outgoing:(BOOL)isOutgoing + forRoom:(XMPPRoom *)room + stream:(XMPPStream *)xmppStream +{ + // Extract needed information + + XMPPJID *roomJID = room.roomJID; + XMPPJID *messageJID = isOutgoing ? room.myRoomJID : [message from]; + + NSDate *localTimestamp; + NSDate *remoteTimestamp; + + if (isOutgoing) + { + localTimestamp = [[NSDate alloc] init]; + remoteTimestamp = nil; + } + else + { + remoteTimestamp = [message delayedDeliveryDate]; + if (remoteTimestamp) { + localTimestamp = remoteTimestamp; + } + else { + localTimestamp = [[NSDate alloc] init]; + } + } + + NSString *messageBody = [[message elementForName:@"body"] stringValue]; + + NSManagedObjectContext *moc = [self managedObjectContext]; + NSString *streamBareJidStr = [[self myJIDForXMPPStream:xmppStream] bare]; + + NSEntityDescription *messageEntity = [self messageEntity:moc]; + + // Add to database + + XMPPRoomMessageHybridCoreDataStorageObject *roomMessage = (XMPPRoomMessageHybridCoreDataStorageObject *) + [[NSManagedObject alloc] initWithEntity:messageEntity insertIntoManagedObjectContext:nil]; + + roomMessage.message = message; + roomMessage.roomJID = roomJID; + roomMessage.jid = messageJID; + roomMessage.nickname = [messageJID resource]; + roomMessage.body = messageBody; + roomMessage.localTimestamp = localTimestamp; + roomMessage.remoteTimestamp = remoteTimestamp; + roomMessage.isFromMe = isOutgoing; + roomMessage.streamBareJidStr = streamBareJidStr; + + [moc insertObject:roomMessage]; // Hook if subclassing XMPPRoomMessageHybridCDSO (awakeFromInsert) + [self didInsertMessage:roomMessage]; // Hook if subclassing XMPPRoomHybridStorage +} + +/** + * Optional override hook for general extensions. + * + * @see insertOccupantWithPresence:room:stream: +**/ +- (void)didInsertOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant +{ + // Override me if you're extending the XMPPRoomOccupantHybridMemoryStorageObject class + // to add additional properties, which you can set here. + // + // At this point the standard properties have already been set. + // So you can, for example, access the XMPPPresence via occupant.presece. +} + +/** + * Optional override hook for general extensions. + * + * @see updateOccupant:withPresence:room:stream: +**/ +- (void)didUpdateOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant +{ + // Override me if you're extending the XMPPRoomOccupantHybridMemoryStorageObject class, + // and you have additional properties that may need to be updated. + // + // At this point the standard properties have already been updated. +} + +/** + * Optional override hook for general extensions. +**/ +- (void)willRemoveOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant +{ + // Override me if you have any custom work to do before an occupant leaves (is removed from storage). +} + +/** + * Optional override hook for general extensions. +**/ +- (void)didRemoveOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant +{ + // Override me if you have any custom work to do after an occupant leaves (is removed from storage). +} + +/** + * Optional override hook for complete customization. + * Override me if you need to do custom work when inserting an occupant in a room. +**/ +- (XMPPRoomOccupantHybridMemoryStorageObject *)insertOccupantWithPresence:(XMPPPresence *)presence + room:(XMPPRoom *)room + stream:(XMPPStream *)xmppStream +{ + XMPPJID *streamFullJid = [self myJIDForXMPPStream:xmppStream]; + XMPPJID *roomJid = room.roomJID; + + NSMutableDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid]; + if (occupantsRoomsDict == nil) + { + occupantsRoomsDict = [[NSMutableDictionary alloc] init]; + occupantsGlobalDict[streamFullJid] = occupantsRoomsDict; + } + + NSMutableDictionary *occupantsRoomDict = occupantsRoomsDict[roomJid]; + if (occupantsRoomDict == nil) + { + occupantsRoomDict = [[NSMutableDictionary alloc] init]; + occupantsRoomsDict[roomJid] = occupantsRoomDict; + } + + XMPPRoomOccupantHybridMemoryStorageObject *occupant = (XMPPRoomOccupantHybridMemoryStorageObject *) + [[self.occupantClass alloc] initWithPresence:presence streamFullJid:streamFullJid]; + + occupantsRoomDict[occupant.jid] = occupant; + + return occupant; +} + +/** + * Optional override hook for complete customization. + * Override me if you need to do custom work when updating an occupant in a room. +**/ +- (void)updateOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant + withPresence:(XMPPPresence *)presence + room:(XMPPRoom *)room + stream:(XMPPStream *)stream +{ + + [occupant updateWithPresence:presence]; +} + +/** + * Optional override hook for complete customization. + * Override me if you need to do custom work when removing an occupant from a room. +**/ +- (void)removeOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant + withPresence:(XMPPPresence *)presence + room:(XMPPRoom *)room + stream:(XMPPStream *)stream +{ + // Remove from dictionary + + XMPPJID *streamFullJid = [self myJIDForXMPPStream:stream]; + XMPPJID *roomJid = occupant.roomJID; + + NSMutableDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid]; + NSMutableDictionary *occupantsRoomDict = occupantsRoomsDict[roomJid]; + + [occupantsRoomDict removeObjectForKey:occupant.jid]; // Remove occupant + if ([occupantsRoomDict count] == 0) + { + [occupantsRoomsDict removeObjectForKey:roomJid]; // Remove room if now empty + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Public API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSEntityDescription *)messageEntity:(NSManagedObjectContext *)moc +{ + // This method should be thread-safe. + // So be sure to access the entity name through the property accessor. + + if (moc == nil) + { + XMPPLogWarn(@"%@: %@ - Invalid parameter, moc is nil", THIS_FILE, THIS_METHOD); + return nil; + } + + NSString *entityName = self.messageEntityName; + return [NSEntityDescription entityForName:entityName inManagedObjectContext:moc]; +} + +- (NSDate *)mostRecentMessageTimestampForRoom:(XMPPJID *)roomJID + stream:(XMPPStream *)xmppStream + inContext:(NSManagedObjectContext *)inMoc +{ + if (roomJID == nil) return nil; + + // It's possible to use our internal managedObjectContext only because we're not returning a NSManagedObject. + + __block NSDate *result = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + NSManagedObjectContext *moc = inMoc ? : [self managedObjectContext]; + + NSEntityDescription *entity = [self messageEntity:moc]; + + NSPredicate *predicate; + if (xmppStream) + { + NSString *streamBareJidStr = [[self myJIDForXMPPStream:xmppStream] bare]; + + NSString *predicateFormat = @"roomJIDStr == %@ AND streamBareJidStr == %@"; + predicate = [NSPredicate predicateWithFormat:predicateFormat, roomJID, streamBareJidStr]; + } + else + { + predicate = [NSPredicate predicateWithFormat:@"roomJIDStr == %@", roomJID]; + } + + NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"localTimestamp" ascending:NO]; + NSArray *sortDescriptors = @[sortDescriptor]; + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:entity]; + [fetchRequest setPredicate:predicate]; + [fetchRequest setSortDescriptors:sortDescriptors]; + [fetchRequest setFetchLimit:1]; + + NSError *error = nil; + XMPPRoomMessageHybridCoreDataStorageObject *message = (XMPPRoomMessageHybridCoreDataStorageObject *) + [[moc executeFetchRequest:fetchRequest error:&error] lastObject]; + + if (error) + { + XMPPLogError(@"%@: %@ - fetchRequest error: %@", THIS_FILE, THIS_METHOD, error); + } + else + { + result = [message.localTimestamp copy]; + } + }}; + + if (inMoc == nil) + dispatch_sync(storageQueue, block); + else + block(); + + return result; +} + +- (XMPPRoomOccupantHybridMemoryStorageObject *)occupantForJID:(XMPPJID *)occupantJid stream:(XMPPStream *)xmppStream +{ + if (occupantJid == nil) return nil; + + __block XMPPRoomOccupantHybridMemoryStorageObject *occupant = nil; + + void (^block)(BOOL) = ^(BOOL shouldCopy){ @autoreleasepool { + + XMPPJID *roomJid = [occupantJid bareJID]; + + if (xmppStream) + { + XMPPJID *streamFullJid = [self myJIDForXMPPStream:xmppStream]; + + NSDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid]; + NSDictionary *occupantsRoomDict = occupantsRoomsDict[roomJid]; + + occupant = occupantsRoomDict[occupantJid]; + } + else + { + for (XMPPJID *streamFullJid in occupantsGlobalDict) + { + NSDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid]; + NSDictionary *occupantsRoomDict = occupantsRoomsDict[roomJid]; + + occupant = occupantsRoomDict[occupantJid]; + if (occupant) break; + } + } + + if (shouldCopy) + { + occupant = [occupant copy]; + } + }}; + + if (dispatch_get_specific(storageQueueTag)) + block(NO); + else + dispatch_sync(storageQueue, ^{ block(YES); }); + + return occupant; +} + +- (NSArray *)occupantsForRoom:(XMPPJID *)roomJid stream:(XMPPStream *)xmppStream +{ + roomJid = [roomJid bareJID]; // Just in case a full jid is accidentally passed + + __block NSArray *results = nil; + + void (^block)(BOOL) = ^(BOOL shouldCopy){ @autoreleasepool { + + if (xmppStream) + { + XMPPJID *streamFullJid = [self myJIDForXMPPStream:xmppStream]; + + NSDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid]; + NSDictionary *occupantsRoomDict = occupantsRoomsDict[roomJid]; + + results = [occupantsRoomDict allValues]; + } + else + { + for (XMPPJID *streamFullJid in occupantsGlobalDict) + { + NSDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid]; + NSDictionary *occupantsRoomDict = occupantsRoomsDict[roomJid]; + + if (occupantsRoomDict) + { + results = [occupantsRoomDict allValues]; + break; + } + } + } + + if (shouldCopy) + { + NSArray *temp = results; + results = [[NSArray alloc] initWithArray:temp copyItems:YES]; + } + }}; + + if (dispatch_get_specific(storageQueueTag)) + block(NO); + else + dispatch_sync(storageQueue, ^{ block(YES); }); + + return results; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPRoomStorage Protocol +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)handlePresence:(XMPPPresence *)presence room:(XMPPRoom *)room +{ + XMPPLogTrace(); + + dispatch_queue_t roomQueue = room.moduleQueue; + XMPPStream *xmppStream = room.xmppStream; + + [self scheduleBlock:^{ + + XMPPJID *from = [presence from]; + + if ([[presence type] isEqualToString:@"unavailable"]) + { + XMPPRoomOccupantHybridMemoryStorageObject *occupant = [self occupantForJID:from stream:xmppStream]; + if (occupant) + { + // Occupant did leave - remove + + [self willRemoveOccupant:occupant]; + [self removeOccupant:occupant withPresence:presence room:room stream:xmppStream]; + [self didRemoveOccupant:occupant]; + + // Notify delegate(s) + + XMPPRoomOccupantHybridMemoryStorageObject *occupantCopy = [occupant copy]; + dispatch_async(roomQueue, ^{ @autoreleasepool { + + GCDMulticastDelegate *roomMulticastDelegate = + (GCDMulticastDelegate *)[room multicastDelegate]; + + [roomMulticastDelegate xmppRoomHybridStorage:self + occupantDidLeave:occupantCopy]; + }}); + } + } + else + { + XMPPRoomOccupantHybridMemoryStorageObject *occupant = [self occupantForJID:from stream:xmppStream]; + if (occupant == nil) + { + // Occupant did join - add + + occupant = [self insertOccupantWithPresence:presence room:room stream:xmppStream]; + if (occupant == nil) + { + // Subclasses may choose to ignore occupants for whatever reason. + return; + } + + [self didInsertOccupant:occupant]; + + // Notify delegate(s) + + XMPPRoomOccupantHybridMemoryStorageObject *occupantCopy = [occupant copy]; + dispatch_async(roomQueue, ^{ @autoreleasepool { + + GCDMulticastDelegate *roomMulticastDelegate = + (GCDMulticastDelegate *)[room multicastDelegate]; + + [roomMulticastDelegate xmppRoomHybridStorage:self + occupantDidJoin:occupantCopy]; + }}); + } + else + { + // Occupant did update - move + + [self updateOccupant:occupant withPresence:presence room:room stream:xmppStream]; + [self didUpdateOccupant:occupant]; + + // Notify delegate(s) + + XMPPRoomOccupantHybridMemoryStorageObject *occupantCopy = [occupant copy]; + dispatch_async(roomQueue, ^{ @autoreleasepool { + + GCDMulticastDelegate *roomMulticastDelegate = + (GCDMulticastDelegate *)[room multicastDelegate]; + + [roomMulticastDelegate xmppRoomHybridStorage:self + occupantDidUpdate:occupantCopy]; + }}); + } + } + }]; +} + +- (void)handleOutgoingMessage:(XMPPMessage *)message room:(XMPPRoom *)room +{ + XMPPLogTrace(); + + XMPPStream *xmppStream = room.xmppStream; + + [self scheduleBlock:^{ + + [self insertMessage:message outgoing:YES forRoom:room stream:xmppStream]; + }]; +} + +- (void)handleIncomingMessage:(XMPPMessage *)message room:(XMPPRoom *)room +{ + XMPPLogTrace(); + + XMPPJID *myRoomJID = room.myRoomJID; + XMPPJID *messageJID = [message from]; + + if ([myRoomJID isEqualToJID:messageJID]) + { + if (![message wasDelayed]) + { + // Ignore - we already stored message in handleOutgoingMessage:room: + return; + } + } + + XMPPStream *xmppStream = room.xmppStream; + + [self scheduleBlock:^{ + + if ([self existsMessage:message forRoom:room stream:xmppStream]) + { + XMPPLogVerbose(@"%@: %@ - Duplicate message", THIS_FILE, THIS_METHOD); + } + else + { + [self insertMessage:message outgoing:NO forRoom:room stream:xmppStream]; + } + }]; +} + +- (void)handleDidLeaveRoom:(XMPPRoom *)room +{ + XMPPLogTrace(); + + XMPPJID *roomJid = room.roomJID; + XMPPStream *xmppStream = room.xmppStream; + + [self scheduleBlock:^{ + + XMPPJID *streamFullJid = [self myJIDForXMPPStream:xmppStream]; + + NSMutableDictionary *occupantsRoomsDict = occupantsGlobalDict[streamFullJid]; + + [occupantsRoomsDict removeObjectForKey:roomJid]; // Remove room (and all associated occupants) + }]; +} + +@end diff --git a/Extensions/XEP-0045/HybridStorage/XMPPRoomHybridStorageProtected.h b/Extensions/XEP-0045/HybridStorage/XMPPRoomHybridStorageProtected.h new file mode 100644 index 0000000..e5b2c94 --- /dev/null +++ b/Extensions/XEP-0045/HybridStorage/XMPPRoomHybridStorageProtected.h @@ -0,0 +1,91 @@ +/** + * The XMPPRoomHybridStorage class is designed to be easily extensible. + * The class has several protected methods that act as hooks, + * allowing you to override various methods to customize the functionality how you see fit. + * + * This header file lists the protected methods, + * and you may need to import it in your subclass if you ever need to invoke these methods directly. + * + * E.g. [super insertOccupant...] +**/ + +@interface XMPPRoomHybridStorage (Protected) + +/** + * Returns whether or not the given message already exists in storage. + * If YES, then the message is ignored. Otherwise it is passed to the insert routines. +**/ +- (BOOL)existsMessage:(XMPPMessage *)message forRoom:(XMPPRoom *)room stream:(XMPPStream *)xmppStream; + +/** + * Override me if you're extending the XMPPRoomMessageHybridCoreDataStorageObject class + * to add additional properties, which you can set here. + * + * At this point the standard properties have already been set. + * So you can, for example, access the XMPPMessage via message.message. +**/ +- (void)didInsertMessage:(XMPPRoomMessageHybridCoreDataStorageObject *)message; + +/** + * Optional override hook for complete customization. + * Override me if you need to do specific custom work when inserting a message in a room. +**/ +- (void)insertMessage:(XMPPMessage *)message + outgoing:(BOOL)isOutgoing + forRoom:(XMPPRoom *)room + stream:(XMPPStream *)xmppStream; + +/** + * Override me if you're extending the XMPPRoomOccupantHybridMemoryStorageObject class + * to add additional properties, which you can set here. + * + * At this point the standard properties have already been set. + * So you can, for example, access the XMPPPresence via occupant.presece. +**/ +- (void)didInsertOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant; + +/** + * Override me if you're extending the XMPPRoomOccupantHybridMemoryStorageObject class, + * and you have additional properties that may need to be updated. + * + * At this point the standard properties have already been updated. +**/ +- (void)didUpdateOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant; + +/** + * Override me if you have any custom work to do before an occupant leaves (is removed from storage). +**/ +- (void)willRemoveOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant; + +/** + * Override me if you have any custom work to do after an occupant leaves (is removed from storage). +**/ +- (void)didRemoveOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant; + +/** + * Optional override hook for complete customization. + * Override me if you need to do custom work when inserting an occupant in a room. +**/ +- (XMPPRoomOccupantHybridMemoryStorageObject *)insertOccupantWithPresence:(XMPPPresence *)presence + room:(XMPPRoom *)room + stream:(XMPPStream *)xmppStream; + +/** + * Optional override hook for complete customization. + * Override me if you need to do custom work when updating an occupant in a room. +**/ +- (void)updateOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant + withPresence:(XMPPPresence *)presence + room:(XMPPRoom *)room + stream:(XMPPStream *)stream; + +/** + * Optional override hook for complete customization. + * Override me if you need to do custom work when removing an occupant from a room. +**/ +- (void)removeOccupant:(XMPPRoomOccupantHybridMemoryStorageObject *)occupant + withPresence:(XMPPPresence *)presence + room:(XMPPRoom *)room + stream:(XMPPStream *)stream; + +@end diff --git a/Extensions/XEP-0045/HybridStorage/XMPPRoomMessageHybridCoreDataStorageObject.h b/Extensions/XEP-0045/HybridStorage/XMPPRoomMessageHybridCoreDataStorageObject.h new file mode 100644 index 0000000..943d2ee --- /dev/null +++ b/Extensions/XEP-0045/HybridStorage/XMPPRoomMessageHybridCoreDataStorageObject.h @@ -0,0 +1,47 @@ +#import +#import + +#import "XMPP.h" +#import "XMPPRoom.h" + + +@interface XMPPRoomMessageHybridCoreDataStorageObject : NSManagedObject + +/** + * The properties below are documented in the XMPPRoomMessage protocol. +**/ + +@property (nonatomic, retain) XMPPMessage * message; // Transient (proper type, not on disk) +@property (nonatomic, retain) NSString * messageStr; // Shadow (binary data, written to disk) + +@property (nonatomic, strong) XMPPJID * roomJID; // Transient (proper type, not on disk) +@property (nonatomic, strong) NSString * roomJIDStr; // Shadow (binary data, written to disk) + +@property (nonatomic, retain) XMPPJID * jid; // Transient (proper type, not on disk) +@property (nonatomic, retain) NSString * jidStr; // Shadow (binary data, written to disk) + +@property (nonatomic, retain) NSString * nickname; +@property (nonatomic, retain) NSString * body; + +@property (nonatomic, retain) NSDate * localTimestamp; +@property (nonatomic, strong) NSDate * remoteTimestamp; + +@property (nonatomic, assign) BOOL isFromMe; +@property (nonatomic, strong) NSNumber * fromMe; + +/** + * The 'type' property can be used to inject event messages. + * For example: "JohnDoe entered the room". + * + * You can define your own types to suit your needs. + * All normal messages will have a type of zero. +**/ +@property (nonatomic, strong) NSNumber * type; + +/** + * If a single instance of XMPPRoomHybridStorage is shared between multiple xmppStream's, + * this may be needed to distinguish between the streams. +**/ +@property (nonatomic, strong) NSString *streamBareJidStr; + +@end diff --git a/Extensions/XEP-0045/HybridStorage/XMPPRoomMessageHybridCoreDataStorageObject.m b/Extensions/XEP-0045/HybridStorage/XMPPRoomMessageHybridCoreDataStorageObject.m new file mode 100644 index 0000000..b64d032 --- /dev/null +++ b/Extensions/XEP-0045/HybridStorage/XMPPRoomMessageHybridCoreDataStorageObject.m @@ -0,0 +1,206 @@ +#import "XMPPRoomMessageHybridCoreDataStorageObject.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@interface XMPPRoomMessageHybridCoreDataStorageObject () + +@property(nonatomic,strong) XMPPJID * primitiveRoomJID; +@property(nonatomic,strong) NSString * primitiveRoomJIDStr; + +@property(nonatomic,strong) XMPPJID * primitiveJid; +@property(nonatomic,strong) NSString * primitiveJidStr; + +@property(nonatomic,strong) XMPPMessage * primitiveMessage; +@property(nonatomic,strong) NSString * primitiveMessageStr; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPRoomMessageHybridCoreDataStorageObject + +@dynamic message; +@dynamic messageStr; + +@dynamic roomJID, primitiveRoomJID; +@dynamic roomJIDStr, primitiveRoomJIDStr; + +@dynamic jid, primitiveJid; +@dynamic jidStr, primitiveJidStr; + +@dynamic nickname; +@dynamic body; +@dynamic localTimestamp; +@dynamic remoteTimestamp; +@dynamic isFromMe; +@dynamic fromMe; + +@dynamic type; + +@dynamic streamBareJidStr; + +@dynamic primitiveMessage; +@dynamic primitiveMessageStr; + +#pragma mark Transient roomJID + +- (XMPPJID *)roomJID +{ + // Create and cache on demand + + [self willAccessValueForKey:@"roomJID"]; + XMPPJID *tmp = self.primitiveRoomJID; + [self didAccessValueForKey:@"roomJID"]; + + if (tmp == nil) + { + NSString *roomJIDStr = self.roomJIDStr; + if (roomJIDStr) + { + tmp = [XMPPJID jidWithString:roomJIDStr]; + self.primitiveRoomJID = tmp; + } + } + + return tmp; +} + +- (void)setRoomJID:(XMPPJID *)roomJID +{ + [self willChangeValueForKey:@"roomJID"]; + [self willChangeValueForKey:@"roomJIDStr"]; + + self.primitiveRoomJID = roomJID; + self.primitiveRoomJIDStr = [roomJID full]; + + [self didChangeValueForKey:@"roomJID"]; + [self didChangeValueForKey:@"roomJIDStr"]; +} + +- (void)setRoomJIDStr:(NSString *)roomJIDStr +{ + [self willChangeValueForKey:@"roomJID"]; + [self willChangeValueForKey:@"roomJIDStr"]; + + self.primitiveRoomJID = [XMPPJID jidWithString:roomJIDStr]; + self.primitiveRoomJIDStr = roomJIDStr; + + [self didChangeValueForKey:@"roomJID"]; + [self didChangeValueForKey:@"roomJIDStr"]; +} + +#pragma mark Transient jid + +- (XMPPJID *)jid +{ + // Create and cache on demand + + [self willAccessValueForKey:@"jid"]; + XMPPJID *tmp = self.primitiveJid; + [self didAccessValueForKey:@"jid"]; + + if (tmp == nil) + { + NSString *jidStr = self.jidStr; + if (jidStr) + { + tmp = [XMPPJID jidWithString:jidStr]; + self.primitiveJid = tmp; + } + } + + return tmp; +} + +- (void)setJid:(XMPPJID *)jid +{ + [self willChangeValueForKey:@"jid"]; + [self willChangeValueForKey:@"jidStr"]; + + self.primitiveJid = jid; + self.primitiveJidStr = [jid full]; + + [self didChangeValueForKey:@"jid"]; + [self didChangeValueForKey:@"jidStr"]; +} + +- (void)setJidStr:(NSString *)jidStr +{ + [self willChangeValueForKey:@"jid"]; + [self willChangeValueForKey:@"jidStr"]; + + self.primitiveJid = [XMPPJID jidWithString:jidStr]; + self.primitiveJidStr = jidStr; + + [self didChangeValueForKey:@"jid"]; + [self didChangeValueForKey:@"jidStr"]; +} + +#pragma mark Scalar + +- (BOOL)isFromMe +{ + return [[self fromMe] boolValue]; +} + +- (void)setIsFromMe:(BOOL)value +{ + self.fromMe = @(value); +} + +#pragma mark - Message +- (XMPPMessage *)message +{ + // Create and cache on demand + [self willAccessValueForKey:@"message"]; + + XMPPMessage *message = self.primitiveMessage; + + [self didAccessValueForKey:@"message"]; + + if (message == nil) + { + NSString *messageStr = self.messageStr; + if (messageStr) + { + NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString: + messageStr error:nil]; + message = [XMPPMessage messageFromElement:element]; + self.primitiveMessage = message; + } + } + + return message; +} + +- (void)setMessage:(XMPPMessage *)message +{ + [self willChangeValueForKey:@"message"]; + [self willChangeValueForKey:@"messageStr"]; + + self.primitiveMessage = message; + self.primitiveMessageStr = [message compactXMLString]; + + [self didChangeValueForKey:@"message"]; + [self didChangeValueForKey:@"messageStr"]; +} + +- (void)setMessageStr:(NSString *)messageStr +{ + [self willChangeValueForKey:@"message"]; + [self willChangeValueForKey:@"messageStr"]; + + NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString:messageStr error:nil]; + self.primitiveMessage = [XMPPMessage messageFromElement:element]; + self.primitiveMessageStr = messageStr; + + [self didChangeValueForKey:@"message"]; + [self didChangeValueForKey:@"messageStr"]; +} + +@end diff --git a/Extensions/XEP-0045/HybridStorage/XMPPRoomOccupantHybridMemoryStorageObject.h b/Extensions/XEP-0045/HybridStorage/XMPPRoomOccupantHybridMemoryStorageObject.h new file mode 100644 index 0000000..65c37c6 --- /dev/null +++ b/Extensions/XEP-0045/HybridStorage/XMPPRoomOccupantHybridMemoryStorageObject.h @@ -0,0 +1,41 @@ +#import +#import "XMPPRoom.h" +#import "XMPPRoomOccupant.h" + + +@interface XMPPRoomOccupantHybridMemoryStorageObject : NSObject + +- (id)initWithPresence:(XMPPPresence *)presence streamFullJid:(XMPPJID *)streamFullJid; +- (void)updateWithPresence:(XMPPPresence *)presence; + +/** + * The properties below are documented in the XMPPRoomOccupant protocol. +**/ + +@property (readonly) XMPPPresence *presence; + +@property (readonly) XMPPJID * jid; +@property (readonly) XMPPJID * roomJID; +@property (readonly) NSString * nickname; + +@property (readonly) NSString * role; +@property (readonly) NSString * affiliation; +@property (readonly) XMPPJID * realJID; + +@property (readonly) NSDate * createdAt; + +/** + * Since XMPPRoomHybridStorage supports multiple xmppStreams, + * this property may be used to differentiate between occupant objects. +**/ +@property (readonly) XMPPJID * streamFullJid; + +/** + * Sample comparison methods. +**/ + +- (NSComparisonResult)compareByNickname:(XMPPRoomOccupantHybridMemoryStorageObject *)another; + +- (NSComparisonResult)compareByCreatedAt:(XMPPRoomOccupantHybridMemoryStorageObject *)another; + +@end diff --git a/Extensions/XEP-0045/HybridStorage/XMPPRoomOccupantHybridMemoryStorageObject.m b/Extensions/XEP-0045/HybridStorage/XMPPRoomOccupantHybridMemoryStorageObject.m new file mode 100644 index 0000000..965b224 --- /dev/null +++ b/Extensions/XEP-0045/HybridStorage/XMPPRoomOccupantHybridMemoryStorageObject.m @@ -0,0 +1,237 @@ +#import "XMPPRoomOccupantHybridMemoryStorageObject.h" + + +@implementation XMPPRoomOccupantHybridMemoryStorageObject +{ + XMPPPresence *presence; + XMPPJID *jid; + NSDate *createdAt; + XMPPJID *streamFullJid; +} + +- (id)initWithPresence:(XMPPPresence *)inPresence streamFullJid:(XMPPJID *)inStreamFullJid +{ + NSParameterAssert(inPresence != nil); + NSParameterAssert(inStreamFullJid != nil); + + if ((self = [super init])) + { + presence = inPresence; + jid = [presence from]; + createdAt = [[NSDate alloc] init]; + streamFullJid = inStreamFullJid; + } + return self; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Encoding, Decoding +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if ! TARGET_OS_IPHONE +- (id)replacementObjectForPortCoder:(NSPortCoder *)encoder +{ + if ([encoder isBycopy]) + return self; + else + return [super replacementObjectForPortCoder:encoder]; + // return [NSDistantObject proxyWithLocal:self connection:[encoder connection]]; +} +#endif + +- (id)initWithCoder:(NSCoder *)coder +{ + if ((self = [super init])) + { + if ([coder allowsKeyedCoding]) + { + if([coder respondsToSelector:@selector(requiresSecureCoding)] && + [coder requiresSecureCoding]) + { + presence = [coder decodeObjectOfClass:[XMPPPresence class] forKey:@"presence"]; + jid = [coder decodeObjectOfClass:[XMPPJID class] forKey:@"jid"]; + createdAt = [coder decodeObjectOfClass:[NSDate class] forKey:@"createdAt"]; + streamFullJid = [coder decodeObjectOfClass:[XMPPJID class] forKey:@"streamFullJid"]; + } + else + { + presence = [coder decodeObjectForKey:@"presence"]; + jid = [coder decodeObjectForKey:@"jid"]; + createdAt = [coder decodeObjectForKey:@"createdAt"]; + streamFullJid = [coder decodeObjectForKey:@"streamFullJid"]; + } + } + else + { + presence = [coder decodeObject]; + jid = [coder decodeObject]; + createdAt = [coder decodeObject]; + streamFullJid = [coder decodeObject]; + } + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + if ([coder allowsKeyedCoding]) + { + [coder encodeObject:presence forKey:@"presence"]; + [coder encodeObject:jid forKey:@"jid"]; + [coder encodeObject:createdAt forKey:@"createdAt"]; + [coder encodeObject:streamFullJid forKey:@"streamFullJid"]; + } + else + { + [coder encodeObject:presence]; + [coder encodeObject:jid]; + [coder encodeObject:createdAt]; + [coder encodeObject:streamFullJid]; + } +} + ++ (BOOL) supportsSecureCoding +{ + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Copying +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)copyWithZone:(NSZone *)zone +{ + // We use [self class] to support subclassing + + XMPPRoomOccupantHybridMemoryStorageObject *deepCopy; + deepCopy = (XMPPRoomOccupantHybridMemoryStorageObject *)[[[self class] alloc] init]; + + deepCopy->presence = [presence copy]; + deepCopy->jid = [jid copy]; + deepCopy->createdAt = [createdAt copy]; + deepCopy->streamFullJid = [streamFullJid copy]; + + return deepCopy; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Updates +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)updateWithPresence:(XMPPPresence *)inPresence +{ + presence = inPresence; + jid = [inPresence from]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (XMPPPresence *)presence +{ + return presence; +} + +- (XMPPJID *)jid +{ + return jid; +} + +- (XMPPJID *)roomJID +{ + return [jid bareJID]; +} + +- (NSString *)nickname +{ + return [jid resource]; +} + +- (NSString *)itemAttributeStringValueForName:(NSString *)attrName +{ + NSXMLElement *x = [presence elementForName:@"x" xmlns:@"http://jabber.org/protocol/muc#user"]; + NSXMLElement *item = [x elementForName:@"item"]; + if (item) + { + NSString *result = [item attributeStringValueForName:attrName]; + if (result) + { + return [result lowercaseString]; + } + } + + return nil; +} + +- (NSString *)role +{ + return [self itemAttributeStringValueForName:@"role"]; +} + +- (NSString *)affiliation +{ + return [self itemAttributeStringValueForName:@"affiliation"]; +} + +- (XMPPJID *)realJID +{ + NSString *jidStr = [self itemAttributeStringValueForName:@"jid"]; + if (jidStr) + return [XMPPJID jidWithString:jidStr]; + else + return nil; +} + +- (NSDate *)createdAt +{ + return createdAt; +} + +- (XMPPJID *)streamFullJid +{ + return streamFullJid; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Compare +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSComparisonResult)compareByNickname:(XMPPRoomOccupantHybridMemoryStorageObject *)another +{ + return [self.nickname compare:another.nickname]; +} + +- (NSComparisonResult)compareByCreatedAt:(XMPPRoomOccupantHybridMemoryStorageObject *)another +{ + return [self.createdAt compare:another.createdAt]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark NSObject Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSUInteger)hash +{ + return [jid hash]; +} + +- (BOOL)isEqual:(id)anObject +{ + if ([anObject isMemberOfClass:[self class]]) + { + XMPPRoomOccupantHybridMemoryStorageObject *another = (XMPPRoomOccupantHybridMemoryStorageObject *)anObject; + + if ([jid isEqualToJID:[another jid]]) + { + if ([streamFullJid isEqualToJID:[another streamFullJid]]) + { + return YES; + } + } + } + + return NO; +} + +@end diff --git a/Extensions/XEP-0045/MemoryStorage/XMPPRoomMemoryStorage.h b/Extensions/XEP-0045/MemoryStorage/XMPPRoomMemoryStorage.h new file mode 100644 index 0000000..e29faba --- /dev/null +++ b/Extensions/XEP-0045/MemoryStorage/XMPPRoomMemoryStorage.h @@ -0,0 +1,130 @@ +#import +#import "XMPPRoom.h" +#import "XMPPRoomOccupant.h" +#import "XMPPRoomMessageMemoryStorageObject.h" +#import "XMPPRoomOccupantMemoryStorageObject.h" + + + +@interface XMPPRoomMemoryStorage : NSObject + +- (id)init; + +@property (readonly) XMPPRoom *parent; + +/** + * You can optionally extend the XMPPRoomMessageMemoryStorageObject and XMPPRoomOccupantMemoryStorageObject classes. + * Then just set the classes here, and your subclasses will automatically get used. + * + * You must set your desired class(es), if different from default, before you begin using the storage class. +**/ +@property (assign, readwrite) Class messageClass; +@property (assign, readwrite) Class occupantClass; + +/** + * Returns the occupant with the given full JID. +**/ +- (XMPPRoomOccupantMemoryStorageObject *)occupantForJID:(XMPPJID *)jid; + +/** + * Returns the messages in sorted order. + * The messages are sorted via the [messageClass compare:] method, which may optionally be overriden in subclasses. +**/ +- (NSArray *)messages; + +/** + * Returns the occupants in sorted order. + * The occupants are sorted via the [occupantsClass compare:] method, which may optionally be overriden in subclasses. +**/ +- (NSArray *)occupants; + +/** + * This method is designed for subclasses of XMPPRoomMessageMemoryStorageObject + * which may dynamically change the operation of the overriden compare: method. + * + * If changed, this method should be invoked to force a resort. +**/ +- (NSArray *)resortMessages; + +/** + * This method is designed for subclasses of XMPPRoomOccupantMemoryStorageObject + * which may dynamically change the operation of the overriden compare: method. + * + * If changed, this method should be invoked to force a resort. +**/ +- (NSArray *)resortOccupants; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPRoomMemoryStorageDelegate +@optional + +// +// XMPPRoomMemoryStorage automatically uses the delegate(s) of its parent XMPPRoom. +// + +/** + * Similar to XMPPRoomDelegate's xmppRoom:occupantDidJoin:withPresence: method. + * + * This method provides the delegate with the occupant storage instance, + * as well as the current snapshot of the occupants array, + * and the index at which the new occupant was inserted. + * + * The given occupant will be included in the given array at the given index. +**/ +- (void)xmppRoomMemoryStorage:(XMPPRoomMemoryStorage *)sender + occupantDidJoin:(XMPPRoomOccupantMemoryStorageObject *)occupant + atIndex:(NSUInteger)index + inArray:(NSArray *)allOccupants; + +/** + * Similar to XMPPRoomDelegate's xmppRoom:occupantDidLeave:withPresence: method. + * + * This method provides the delegate with the occupant storage instance, + * as well as the current snapshot of the occupants array, + * and the index at which the occupant used to reside. + * + * The given occupant will not be included in the given array. + * It's previous location is given by the index. +**/ +- (void)xmppRoomMemoryStorage:(XMPPRoomMemoryStorage *)sender + occupantDidLeave:(XMPPRoomOccupantMemoryStorageObject *)occupant + atIndex:(NSUInteger)index + fromArray:(NSArray *)allOccupants; + +/** + * Similar to XMPPRoomDelegate's xmppRoom:occupantDidUpdate:withPresence: method. + * + * This method provides the delegate with the occupant storage instance, + * as well as the current snapshot of the occupants array, + * including the old and new index of the occupant. + * + * The given occupant will be included in the given array at the given newIndex. + * If the location of the occupant didn't change, then the oldIndex and newIndex will be the same. +**/ +- (void)xmppRoomMemoryStorage:(XMPPRoomMemoryStorage *)sender + occupantDidUpdate:(XMPPRoomOccupantMemoryStorageObject *)occupant + fromIndex:(NSUInteger)oldIndex + toIndex:(NSUInteger)newIndex + inArray:(NSArray *)allOccupants; + +/** + * Similar to XMPPRoomDelegate's xmppRoom:didReceiveMessage:fromOccupant: method. + * + * This method provides the delegate with the occupant and message storage instance, + * as well as the current snapshot of the messages array, + * including the new index of the message. + * + * The given message will be included in the given array at the given index. +**/ +- (void)xmppRoomMemoryStorage:(XMPPRoomMemoryStorage *)sender + didReceiveMessage:(XMPPRoomMessageMemoryStorageObject *)message + fromOccupant:(XMPPRoomOccupantMemoryStorageObject *)occupant + atIndex:(NSUInteger)index + inArray:(NSArray *)allMessages; + +@end diff --git a/Extensions/XEP-0045/MemoryStorage/XMPPRoomMemoryStorage.m b/Extensions/XEP-0045/MemoryStorage/XMPPRoomMemoryStorage.m new file mode 100644 index 0000000..0714f25 --- /dev/null +++ b/Extensions/XEP-0045/MemoryStorage/XMPPRoomMemoryStorage.m @@ -0,0 +1,707 @@ +#import "XMPPRoomMemoryStorage.h" +#import "XMPPRoomPrivate.h" +#import "XMPP.h" +#import "NSXMLElement+XEP_0203.h" +#import "XMPPLogging.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +#define AssertPrivateQueue() \ + NSAssert(dispatch_get_specific(parentQueueTag), @"Private method: MUST run on parentQueue"); + +#define AssertParentQueue() \ + NSAssert(dispatch_get_specific(parentQueueTag), @"Private protocol method: MUST run on parentQueue"); + +@interface XMPPRoomMemoryStorage () +{ + #if __has_feature(objc_arc_weak) + __weak XMPPRoom *parent; + #else + __unsafe_unretained XMPPRoom *parent; + #endif + dispatch_queue_t parentQueue; + void *parentQueueTag; + + NSMutableArray * messages; + NSMutableArray * occupantsArray; + NSMutableDictionary * occupantsDict; + + Class messageClass; + Class occupantClass; +} + +@property (readonly) dispatch_queue_t parentQueue; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPRoomMemoryStorage + +- (id)init +{ + if ((self = [super init])) + { + messages = [[NSMutableArray alloc] init]; + occupantsArray = [[NSMutableArray alloc] init]; + occupantsDict = [[NSMutableDictionary alloc] init]; + + messageClass = [XMPPRoomMessageMemoryStorageObject class]; + occupantClass = [XMPPRoomOccupantMemoryStorageObject class]; + } + return self; +} + +- (BOOL)configureWithParent:(XMPPRoom *)aParent queue:(dispatch_queue_t)queue +{ + NSParameterAssert(aParent != nil); + NSParameterAssert(queue != NULL); + + BOOL result = NO; + + @synchronized(self) + { + if ((parent == nil) && (parentQueue == NULL)) + { + parent = aParent; + parentQueue = queue; + parentQueueTag = &parentQueueTag; + dispatch_queue_set_specific(parentQueue, parentQueueTag, parentQueueTag, NULL); + + #if !OS_OBJECT_USE_OBJC + dispatch_retain(parentQueue); + #endif + + result = YES; + } + } + + return result; +} + +- (GCDMulticastDelegate *)multicastDelegate +{ + return (GCDMulticastDelegate *)[parent multicastDelegate]; +} + +- (void)dealloc +{ + #if !OS_OBJECT_USE_OBJC + if (parentQueue) + dispatch_release(parentQueue); + #endif +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@synthesize messageClass; +@synthesize occupantClass; + +- (XMPPRoom *)parent +{ + XMPPRoom *result = nil; + + @synchronized(self) // synchronized with configureWithParent:queue: + { + result = parent; + } + + return result; +} + +- (dispatch_queue_t)parentQueue +{ + dispatch_queue_t result = NULL; + + @synchronized(self) // synchronized with configureWithParent:queue: + { + result = parentQueue; + } + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Internal API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)existsMessage:(XMPPMessage *)message +{ + NSDate *remoteTimestamp = [message delayedDeliveryDate]; + + if (remoteTimestamp == nil) + { + // When the xmpp server sends us a room message, it will always timestamp delayed messages. + // For example, when retrieving the discussion history, all messages will include the original timestamp. + // If a message doesn't include such timestamp, then we know we're getting it in "real time". + + return NO; + } + + if ([messages count] == 0) + { + // Safety net for binary search algorithm used below + + return NO; + } + + + // Does this message already exist in the messages array? + // How can we tell if two XMPPRoomMessages are the same? + // + // 1. Same jid + // 2. Same text + // 3. Same remoteTimestamp + // 4. Approximately the same localTimestamps (if existing message doesn't have set remoteTimestamp) + // + // This is actually a rather difficult question. + // What if the same user sends the exact same message multiple times? + // + // If we first received the message while already in the room, it won't contain a remoteTimestamp. + // Returning to the room later and downloading the discussion history will return the same message, + // this time with a remote timestamp. + // + // So if the message doesn't have a remoteTimestamp, + // but it's localTimestamp is approximately the same as the remoteTimestamp, + // then this is enough evidence to consider the messages the same. + + // Algorithm overview: + // + // Since the clock of the client and server may be out of sync, + // a localTimestamp and remoteTimestamp may be off by several seconds. + // So we're going to search a range of messages, bounded by a min and max localTimestamp. + // + // We find the first message that has a localTimestamp >= minLocalTimestamp. + // We then search from there to the first message that has a localTimestamp > maxLocalTimestamp. + // + // This represents our range of messages to search. + // Then we can simply iterate over these messages to see if any have the same jid and text. + + NSDate *minLocalTimestamp = [remoteTimestamp dateByAddingTimeInterval:-60]; + NSDate *maxLocalTimestamp = [remoteTimestamp dateByAddingTimeInterval: 60]; + + // Use binary search to locate first message with localTimestamp >= minLocalTimestamp. + + NSInteger mid; + NSInteger min = 0; + NSInteger max = [messages count] - 1; + + while (YES) + { + mid = (min + max) / 2; + XMPPRoomMessageMemoryStorageObject *currentMessage = messages[mid]; + + NSComparisonResult cmp = [minLocalTimestamp compare:[currentMessage localTimestamp]]; + if (cmp == NSOrderedAscending) + { + // minLocalTimestamp < currentMessage.localTimestamp + + if (mid == min) + break; + else + max = mid - 1; + } + else // Descending || Same + { + // minLocalTimestamp >= currentMessage.localTimestamp + + if (mid == max) { + mid++; + break; + } + else { + min = mid + 1; + } + } + } + + // The 'mid' variable now points to the index of the first message in the sorted messages array + // that has a localTimestamp >= minLocalTimestamp. + // + // We're going to start looking for matching messages here, + // and break if we find a message with localTimestamp <= maxLocalTimestamp. + + XMPPJID *messageJid = [message from]; + NSString *messageBody = [[message elementForName:@"body"] stringValue]; + + NSInteger index; + for (index = mid; index < [messages count]; index++) + { + XMPPRoomMessageMemoryStorageObject *currentMessage = messages[index]; + + NSComparisonResult cmp = [maxLocalTimestamp compare:[currentMessage localTimestamp]]; + if (cmp != NSOrderedAscending) + { + // maxLocalTimestamp >= currentMessage.localTimestamp + break; + } + + if ([currentMessage.jid isEqualToJID:messageJid]) + { + if ([currentMessage.body isEqualToString:messageBody]) + { + if (currentMessage.remoteTimestamp) + { + if ([currentMessage.remoteTimestamp isEqualToDate:remoteTimestamp]) + { + // 1. jid matches + // 2. body matches + // 3. remoteTimestamp matches + // + // => Incoming message already exists in the array. + + return YES; + } + } + else + { + // 1. jid matches + // 2. body matches + // 3. existing message in array doesn't have set remoteTimestamp + // 4. existing message has approximately the same localTimestamp + // + // => Incoming message already exists in the array. + + return YES; + } + } + } + } + + return NO; +} + +- (NSUInteger)insertMessage:(XMPPRoomMessageMemoryStorageObject *)message +{ + NSUInteger count = [messages count]; + + if (count == 0) + { + [messages addObject:message]; + return 0; + } + + // Shortcut - Most (if not all) messages are inserted at the end + + XMPPRoomMessageMemoryStorageObject *lastMessage = messages[count - 1]; + if ([message compare:lastMessage] != NSOrderedAscending) + { + [messages addObject:message]; + return count; + } + + // Shortcut didn't work. + // Find location using binary search algorithm. + + NSInteger mid; + NSInteger min = 0; + NSInteger max = count - 1; + + while (YES) + { + mid = (min + max) / 2; + XMPPRoomMessageMemoryStorageObject *currentMessage = messages[mid]; + + NSComparisonResult cmp = [message compare:currentMessage]; + if (cmp == NSOrderedAscending) + { + // message < currentMessage + + if (mid == min) + break; + else + max = mid - 1; + } + else // Descending || Same + { + // message >= currentMessage + + if (mid == max) { + mid++; + break; + } + else { + min = mid + 1; + } + } + } + + // Algorithm check: + // + // Insert in array[length] at index (index) + // : min, mid, max (cmp_result) + // + // + // Insert in array[3] at index (0) + // : 0, 1, 2 (asc) + // : 0, 0, 0 (asc) break + // + // Insert in array[3] at index (1) + // : 0, 1, 2 (asc) + // : 0, 0, 0 (dsc) mid++, break + // + // Insert in array[3] at index (2) + // : 0, 1, 2 (dsc) + // : 2, 2, 2 (asc) break + // + // Insert in array[3] at index (3) + // : 0, 1, 2 (dsc) + // : 2, 2, 2 (dsc) mid++, break + + [messages insertObject:message atIndex:mid]; + return (NSUInteger)mid; +} + +- (void)addMessage:(XMPPRoomMessageMemoryStorageObject *)roomMsg +{ + NSUInteger index = [self insertMessage:roomMsg]; + + XMPPRoomOccupantMemoryStorageObject *occupant = occupantsDict[[roomMsg jid]]; + + XMPPRoomMessageMemoryStorageObject *roomMsgCopy = [roomMsg copy]; + XMPPRoomOccupantMemoryStorageObject *occupantCopy = [occupant copy]; + NSArray *messagesCopy = [[NSArray alloc] initWithArray:messages copyItems:YES]; + + [[self multicastDelegate] xmppRoomMemoryStorage:self + didReceiveMessage:roomMsgCopy + fromOccupant:occupantCopy + atIndex:index + inArray:messagesCopy]; +} + +- (NSUInteger)insertOccupant:(XMPPRoomOccupantMemoryStorageObject *)occupant +{ + NSUInteger count = [occupantsArray count]; + + if (count == 0) + { + [occupantsArray addObject:occupant]; + return 0; + } + + // Find location using binary search algorithm. + + NSInteger mid; + NSInteger min = 0; + NSInteger max = count - 1; + + while (YES) + { + mid = (min + max) / 2; + XMPPRoomOccupantMemoryStorageObject *currentOccupant = occupantsArray[mid]; + + NSComparisonResult cmp = [occupant compare:currentOccupant]; + if (cmp == NSOrderedAscending) + { + // occupant < currentOccupant + + if (mid == min) + break; + else + max = mid - 1; + } + else // Descending || Same + { + // occupant >= currentOccupant + + if (mid == max) { + mid++; + break; + } + else { + min = mid + 1; + } + } + } + + [occupantsArray insertObject:occupant atIndex:mid]; + return (NSUInteger)mid; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Public API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (XMPPRoomOccupantMemoryStorageObject *)occupantForJID:(XMPPJID *)jid +{ + XMPPLogTrace(); + + if (self.parentQueue == NULL) + { + // Haven't been attached to parent yet + return nil; + } + + if (dispatch_get_specific(parentQueueTag)) + { + return occupantsDict[jid]; + } + else + { + __block XMPPRoomOccupantMemoryStorageObject *occupant = nil; + + dispatch_sync(parentQueue, ^{ @autoreleasepool { + + occupant = [occupantsDict[jid] copy]; + }}); + + return occupant; + } +} + +- (NSArray *)messages +{ + XMPPLogTrace(); + + if (self.parentQueue == NULL) + { + // Haven't been attached to parent yet + return nil; + } + + if (dispatch_get_specific(parentQueueTag)) + { + return messages; + } + else + { + __block NSArray *result = nil; + + dispatch_sync(parentQueue, ^{ @autoreleasepool { + + result = [[NSArray alloc] initWithArray:messages copyItems:YES]; + }}); + + return result; + } +} + +- (NSArray *)occupants +{ + XMPPLogTrace(); + + if (self.parentQueue == NULL) + { + // Haven't been attached to parent yet + return nil; + } + + if (dispatch_get_specific(parentQueueTag)) + { + return occupantsArray; + } + else + { + __block NSArray *result = nil; + + dispatch_sync(parentQueue, ^{ @autoreleasepool { + + result = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES]; + }}); + + return result; + } +} + +- (NSArray *)resortMessages +{ + XMPPLogTrace(); + + if (self.parentQueue == NULL) + { + // Haven't been attached to parent yet + return nil; + } + + if (dispatch_get_specific(parentQueueTag)) + { + [messages sortUsingSelector:@selector(compare:)]; + return messages; + } + else + { + __block NSArray *result = nil; + + dispatch_sync(parentQueue, ^{ @autoreleasepool { + + [messages sortUsingSelector:@selector(compare:)]; + result = [[NSArray alloc] initWithArray:messages copyItems:YES]; + }}); + + return result; + } +} + +- (NSArray *)resortOccupants +{ + XMPPLogTrace(); + + if (self.parentQueue == NULL) + { + // Haven't been attached to parent yet + return nil; + } + + if (dispatch_get_specific(parentQueueTag)) + { + [occupantsArray sortUsingSelector:@selector(compare:)]; + return occupantsArray; + } + else + { + __block NSArray *result = nil; + + dispatch_sync(parentQueue, ^{ @autoreleasepool { + + [occupantsArray sortUsingSelector:@selector(compare:)]; + result = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES]; + }}); + + return result; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPRoomStorage Protocol +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)handlePresence:(XMPPPresence *)presence room:(XMPPRoom *)room +{ + XMPPLogTrace(); + AssertParentQueue(); + + XMPPJID *from = [presence from]; + + if ([[presence type] isEqualToString:@"unavailable"]) + { + XMPPRoomOccupantMemoryStorageObject *occupant = occupantsDict[from]; + if (occupant) + { + // Occupant did leave - remove + + NSUInteger index = [occupantsArray indexOfObjectIdenticalTo:occupant]; + + [occupantsArray removeObjectAtIndex:index]; + [occupantsDict removeObjectForKey:from]; + + // Notify delegate(s) + + XMPPRoomOccupantMemoryStorageObject *occupantCopy = [occupant copy]; + NSArray *occupantsCopy = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES]; + + [[self multicastDelegate] xmppRoomMemoryStorage:self + occupantDidLeave:occupantCopy + atIndex:index + fromArray:occupantsCopy]; + } + } + else + { + XMPPRoomOccupantMemoryStorageObject *occupant = occupantsDict[from]; + if (occupant == nil) + { + // Occupant did join - add + + occupant = [[self.occupantClass alloc] initWithPresence:presence]; + + NSUInteger index = [self insertOccupant:occupant]; + occupantsDict[from] = occupant; + + // Notify delegate(s) + + XMPPRoomOccupantMemoryStorageObject *occupantCopy = [occupant copy]; + NSArray *occupantsCopy = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES]; + + [[self multicastDelegate] xmppRoomMemoryStorage:self + occupantDidJoin:occupantCopy + atIndex:index + inArray:occupantsCopy]; + } + else + { + // Occupant did update - move + + [occupant updateWithPresence:presence]; + + NSUInteger oldIndex = [occupantsArray indexOfObjectIdenticalTo:occupant]; + [occupantsArray removeObjectAtIndex:oldIndex]; + NSUInteger newIndex = [self insertOccupant:occupant]; + + // Notify delegate(s) + + XMPPRoomOccupantMemoryStorageObject *occupantCopy = [occupant copy]; + NSArray *occupantsCopy = [[NSArray alloc] initWithArray:occupantsArray copyItems:YES]; + + [[self multicastDelegate] xmppRoomMemoryStorage:self + occupantDidUpdate:occupantCopy + fromIndex:oldIndex + toIndex:newIndex + inArray:occupantsCopy]; + } + } +} + +- (void)handleOutgoingMessage:(XMPPMessage *)message room:(XMPPRoom *)room +{ + XMPPLogTrace(); + AssertParentQueue(); + + XMPPJID *msgJID = room.myRoomJID; + + XMPPRoomMessageMemoryStorageObject *roomMsg; + roomMsg = [[self.messageClass alloc] initWithOutgoingMessage:message jid:msgJID]; + + [self addMessage:roomMsg]; +} + +- (void)handleIncomingMessage:(XMPPMessage *)message room:(XMPPRoom *)room +{ + XMPPLogTrace(); + AssertParentQueue(); + + XMPPJID *myRoomJID = room.myRoomJID; + XMPPJID *messageJID = [message from]; + + if ([myRoomJID isEqualToJID:messageJID]) + { + if (![message wasDelayed]) + { + // Ignore - we already stored message in handleOutgoingMessage:room: + return; + } + } + + if ([self existsMessage:message]) + { + XMPPLogVerbose(@"%@: %@ - Duplicate message", THIS_FILE, THIS_METHOD); + } + else + { + XMPPRoomMessageMemoryStorageObject *roomMessage = [[self.messageClass alloc] initWithIncomingMessage:message]; + [self addMessage:roomMessage]; + } +} + +- (void)handleDidLeaveRoom:(XMPPRoom *)room +{ + XMPPLogTrace(); + AssertParentQueue(); + + [occupantsDict removeAllObjects]; + [occupantsArray removeAllObjects]; +} + +@end diff --git a/Extensions/XEP-0045/MemoryStorage/XMPPRoomMessageMemoryStorageObject.h b/Extensions/XEP-0045/MemoryStorage/XMPPRoomMessageMemoryStorageObject.h new file mode 100644 index 0000000..fc83094 --- /dev/null +++ b/Extensions/XEP-0045/MemoryStorage/XMPPRoomMessageMemoryStorageObject.h @@ -0,0 +1,34 @@ +#import +#import "XMPPRoomMessage.h" + + +@interface XMPPRoomMessageMemoryStorageObject : NSObject + +- (id)initWithIncomingMessage:(XMPPMessage *)message; +- (id)initWithOutgoingMessage:(XMPPMessage *)message jid:(XMPPJID *)myRoomJID; + +/** + * The properties below are documented in the XMPPRoomMessage protocol. +**/ + +@property (nonatomic, readonly) XMPPMessage *message; + +@property (nonatomic, readonly) XMPPJID * roomJID; + +@property (nonatomic, readonly) XMPPJID * jid; +@property (nonatomic, readonly) NSString * nickname; + +@property (nonatomic, readonly) NSDate * localTimestamp; +@property (nonatomic, readonly) NSDate * remoteTimestamp; + +@property (nonatomic, readonly) BOOL isFromMe; + +/** + * Compares two messages based on the localTimestamp. + * + * This method provides the ordering used by XMPPRoomMemoryStorage. + * Subclasses may override this method to provide an alternative sorting mechanism. +**/ +- (NSComparisonResult)compare:(XMPPRoomMessageMemoryStorageObject *)another; + +@end diff --git a/Extensions/XEP-0045/MemoryStorage/XMPPRoomMessageMemoryStorageObject.m b/Extensions/XEP-0045/MemoryStorage/XMPPRoomMessageMemoryStorageObject.m new file mode 100644 index 0000000..02081fd --- /dev/null +++ b/Extensions/XEP-0045/MemoryStorage/XMPPRoomMessageMemoryStorageObject.m @@ -0,0 +1,175 @@ +#import "XMPPRoomMessageMemoryStorageObject.h" +#import "XMPP.h" +#import "NSXMLElement+XEP_0203.h" + + +@implementation XMPPRoomMessageMemoryStorageObject +{ + XMPPMessage *message; + XMPPJID *jid; + NSDate *localTimestamp; + NSDate *remoteTimestamp; + BOOL isFromMe; +} + +- (id)initWithIncomingMessage:(XMPPMessage *)inMessage +{ + if ((self = [super init])) + { + message = inMessage; + jid = [inMessage from]; + isFromMe = NO; + + remoteTimestamp = [inMessage delayedDeliveryDate]; + if (remoteTimestamp) + localTimestamp = remoteTimestamp; + else + localTimestamp = [[NSDate alloc] init]; + } + return self; +} + +- (id)initWithOutgoingMessage:(XMPPMessage *)inMessage jid:(XMPPJID *)myRoomJID +{ + if ((self = [super init])) + { + message = inMessage; + jid = myRoomJID; + isFromMe = YES; + + localTimestamp = [[NSDate alloc] init]; + } + return self; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Encoding, Decoding +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if ! TARGET_OS_IPHONE +- (id)replacementObjectForPortCoder:(NSPortCoder *)encoder +{ + if ([encoder isBycopy]) + return self; + else + return [super replacementObjectForPortCoder:encoder]; + // return [NSDistantObject proxyWithLocal:self connection:[encoder connection]]; +} +#endif + +- (id)initWithCoder:(NSCoder *)coder +{ + if ((self = [super init])) + { + if ([coder allowsKeyedCoding]) + { + if([coder respondsToSelector:@selector(requiresSecureCoding)] && + [coder requiresSecureCoding]) + { + message = [coder decodeObjectOfClass:[XMPPMessage class] forKey:@"message"]; + jid = [coder decodeObjectOfClass:[XMPPJID class] forKey:@"jid"]; + localTimestamp = [coder decodeObjectOfClass:[NSDate class] forKey:@"localTimestamp"]; + remoteTimestamp = [coder decodeObjectOfClass:[NSDate class] forKey:@"remoteTimestamp"]; + isFromMe = [coder decodeBoolForKey:@"isFromMe"]; + } + else + { + message = [coder decodeObjectForKey:@"message"]; + jid = [coder decodeObjectForKey:@"jid"]; + localTimestamp = [coder decodeObjectForKey:@"localTimestamp"]; + remoteTimestamp = [coder decodeObjectForKey:@"remoteTimestamp"]; + isFromMe = [coder decodeBoolForKey:@"isFromMe"]; + } + + } + else + { + message = [coder decodeObject]; + jid = [coder decodeObject]; + localTimestamp = [coder decodeObject]; + remoteTimestamp = [coder decodeObject]; + isFromMe = [[coder decodeObject] boolValue]; + } + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + if ([coder allowsKeyedCoding]) + { + [coder encodeObject:message forKey:@"message"]; + [coder encodeObject:jid forKey:@"jid"]; + [coder encodeObject:localTimestamp forKey:@"timestamp"]; + [coder encodeObject:remoteTimestamp forKey:@"remoteTimestamp"]; + [coder encodeBool:isFromMe forKey:@"isFromMe"]; + } + else + { + [coder encodeObject:message]; + [coder encodeObject:jid]; + [coder encodeObject:localTimestamp]; + [coder encodeObject:remoteTimestamp]; + [coder encodeObject:@(isFromMe)]; + } +} + ++ (BOOL) supportsSecureCoding +{ + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Copying +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)copyWithZone:(NSZone *)zone +{ + // We use [self class] to support subclassing + + XMPPRoomMessageMemoryStorageObject *deepCopy = (XMPPRoomMessageMemoryStorageObject *)[[[self class] alloc] init]; + + deepCopy->message = [message copy]; + deepCopy->jid = [jid copy]; + deepCopy->localTimestamp = [localTimestamp copy]; + deepCopy->remoteTimestamp = [remoteTimestamp copy]; + deepCopy->isFromMe = isFromMe; + + return deepCopy; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@synthesize message; +@synthesize jid; +@synthesize localTimestamp; +@synthesize remoteTimestamp; +@synthesize isFromMe; + +- (XMPPJID *)roomJID +{ + return [jid bareJID]; +} + +- (NSString *)nickname +{ + return [jid resource]; +} + +- (NSString *)body +{ + return [[message elementForName:@"body"] stringValue]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Comparisons +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSComparisonResult)compare:(XMPPRoomMessageMemoryStorageObject *)another +{ + return [localTimestamp compare:[another localTimestamp]]; +} + +@end diff --git a/Extensions/XEP-0045/MemoryStorage/XMPPRoomOccupantMemoryStorageObject.h b/Extensions/XEP-0045/MemoryStorage/XMPPRoomOccupantMemoryStorageObject.h new file mode 100644 index 0000000..cb9c44a --- /dev/null +++ b/Extensions/XEP-0045/MemoryStorage/XMPPRoomOccupantMemoryStorageObject.h @@ -0,0 +1,33 @@ +#import +#import "XMPPRoom.h" +#import "XMPPRoomOccupant.h" + + +@interface XMPPRoomOccupantMemoryStorageObject : NSObject + +- (id)initWithPresence:(XMPPPresence *)presence; +- (void)updateWithPresence:(XMPPPresence *)presence; + +/** + * The properties below are documented in the XMPPRoomOccupant protocol. +**/ + +@property (readonly) XMPPPresence *presence; + +@property (readonly) XMPPJID * jid; +@property (readonly) XMPPJID * roomJID; +@property (readonly) NSString * nickname; + +@property (readonly) NSString * role; +@property (readonly) NSString * affiliation; +@property (readonly) XMPPJID * realJID; + +/** + * Compares two occupants based on the nickname. + * + * This method provides the ordering used by XMPPRoomMemoryStorage. + * Subclasses may override this method to provide an alternative sorting mechanism. +**/ +- (NSComparisonResult)compare:(XMPPRoomOccupantMemoryStorageObject *)another; + +@end diff --git a/Extensions/XEP-0045/MemoryStorage/XMPPRoomOccupantMemoryStorageObject.m b/Extensions/XEP-0045/MemoryStorage/XMPPRoomOccupantMemoryStorageObject.m new file mode 100644 index 0000000..514852f --- /dev/null +++ b/Extensions/XEP-0045/MemoryStorage/XMPPRoomOccupantMemoryStorageObject.m @@ -0,0 +1,196 @@ +#import "XMPPRoomOccupantMemoryStorageObject.h" + + +@implementation XMPPRoomOccupantMemoryStorageObject +{ + XMPPPresence *presence; + XMPPJID *jid; +} + +- (id)initWithPresence:(XMPPPresence *)inPresence +{ + if ((self = [super init])) + { + presence = inPresence; + jid = [inPresence from]; + } + return self; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Encoding, Decoding +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if ! TARGET_OS_IPHONE +- (id)replacementObjectForPortCoder:(NSPortCoder *)encoder +{ + if ([encoder isBycopy]) + return self; + else + return [super replacementObjectForPortCoder:encoder]; + // return [NSDistantObject proxyWithLocal:self connection:[encoder connection]]; +} +#endif + +- (id)initWithCoder:(NSCoder *)coder +{ + if ((self = [super init])) + { + if ([coder allowsKeyedCoding]) + { + if([coder respondsToSelector:@selector(requiresSecureCoding)] && + [coder requiresSecureCoding]) + { + presence = [coder decodeObjectOfClass:[XMPPPresence class] forKey:@"presence"]; + jid = [coder decodeObjectOfClass:[XMPPJID class] forKey:@"jid"]; + } + else + { + presence = [coder decodeObjectForKey:@"presence"]; + jid = [coder decodeObjectForKey:@"jid"]; + } + } + else + { + presence = [coder decodeObject]; + jid = [coder decodeObject]; + } + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + if ([coder allowsKeyedCoding]) + { + [coder encodeObject:presence forKey:@"presence"]; + [coder encodeObject:jid forKey:@"jid"]; + } + else + { + [coder encodeObject:presence]; + [coder encodeObject:jid]; + } +} + ++ (BOOL) supportsSecureCoding +{ + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Copying +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)copyWithZone:(NSZone *)zone +{ + // We use [self class] to support subclassing + + XMPPRoomOccupantMemoryStorageObject *deepCopy = (XMPPRoomOccupantMemoryStorageObject *)[[[self class] alloc] init]; + + deepCopy->presence = [presence copy]; + deepCopy->jid = [jid copy]; + + return deepCopy; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Updates +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)updateWithPresence:(XMPPPresence *)inPresence +{ + presence = inPresence; + jid = [inPresence from]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (XMPPJID *)roomJID +{ + return [jid bareJID]; +} + +- (XMPPJID *)jid +{ + return jid; +} + +- (NSString *)nickname +{ + return [jid resource]; +} + +- (NSString *)itemAttributeStringValueForName:(NSString *)attrName +{ + NSXMLElement *x = [presence elementForName:@"x" xmlns:@"http://jabber.org/protocol/muc#user"]; + NSXMLElement *item = [x elementForName:@"item"]; + if (item) + { + NSString *result = [item attributeStringValueForName:attrName]; + if (result) + { + return [result lowercaseString]; + } + } + + return nil; +} + +- (NSString *)role +{ + return [self itemAttributeStringValueForName:@"role"]; +} + +- (NSString *)affiliation +{ + return [self itemAttributeStringValueForName:@"affiliation"]; +} + +- (XMPPJID *)realJID +{ + NSString *jidStr = [self itemAttributeStringValueForName:@"jid"]; + if (jidStr) + return [XMPPJID jidWithString:jidStr]; + else + return nil; +} + +- (XMPPPresence *)presence +{ + return presence; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Comparisons +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSComparisonResult)compare:(XMPPRoomOccupantMemoryStorageObject *)another +{ + return [self.nickname compare:another.nickname]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark NSObject Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSUInteger)hash +{ + return [jid hash]; +} + +- (BOOL)isEqual:(id)anObject +{ + if ([anObject isMemberOfClass:[self class]]) + { + XMPPRoomOccupantMemoryStorageObject *another = (XMPPRoomOccupantMemoryStorageObject *)anObject; + + return [jid isEqualToJID:[another jid]]; + } + + return NO; +} + +@end diff --git a/Extensions/XEP-0045/XMPPMUC.h b/Extensions/XEP-0045/XMPPMUC.h new file mode 100644 index 0000000..53dac11 --- /dev/null +++ b/Extensions/XEP-0045/XMPPMUC.h @@ -0,0 +1,131 @@ +#import +#import "XMPP.h" +#import "XMPPRoom.h" + +#define _XMPP_MUC_H + +@class XMPPIDTracker; + +/** + * The XMPPMUC module, combined with XMPPRoom and associated storage classes, + * provides an implementation of XEP-0045 Multi-User Chat. + * + * The bulk of the code resides in XMPPRoom, which handles the xmpp technical details + * such as surrounding joining/leaving a room, sending/receiving messages, etc. + * + * The XMPPMUC class provides general (but important) tasks relating to MUC: + * - It integrates with XMPPCapabilities (if available) to properly advertise support for MUC. + * - It monitors active XMPPRoom instances on the xmppStream, + * and provides an efficient query to see if a presence or message element is targeted at a room. + * - It listens for MUC room invitations sent from other users. +**/ +@interface XMPPMUC : XMPPModule +{ +/* Inherited from XMPPModule: + + XMPPStream *xmppStream; + + dispatch_queue_t moduleQueue; + */ + + NSMutableSet *rooms; + + XMPPIDTracker *xmppIDTracker; +} + +/* Inherited from XMPPModule: + +- (id)init; +- (id)initWithDispatchQueue:(dispatch_queue_t)queue; + +- (BOOL)activate:(XMPPStream *)xmppStream; +- (void)deactivate; + +@property (readonly) XMPPStream *xmppStream; + +- (NSString *)moduleName; + +*/ + +- (BOOL)isMUCRoomPresence:(XMPPPresence *)presence; +- (BOOL)isMUCRoomMessage:(XMPPMessage *)message; + +/** +* This method will attempt to discover existing services for the domain found in xmppStream.myJID. +* +* @see xmppMUC:didDiscoverServices: +* @see xmppMUCFailedToDiscoverServices:withError: +*/ +- (void)discoverServices; + +/** +* This method will attempt to discover existing rooms (that are not hidden) for a given service. +* +* @see xmppMUC:didDiscoverRooms:forServiceNamed: +* @see xmppMUC:failedToDiscoverRoomsForServiceNamed:withError: +* +* @param serviceName The name of the service for which to discover rooms. Normally in the form +* of "chat.shakespeare.lit". +* +* @return NO if a serviceName is not provided, otherwise YES +*/ +- (BOOL)discoverRoomsForServiceNamed:(NSString *)serviceName; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPMUCDelegate +@optional + +- (void)xmppMUC:(XMPPMUC *)sender roomJID:(XMPPJID *)roomJID didReceiveInvitation:(XMPPMessage *)message; +- (void)xmppMUC:(XMPPMUC *)sender roomJID:(XMPPJID *)roomJID didReceiveInvitationDecline:(XMPPMessage *)message; + +/** +* Implement this method when calling [mucInstance discoverServices]. It will be invoked if the request +* for discovering services is successfully executed and receives a successful response. +* +* @param sender XMPPMUC object invoking this delegate method. +* @param services An array of NSXMLElements in the form shown below. You will need to extract the data you +* wish to use. +* +* +*/ +- (void)xmppMUC:(XMPPMUC *)sender didDiscoverServices:(NSArray *)services; + +/** +* Implement this method when calling [mucInstanse discoverServices]. It will be invoked if the request +* for discovering services is unsuccessfully executed or receives an unsuccessful response. +* +* @param sender XMPPMUC object invoking this delegate method. +* @param error NSError containing more details of the failure. +*/ +- (void)xmppMUCFailedToDiscoverServices:(XMPPMUC *)sender withError:(NSError *)error; + +/** +* Implement this method when calling [mucInstance discoverRoomsForServiceNamed:]. It will be invoked if +* the request for discovering rooms is successfully executed and receives a successful response. +* +* @param sender XMPPMUC object invoking this delegate method. +* @param rooms An array of NSXMLElements in the form shown below. You will need to extract the data you +* wish to use. +* +* +* +* @param serviceName The name of the service for which rooms were discovered. +*/ +- (void)xmppMUC:(XMPPMUC *)sender didDiscoverRooms:(NSArray *)rooms forServiceNamed:(NSString *)serviceName; + +/** +* Implement this method when calling [mucInstance discoverRoomsForServiceNamed:]. It will be invoked if +* the request for discovering rooms is unsuccessfully executed or receives an unsuccessful response. +* +* @param sender XMPPMUC object invoking this delegate method. +* @param serviceName The name of the service for which rooms were attempted to be discovered. +* @param error NSError containing more details of the failure. +*/ +- (void)xmppMUC:(XMPPMUC *)sender failedToDiscoverRoomsForServiceNamed:(NSString *)serviceName withError:(NSError *)error; + +@end diff --git a/Extensions/XEP-0045/XMPPMUC.m b/Extensions/XEP-0045/XMPPMUC.m new file mode 100644 index 0000000..b286394 --- /dev/null +++ b/Extensions/XEP-0045/XMPPMUC.m @@ -0,0 +1,423 @@ +#import "XMPPMUC.h" +#import "XMPPFramework.h" +#import "XMPPLogging.h" +#import "XMPPIDTracker.h" + +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_VERBOSE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +NSString *const XMPPDiscoverItemsNamespace = @"http://jabber.org/protocol/disco#items"; +NSString *const XMPPMUCErrorDomain = @"XMPPMUCErrorDomain"; + +@interface XMPPMUC() +{ + BOOL hasRequestedServices; + BOOL hasRequestedRooms; +} + +@end + +@implementation XMPPMUC + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super initWithDispatchQueue:queue])) { + rooms = [[NSMutableSet alloc] init]; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + if ([super activate:aXmppStream]) + { + XMPPLogVerbose(@"%@: Activated", THIS_FILE); + + xmppIDTracker = [[XMPPIDTracker alloc] initWithStream:xmppStream + dispatchQueue:moduleQueue]; + +#ifdef _XMPP_CAPABILITIES_H + [xmppStream autoAddDelegate:self + delegateQueue:moduleQueue + toModulesOfClass:[XMPPCapabilities class]]; +#endif + return YES; + } + + return NO; +} + +- (void)deactivate +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool { + [xmppIDTracker removeAllIDs]; + xmppIDTracker = nil; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + +#ifdef _XMPP_CAPABILITIES_H + [xmppStream removeAutoDelegate:self delegateQueue:moduleQueue fromModulesOfClass:[XMPPCapabilities class]]; +#endif + + [super deactivate]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Public API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isMUCRoomElement:(XMPPElement *)element +{ + XMPPJID *bareFrom = [[element from] bareJID]; + if (bareFrom == nil) + { + return NO; + } + + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool { + + result = [rooms containsObject:bareFrom]; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (BOOL)isMUCRoomPresence:(XMPPPresence *)presence +{ + return [self isMUCRoomElement:presence]; +} + +- (BOOL)isMUCRoomMessage:(XMPPMessage *)message +{ + return [self isMUCRoomElement:message]; +} + +/** +* This method provides functionality of XEP-0045 6.1 Discovering a MUC Service. +* +* @link {http://xmpp.org/extensions/xep-0045.html#disco-service} +* +* Example 1. Entity Queries Server for Associated Services +* +* +* +* +*/ +- (void)discoverServices +{ + // This is a public method, so it may be invoked on any thread/queue. + + dispatch_block_t block = ^{ @autoreleasepool { + if (hasRequestedServices) return; // We've already requested services + + NSString *toStr = xmppStream.myJID.domain; + NSXMLElement *query = [NSXMLElement elementWithName:@"query" + xmlns:XMPPDiscoverItemsNamespace]; + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" + to:[XMPPJID jidWithString:toStr] + elementID:[xmppStream generateUUID] + child:query]; + + [xmppIDTracker addElement:iq + target:self + selector:@selector(handleDiscoverServicesQueryIQ:withInfo:) + timeout:60]; + + [xmppStream sendElement:iq]; + hasRequestedServices = YES; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +/** +* This method provides functionality of XEP-0045 6.3 Discovering Rooms +* +* @link {http://xmpp.org/extensions/xep-0045.html#disco-rooms} +* +* Example 5. Entity Queries Chat Service for Rooms +* +* +* +* +*/ +- (BOOL)discoverRoomsForServiceNamed:(NSString *)serviceName +{ + // This is a public method, so it may be invoked on any thread/queue. + + if (serviceName.length < 2) + return NO; + + dispatch_block_t block = ^{ @autoreleasepool { + if (hasRequestedRooms) return; // We've already requested rooms + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" + xmlns:XMPPDiscoverItemsNamespace]; + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" + to:[XMPPJID jidWithString:serviceName] + elementID:[xmppStream generateUUID] + child:query]; + + [xmppIDTracker addElement:iq + target:self + selector:@selector(handleDiscoverRoomsQueryIQ:withInfo:) + timeout:60]; + + [xmppStream sendElement:iq]; + hasRequestedRooms = YES; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); + + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPIDTracker +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** +* This method handles the response received (or not received) after calling discoverServices. +*/ +- (void)handleDiscoverServicesQueryIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)info +{ + dispatch_block_t block = ^{ @autoreleasepool { + NSXMLElement *errorElem = [iq elementForName:@"error"]; + + if (errorElem) { + NSString *errMsg = [errorElem.children componentsJoinedByString:@", "]; + NSDictionary *dict = @{NSLocalizedDescriptionKey : errMsg}; + NSError *error = [NSError errorWithDomain:XMPPMUCErrorDomain + code:[errorElem attributeIntegerValueForName:@"code" + withDefaultValue:0] + userInfo:dict]; + + [multicastDelegate xmppMUCFailedToDiscoverServices:self + withError:error]; + return; + } + + NSXMLElement *query = [iq elementForName:@"query" + xmlns:XMPPDiscoverItemsNamespace]; + + NSArray *items = [query elementsForName:@"item"]; + [multicastDelegate xmppMUC:self didDiscoverServices:items]; + hasRequestedServices = NO; // Set this back to NO to allow for future requests + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +/** +* This method handles the response received (or not received) after calling discoverRoomsForServiceNamed:. +*/ +- (void)handleDiscoverRoomsQueryIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)info +{ + dispatch_block_t block = ^{ @autoreleasepool { + NSXMLElement *errorElem = [iq elementForName:@"error"]; + NSString *serviceName = [iq attributeStringValueForName:@"from" withDefaultValue:@""]; + + if (errorElem) { + NSString *errMsg = [errorElem.children componentsJoinedByString:@", "]; + NSDictionary *dict = @{NSLocalizedDescriptionKey : errMsg}; + NSError *error = [NSError errorWithDomain:XMPPMUCErrorDomain + code:[errorElem attributeIntegerValueForName:@"code" + withDefaultValue:0] + userInfo:dict]; + [multicastDelegate xmppMUC:self +failedToDiscoverRoomsForServiceNamed:serviceName + withError:error]; + return; + } + + NSXMLElement *query = [iq elementForName:@"query" + xmlns:XMPPDiscoverItemsNamespace]; + + NSArray *items = [query elementsForName:@"item"]; + [multicastDelegate xmppMUC:self + didDiscoverRooms:items + forServiceNamed:serviceName]; + hasRequestedRooms = NO; // Set this back to NO to allow for future requests + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppStream:(XMPPStream *)sender didRegisterModule:(id)module +{ + if ([module isKindOfClass:[XMPPRoom class]]) + { + XMPPJID *roomJID = [(XMPPRoom *)module roomJID]; + + [rooms addObject:roomJID]; + } +} + +- (void)xmppStream:(XMPPStream *)sender willUnregisterModule:(id)module +{ + if ([module isKindOfClass:[XMPPRoom class]]) + { + XMPPJID *roomJID = [(XMPPRoom *)module roomJID]; + + // It's common for the room to get deactivated and deallocated before + // we've received the goodbye presence from the server. + // So we're going to postpone for a bit removing the roomJID from the list. + // This way the isMUCRoomElement will still remain accurate + // for presence elements that may arrive momentarily. + + double delayInSeconds = 30.0; + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC); + dispatch_after(popTime, moduleQueue, ^{ @autoreleasepool { + + [rooms removeObject:roomJID]; + }}); + } +} + +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message +{ + // Examples from XEP-0045: + // + // + // Example 124. Room Sends Invitation to New Member: + // + // + // + // + // cauldronburn + // + // + // + // + // Example 125. Service Returns Error on Attempt by Mere Member to Invite Others to a Members-Only Room + // + // + // + // + // + // Hey Hecate, this is the place for all good witches! + // + // + // + // + // + // + // + // + // + // Example 50. Room Informs Invitor that Invitation Was Declined + // + // + // + // + // + // Sorry, I'm too busy right now. + // + // + // + // + // + // + // Examples from XEP-0249: + // + // + // Example 1. A direct invitation + // + // + // + // + + NSXMLElement * x = [message elementForName:@"x" xmlns:XMPPMUCUserNamespace]; + NSXMLElement * invite = [x elementForName:@"invite"]; + NSXMLElement * decline = [x elementForName:@"decline"]; + + NSXMLElement * directInvite = [message elementForName:@"x" xmlns:@"jabber:x:conference"]; + + XMPPJID * roomJID = [message from]; + + if (invite || directInvite) + { + [multicastDelegate xmppMUC:self roomJID:roomJID didReceiveInvitation:message]; + } + else if (decline) + { + [multicastDelegate xmppMUC:self roomJID:roomJID didReceiveInvitationDecline:message]; + } +} + +- (BOOL)xmppStream:(XMPPStream *)stream didReceiveIQ:(XMPPIQ *)iq +{ + NSString *type = [iq type]; + + if ([type isEqualToString:@"result"] || [type isEqualToString:@"error"]) { + return [xmppIDTracker invokeForElement:iq withObject:iq]; + } + + return NO; +} + + +#ifdef _XMPP_CAPABILITIES_H +/** + * If an XMPPCapabilites instance is used we want to advertise our support for MUC. +**/ +- (void)xmppCapabilities:(XMPPCapabilities *)sender collectingMyCapabilities:(NSXMLElement *)query +{ + // This method is invoked on our moduleQueue. + + // + // ... + // + // ... + // + + NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"]; + [feature addAttributeWithName:@"var" stringValue:@"http://jabber.org/protocol/muc"]; + + [query addChild:feature]; +} +#endif + +@end diff --git a/Extensions/XEP-0045/XMPPMessage+XEP0045.h b/Extensions/XEP-0045/XMPPMessage+XEP0045.h new file mode 100644 index 0000000..039948c --- /dev/null +++ b/Extensions/XEP-0045/XMPPMessage+XEP0045.h @@ -0,0 +1,11 @@ +#import +#import "XMPPMessage.h" + + +@interface XMPPMessage(XEP0045) + +- (BOOL)isGroupChatMessage; +- (BOOL)isGroupChatMessageWithBody; +- (BOOL)isGroupChatMessageWithSubject; + +@end diff --git a/Extensions/XEP-0045/XMPPMessage+XEP0045.m b/Extensions/XEP-0045/XMPPMessage+XEP0045.m new file mode 100644 index 0000000..1e66392 --- /dev/null +++ b/Extensions/XEP-0045/XMPPMessage+XEP0045.m @@ -0,0 +1,36 @@ +#import "XMPPMessage+XEP0045.h" +#import "NSXMLElement+XMPP.h" + + +@implementation XMPPMessage(XEP0045) + +- (BOOL)isGroupChatMessage +{ + return [[[self attributeForName:@"type"] stringValue] isEqualToString:@"groupchat"]; +} + +- (BOOL)isGroupChatMessageWithBody +{ + if ([self isGroupChatMessage]) + { + NSString *body = [[self elementForName:@"body"] stringValue]; + + return ([body length] > 0); + } + + return NO; +} + +- (BOOL)isGroupChatMessageWithSubject +{ + if ([self isGroupChatMessage]) + { + NSString *subject = [[self elementForName:@"subject"] stringValue]; + + return ([subject length] > 0); + } + + return NO; +} + +@end diff --git a/Extensions/XEP-0045/XMPPRoom.h b/Extensions/XEP-0045/XMPPRoom.h new file mode 100644 index 0000000..fd5eaa4 --- /dev/null +++ b/Extensions/XEP-0045/XMPPRoom.h @@ -0,0 +1,314 @@ +#import +#import "XMPP.h" +#import "XMPPRoomMessage.h" +#import "XMPPRoomOccupant.h" + +#define _XMPP_ROOM_H + +@class XMPPIDTracker; +@protocol XMPPRoomStorage; +@protocol XMPPRoomDelegate; + +static NSString *const XMPPMUCNamespace = @"http://jabber.org/protocol/muc"; +static NSString *const XMPPMUCUserNamespace = @"http://jabber.org/protocol/muc#user"; +static NSString *const XMPPMUCAdminNamespace = @"http://jabber.org/protocol/muc#admin"; +static NSString *const XMPPMUCOwnerNamespace = @"http://jabber.org/protocol/muc#owner"; + + +@interface XMPPRoom : XMPPModule +{ +/* Inherited from XMPPModule: + + XMPPStream *xmppStream; + + dispatch_queue_t moduleQueue; + id multicastDelegate; + */ + + __strong id xmppRoomStorage; + + __strong XMPPJID *roomJID; + + __strong XMPPJID *myRoomJID; + __strong NSString *myNickname; + __strong NSString *myOldNickname; + + __strong NSString *roomSubject; + + XMPPIDTracker *responseTracker; + + uint16_t state; +} + +- (id)initWithRoomStorage:(id )storage jid:(XMPPJID *)roomJID; +- (id)initWithRoomStorage:(id )storage jid:(XMPPJID *)roomJID dispatchQueue:(dispatch_queue_t)queue; + +/* Inherited from XMPPModule: + +- (BOOL)activate:(XMPPStream *)xmppStream; +- (void)deactivate; + +@property (readonly) XMPPStream *xmppStream; + +- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; +- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; +- (void)removeDelegate:(id)delegate; + +- (NSString *)moduleName; + +*/ + +#pragma mark Properties + +@property (readonly) id xmppRoomStorage; + +@property (readonly) XMPPJID * roomJID; // E.g. xmpp-development@conference.deusty.com + +@property (readonly) XMPPJID * myRoomJID; // E.g. xmpp-development@conference.deusty.com/robbiehanson +@property (readonly) NSString * myNickname; // E.g. robbiehanson + +@property (readonly) NSString *roomSubject; + +@property (readonly) BOOL isJoined; + +#pragma mark Room Lifecycle + +/** + * Sends a presence element to the join room. + * + * If the room already exists, then the xmppRoomDidJoin: delegate method will be invoked upon + * notifiaction from the server that we successfully joined the room. + * + * If the room did not already exist, and the authenticated user is allowed to create the room, + * then the server will automatically create the room, + * and the xmppRoomDidCreate: delegate method will be invoked (followed by xmppRoomDidJoin:). + * You'll then need to configure the room before others can join. + * + * @param desiredNickname (required) + * The nickname to use within the room. + * If the room is anonymous, this is the only identifier other occupants of the room will see. + * + * @param history (optional) + * A history element specifying how much discussion history to request from the server. + * E.g. + * For more information, please see XEP-0045, Section 7.1.16 - Managing Discussion History. + * You may also want to query your storage module to see how old the most recent stored message for this room is. + * + * @see fetchConfigurationForm + * @see configureRoomUsingOptions: +**/ +- (void)joinRoomUsingNickname:(NSString *)desiredNickname history:(NSXMLElement *)history; +- (void)joinRoomUsingNickname:(NSString *)desiredNickname history:(NSXMLElement *)history password:(NSString *)passwd; + +/** + * There are two ways to configure a room. + * 1.) Accept the default configuration + * 2.) Send a custom configuration + * + * To see which configuration options the server supports, + * or to inspect the default options, you'll need to fetch the configuration form. + * + * @see configureRoomUsingOptions: +**/ +- (void)fetchConfigurationForm; + +/** + * Pass nil to accept the default configuration. +**/ +- (void)configureRoomUsingOptions:(NSXMLElement *)roomConfigForm; + +- (void)leaveRoom; +- (void)destroyRoom; + +#pragma mark Room Interaction + +- (void)changeNickname:(NSString *)newNickname; +- (void)changeRoomSubject:(NSString *)newRoomSubject; + +- (void)inviteUser:(XMPPJID *)jid withMessage:(NSString *)invitationMessage; + +- (void)sendMessage:(XMPPMessage *)message; + +- (void)sendMessageWithBody:(NSString *)messageBody; + +#pragma mark Room Moderation + +- (void)fetchBanList; +- (void)fetchMembersList; +- (void)fetchModeratorsList; + +/** + * The ban list, member list, and moderator list are simply subsets of the room privileges list. + * That is, a user's status as 'banned', 'member', 'moderator', etc, + * are simply different priveleges that may be assigned to a user. + * + * You may edit the list of privileges using this method. + * The array of items corresponds with the stanzas of Section 9 of XEP-0045. + * This class provides helper methods to create these item elements. + * + * @see itemWithAffiliation:jid: + * @see itemWithRole:jid: + * + * The authenticated user must be an admin or owner of the room, or the server will deny the request. + * + * To add a member: +@required + +// +// +// -- PUBLIC METHODS -- +// +// There are no public methods required by this protocol. +// +// Each individual storage class will provide a proper way to access/enumerate the +// occupants/messages according to the underlying storage mechanism. +// + + +// +// +// -- PRIVATE METHODS -- +// +// These methods are designed to be used ONLY by the XMPPRoom class. +// +// + +/** + * Configures the storage class, passing it's parent and parent's dispatch queue. + * + * This method is called by the init method of the XMPPRoom class. + * This method is designed to inform the storage class of it's parent + * and of the dispatch queue the parent will be operating on. + * + * A storage class may choose to operate on the same queue as it's parent, + * as the majority of the time it will be getting called by the parent. + * If both are operating on the same queue, the combination may run faster. + * + * Some storage classes support multiple xmppStreams, + * and may choose to operate on their own internal queue. + * + * This method should return YES if it was configured properly. + * It should return NO only if configuration failed. + * For example, a storage class designed to be used only with a single xmppStream is being added to a second stream. + * The XMPPCapabilites class is configured to ignore the passed + * storage class in it's init method if this method returns NO. +**/ +- (BOOL)configureWithParent:(XMPPRoom *)aParent queue:(dispatch_queue_t)queue; + +/** + * Updates and returns the occupant for the given presence element. + * If the presence type is "available", and the occupant doesn't already exist, then one should be created. +**/ +- (void)handlePresence:(XMPPPresence *)presence room:(XMPPRoom *)room; + +/** + * Stores or otherwise handles the given message element. +**/ +- (void)handleIncomingMessage:(XMPPMessage *)message room:(XMPPRoom *)room; +- (void)handleOutgoingMessage:(XMPPMessage *)message room:(XMPPRoom *)room; + +/** + * Handles leaving the room, which generally means clearing the list of occupants. +**/ +- (void)handleDidLeaveRoom:(XMPPRoom *)room; + +@optional + +/** + * May be used if there's anything special to do when joining a room. +**/ +- (void)handleDidJoinRoom:(XMPPRoom *)room withNickname:(NSString *)nickname; + + +@end + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPRoomDelegate +@optional + +- (void)xmppRoomDidCreate:(XMPPRoom *)sender; + +/** + * Invoked with the results of a request to fetch the configuration form. + * The given config form will look something like: + * + * + * Configuration for MUC Room + * + * http://jabber.org/protocol/muc#roomconfig + * + * + * + * 0 + * + * ... + * + * + * The form is to be filled out and then submitted via the configureRoomUsingOptions: method. + * + * @see fetchConfigurationForm: + * @see configureRoomUsingOptions: +**/ +- (void)xmppRoom:(XMPPRoom *)sender didFetchConfigurationForm:(NSXMLElement *)configForm; + +- (void)xmppRoom:(XMPPRoom *)sender willSendConfiguration:(XMPPIQ *)roomConfigForm; + +- (void)xmppRoom:(XMPPRoom *)sender didConfigure:(XMPPIQ *)iqResult; +- (void)xmppRoom:(XMPPRoom *)sender didNotConfigure:(XMPPIQ *)iqResult; + +- (void)xmppRoomDidJoin:(XMPPRoom *)sender; +- (void)xmppRoomDidLeave:(XMPPRoom *)sender; + + +- (void)xmppRoomDidDestroy:(XMPPRoom *)sender; +- (void)xmppRoom:(XMPPRoom *)sender didFailToDestroy:(XMPPIQ *)iqError; + + +- (void)xmppRoom:(XMPPRoom *)sender occupantDidJoin:(XMPPJID *)occupantJID withPresence:(XMPPPresence *)presence; +- (void)xmppRoom:(XMPPRoom *)sender occupantDidLeave:(XMPPJID *)occupantJID withPresence:(XMPPPresence *)presence; +- (void)xmppRoom:(XMPPRoom *)sender occupantDidUpdate:(XMPPJID *)occupantJID withPresence:(XMPPPresence *)presence; + +/** + * Invoked when a message is received. + * The occupant parameter may be nil if the message came directly from the room, or from a non-occupant. +**/ +- (void)xmppRoom:(XMPPRoom *)sender didReceiveMessage:(XMPPMessage *)message fromOccupant:(XMPPJID *)occupantJID; + +- (void)xmppRoom:(XMPPRoom *)sender didFetchBanList:(NSArray *)items; +- (void)xmppRoom:(XMPPRoom *)sender didNotFetchBanList:(XMPPIQ *)iqError; + +- (void)xmppRoom:(XMPPRoom *)sender didFetchMembersList:(NSArray *)items; +- (void)xmppRoom:(XMPPRoom *)sender didNotFetchMembersList:(XMPPIQ *)iqError; + +- (void)xmppRoom:(XMPPRoom *)sender didFetchModeratorsList:(NSArray *)items; +- (void)xmppRoom:(XMPPRoom *)sender didNotFetchModeratorsList:(XMPPIQ *)iqError; + +- (void)xmppRoom:(XMPPRoom *)sender didEditPrivileges:(XMPPIQ *)iqResult; +- (void)xmppRoom:(XMPPRoom *)sender didNotEditPrivileges:(XMPPIQ *)iqError; + +@end diff --git a/Extensions/XEP-0045/XMPPRoom.m b/Extensions/XEP-0045/XMPPRoom.m new file mode 100644 index 0000000..1dec23e --- /dev/null +++ b/Extensions/XEP-0045/XMPPRoom.m @@ -0,0 +1,1174 @@ +#import "XMPP.h" +#import "XMPPRoom.h" +#import "XMPPIDTracker.h" +#import "XMPPMessage+XEP0045.h" +#import "XMPPLogging.h" + + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +enum XMPPRoomState +{ + kXMPPRoomStateNone = 0, + kXMPPRoomStateCreated = 1 << 1, + kXMPPRoomStateJoining = 1 << 3, + kXMPPRoomStateJoined = 1 << 4, + kXMPPRoomStateLeaving = 1 << 5, +}; + +@interface XMPPRoom () + +// List private methods here + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPRoom + +- (id)init +{ + // This will cause a crash - it's designed to. + // Only the init methods listed in XMPPRoom.h are supported. + + return [self initWithRoomStorage:nil jid:nil dispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + // This will cause a crash - it's designed to. + // Only the init methods listed in XMPPRoom.h are supported. + + return [self initWithRoomStorage:nil jid:nil dispatchQueue:queue]; +} + +- (id)initWithRoomStorage:(id )storage jid:(XMPPJID *)aRoomJID +{ + return [self initWithRoomStorage:storage jid:aRoomJID dispatchQueue:NULL]; +} + +- (id)initWithRoomStorage:(id )storage jid:(XMPPJID *)aRoomJID dispatchQueue:(dispatch_queue_t)queue +{ + NSParameterAssert(storage != nil); + NSParameterAssert(aRoomJID != nil); + + if ((self = [super initWithDispatchQueue:queue])) + { + if ([storage configureWithParent:self queue:moduleQueue]) + { + xmppRoomStorage = storage; + } + else + { + XMPPLogError(@"%@: %@ - Unable to configure storage!", THIS_FILE, THIS_METHOD); + } + + roomJID = [aRoomJID bareJID]; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + if ([super activate:aXmppStream]) + { + responseTracker = [[XMPPIDTracker alloc] initWithDispatchQueue:moduleQueue]; + + return YES; + } + + return NO; +} + +- (void)deactivate +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool { + + if (self.isJoined) + { + [self leaveRoom]; + } + + [responseTracker removeAllIDs]; + responseTracker = nil; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + [super deactivate]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Internal +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method may optionally be used by XMPPRosterStorage classes (method declared in XMPPRosterPrivate.h) +**/ +- (dispatch_queue_t)moduleQueue +{ + return moduleQueue; +} + +/** + * This method may optionally be used by XMPPRosterStorage classes (method declared in XMPPRosterPrivate.h). +**/ +- (GCDMulticastDelegate *)multicastDelegate +{ + return multicastDelegate; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id )xmppRoomStorage +{ + // This variable is readonly - set in init method and never changed. + return xmppRoomStorage; +} + +- (XMPPJID *)roomJID +{ + // This variable is readonly - set in init method and never changed. + return roomJID; +} + +- (XMPPJID *)myRoomJID +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return myRoomJID; + } + else + { + __block XMPPJID *result; + + dispatch_sync(moduleQueue, ^{ + result = myRoomJID; + }); + + return result; + } +} + +- (NSString *)myNickname +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return myNickname; + } + else + { + __block NSString *result; + + dispatch_sync(moduleQueue, ^{ + result = myNickname; + }); + + return result; + } +} + +- (NSString *)roomSubject +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return roomSubject; + } + else + { + __block NSString *result; + + dispatch_sync(moduleQueue, ^{ + result = roomSubject; + }); + + return result; + } +} + +- (BOOL)isJoined +{ + __block BOOL result = 0; + + dispatch_block_t block = ^{ + result = (state & kXMPPRoomStateJoined) ? YES : NO; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} +/* +- (BOOL)isRoomOwner +{ + __block BOOL result; + + dispatch_block_t block = ^{ + + id myOccupant = [xmppRoomStorage occupantForJID:myRoomJID stream:xmppStream]; + + result = [myOccupant.affiliation isEqualToString:@"owner"]; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} +*/ + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Create & Join +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)preJoinWithNickname:(NSString *)nickname +{ + if ((state != kXMPPRoomStateNone) && (state != kXMPPRoomStateLeaving)) + { + XMPPLogWarn(@"%@[%@] - Cannot create/join room when already creating/joining/joined", THIS_FILE, roomJID); + + return NO; + } + + myNickname = [nickname copy]; + myRoomJID = [XMPPJID jidWithUser:[roomJID user] domain:[roomJID domain] resource:myNickname]; + + return YES; +} + +- (void)joinRoomUsingNickname:(NSString *)desiredNickname history:(NSXMLElement *)history +{ + [self joinRoomUsingNickname:desiredNickname history:history password:nil]; +} + +- (void)joinRoomUsingNickname:(NSString *)desiredNickname history:(NSXMLElement *)history password:(NSString *)passwd +{ + dispatch_block_t block = ^{ @autoreleasepool { + + XMPPLogTrace2(@"%@[%@] - %@", THIS_FILE, roomJID, THIS_METHOD); + + // Check state and update variables + + if (![self preJoinWithNickname:desiredNickname]) + { + return; + } + + // + // + // + // passwd + // + // + + NSXMLElement *x = [NSXMLElement elementWithName:@"x" xmlns:XMPPMUCNamespace]; + if (history) + { + [x addChild:history]; + } + if (passwd) + { + [x addChild:[NSXMLElement elementWithName:@"password" stringValue:passwd]]; + } + + XMPPPresence *presence = [XMPPPresence presenceWithType:nil to:myRoomJID]; + [presence addChild:x]; + + [xmppStream sendElement:presence]; + + state |= kXMPPRoomStateJoining; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Room Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)handleConfigurationFormResponse:(XMPPIQ *)iq withInfo:(id )info +{ + XMPPLogTrace(); + + if ([[iq type] isEqualToString:@"result"]) + { + // + // + // + // Configuration for "coven" Room + // + // http://jabber.org/protocol/muc#roomconfig + // + // + // + // 0 + // + // ... + // + // + // + + NSXMLElement *query = [iq elementForName:@"query" xmlns:XMPPMUCOwnerNamespace]; + NSXMLElement *x = [query elementForName:@"x" xmlns:@"jabber:x:data"]; + + [multicastDelegate xmppRoom:self didFetchConfigurationForm:x]; + } +} + +- (void)fetchConfigurationForm +{ + dispatch_block_t block = ^{ @autoreleasepool { + + XMPPLogTrace(); + + // + // + // + + NSString *fetchID = [xmppStream generateUUID]; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMPPMUCOwnerNamespace]; + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:roomJID elementID:fetchID child:query]; + + [xmppStream sendElement:iq]; + + [responseTracker addID:fetchID + target:self + selector:@selector(handleConfigurationFormResponse:withInfo:) + timeout:60.0]; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)handleConfigureRoomResponse:(XMPPIQ *)iq withInfo:(id )info +{ + XMPPLogTrace(); + + if ([[iq type] isEqualToString:@"result"]) + { + [multicastDelegate xmppRoom:self didConfigure:iq]; + } + else + { + [multicastDelegate xmppRoom:self didNotConfigure:iq]; + } +} + +- (void)configureRoomUsingOptions:(NSXMLElement *)roomConfigForm +{ + dispatch_block_t block = ^{ @autoreleasepool { + + XMPPLogTrace(); + + if (roomConfigForm) + { + // Explicit configuration using given form. + // + // + // + // + // + // http://jabber.org/protocol/muc#roomconfig + // + // + // A Dark Cave + // + // + // 0 + // + // ... + // + // + // + + NSXMLElement *x = roomConfigForm; + [x addAttributeWithName:@"type" stringValue:@"submit"]; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMPPMUCOwnerNamespace]; + [query addChild:x]; + + NSString *iqID = [xmppStream generateUUID]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:roomJID elementID:iqID child:query]; + + [xmppStream sendElement:iq]; + + [responseTracker addID:iqID + target:self + selector:@selector(handleConfigureRoomResponse:withInfo:) + timeout:60.0]; + } + else + { + // Default room configuration (as per server settings). + // + // + // + // + // + // + + NSXMLElement *x = [NSXMLElement elementWithName:@"x" xmlns:@"jabber:x:data"]; + [x addAttributeWithName:@"type" stringValue:@"submit"]; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMPPMUCOwnerNamespace]; + [query addChild:x]; + + NSString *iqID = [xmppStream generateUUID]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:roomJID elementID:iqID child:query]; + + [xmppStream sendElement:iq]; + + [responseTracker addID:iqID + target:self + selector:@selector(handleConfigureRoomResponse:withInfo:) + timeout:60.0]; + } + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)changeNickname:(NSString *)newNickname +{ + myOldNickname = [myNickname copy]; + myNickname = [newNickname copy]; + myRoomJID = [XMPPJID jidWithUser:[roomJID user] domain:[roomJID domain] resource:myNickname]; + XMPPPresence *presence = [XMPPPresence presenceWithType:nil to:myRoomJID]; + [xmppStream sendElement:presence]; +} + +- (void)changeRoomSubject:(NSString *)newRoomSubject +{ + NSXMLElement *subject = [NSXMLElement elementWithName:@"subject" stringValue:newRoomSubject]; + + XMPPMessage *message = [XMPPMessage message]; + [message addAttributeWithName:@"from" stringValue:[myRoomJID full]]; + [message addChild:subject]; + + [self sendMessage:message]; +} + +- (void)handleFetchBanListResponse:(XMPPIQ *)iq withInfo:(id )info +{ + if ([[iq type] isEqualToString:@"result"]) + { + // + // + // + // Treason + // + // + // + + NSXMLElement *query = [iq elementForName:@"query" xmlns:XMPPMUCAdminNamespace]; + NSArray *items = [query elementsForName:@"item"]; + + [multicastDelegate xmppRoom:self didFetchBanList:items]; + } + else + { + [multicastDelegate xmppRoom:self didNotFetchBanList:iq]; + } +} + +- (void)fetchBanList +{ + dispatch_block_t block = ^{ @autoreleasepool { + + XMPPLogTrace(); + + // + // + // + // + // + + NSString *fetchID = [xmppStream generateUUID]; + + NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; + [item addAttributeWithName:@"affiliation" stringValue:@"outcast"]; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMPPMUCAdminNamespace]; + [query addChild:item]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:roomJID elementID:fetchID child:query]; + + [xmppStream sendElement:iq]; + + [responseTracker addID:fetchID + target:self + selector:@selector(handleFetchBanListResponse:withInfo:) + timeout:60.0]; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)handleFetchMembersListResponse:(XMPPIQ *)iq withInfo:(id )info +{ + if ([[iq type] isEqualToString:@"result"]) + { + // + // + // + // + // + + NSXMLElement *query = [iq elementForName:@"query" xmlns:XMPPMUCAdminNamespace]; + NSArray *items = [query elementsForName:@"item"]; + + [multicastDelegate xmppRoom:self didFetchMembersList:items]; + } + else + { + [multicastDelegate xmppRoom:self didNotFetchMembersList:iq]; + } +} + +- (void)fetchMembersList +{ + dispatch_block_t block = ^{ @autoreleasepool { + + XMPPLogTrace(); + + // + // + // + // + // + + NSString *fetchID = [xmppStream generateUUID]; + + NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; + [item addAttributeWithName:@"affiliation" stringValue:@"member"]; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMPPMUCAdminNamespace]; + [query addChild:item]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:roomJID elementID:fetchID child:query]; + + [xmppStream sendElement:iq]; + + [responseTracker addID:fetchID + target:self + selector:@selector(handleFetchMembersListResponse:withInfo:) + timeout:60.0]; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); + + +} + +- (void)handleFetchModeratorsListResponse:(XMPPIQ *)iq withInfo:(id )info +{ + if ([[iq type] isEqualToString:@"result"]) + { + // + // + // + // + // + + NSXMLElement *query = [iq elementForName:@"query" xmlns:XMPPMUCAdminNamespace]; + NSArray *items = [query elementsForName:@"item"]; + + [multicastDelegate xmppRoom:self didFetchModeratorsList:items]; + } + else + { + [multicastDelegate xmppRoom:self didNotFetchModeratorsList:iq]; + } +} + +- (void)fetchModeratorsList +{ + dispatch_block_t block = ^{ @autoreleasepool { + + // + // + // + // + // + + NSString *fetchID = [xmppStream generateUUID]; + + NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; + [item addAttributeWithName:@"role" stringValue:@"moderator"]; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMPPMUCAdminNamespace]; + [query addChild:item]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:roomJID elementID:fetchID child:query]; + + [xmppStream sendElement:iq]; + + [responseTracker addID:fetchID + target:self + selector:@selector(handleFetchModeratorsListResponse:withInfo:) + timeout:60.0]; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)handleEditRoomPrivilegesResponse:(XMPPIQ *)iq withInfo:(id )info +{ + if ([[iq type] isEqualToString:@"result"]) + { + [multicastDelegate xmppRoom:self didEditPrivileges:iq]; + } + else + { + [multicastDelegate xmppRoom:self didNotEditPrivileges:iq]; + } +} + +- (NSString *)editRoomPrivileges:(NSArray *)items +{ + NSString *iqID = [xmppStream generateUUID]; + + dispatch_block_t block = ^{ @autoreleasepool { + + XMPPLogTrace(); + + // + // + // + // + // + // + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMPPMUCAdminNamespace]; + for (NSXMLElement *item in items) + { + [query addChild:item]; + } + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:roomJID elementID:iqID child:query]; + + [xmppStream sendElement:iq]; + + [responseTracker addID:iqID + target:self + selector:@selector(handleEditRoomPrivilegesResponse:withInfo:) + timeout:60.0]; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); + + return iqID; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Leave & Destroy +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)leaveRoom +{ + dispatch_block_t block = ^{ @autoreleasepool { + + XMPPLogTrace(); + + // + + XMPPPresence *presence = [XMPPPresence presence]; + [presence addAttributeWithName:@"to" stringValue:[myRoomJID full]]; + [presence addAttributeWithName:@"type" stringValue:@"unavailable"]; + + [xmppStream sendElement:presence]; + + state &= ~kXMPPRoomStateJoining; + state &= ~kXMPPRoomStateJoined; + state |= kXMPPRoomStateLeaving; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)handleDestroyRoomResponse:(XMPPIQ *)iq withInfo:(id )info +{ + XMPPLogTrace(); + + if ([iq isResultIQ]) + { + [multicastDelegate xmppRoomDidDestroy:self]; + } + else + { + [multicastDelegate xmppRoom:self didFailToDestroy:iq]; + } +} + +- (void)destroyRoom +{ + dispatch_block_t block = ^{ @autoreleasepool { + + XMPPLogTrace(); + + // + // + // + // + // + + NSXMLElement *destroy = [NSXMLElement elementWithName:@"destroy"]; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMPPMUCOwnerNamespace]; + [query addChild:destroy]; + + NSString *iqID = [xmppStream generateUUID]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:roomJID elementID:iqID child:query]; + + [xmppStream sendElement:iq]; + + [responseTracker addID:iqID + target:self + selector:@selector(handleDestroyRoomResponse:withInfo:) + timeout:60.0]; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Messages +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)inviteUser:(XMPPJID *)jid withMessage:(NSString *)inviteMessageStr +{ + dispatch_block_t block = ^{ @autoreleasepool { + + XMPPLogTrace(); + + // + // + // + // + // Hey Hecate, this is the place for all good witches! + // + // + // + // + + NSXMLElement *invite = [NSXMLElement elementWithName:@"invite"]; + [invite addAttributeWithName:@"to" stringValue:[jid full]]; + + if ([inviteMessageStr length] > 0) + { + [invite addChild:[NSXMLElement elementWithName:@"reason" stringValue:inviteMessageStr]]; + } + + NSXMLElement *x = [NSXMLElement elementWithName:@"x" xmlns:XMPPMUCUserNamespace]; + [x addChild:invite]; + + XMPPMessage *message = [XMPPMessage message]; + [message addAttributeWithName:@"to" stringValue:[roomJID full]]; + [message addChild:x]; + + [xmppStream sendElement:message]; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)sendMessage:(XMPPMessage *)message +{ + dispatch_block_t block = ^{ @autoreleasepool { + + XMPPLogTrace(); + + [message addAttributeWithName:@"to" stringValue:[roomJID full]]; + [message addAttributeWithName:@"type" stringValue:@"groupchat"]; + + [xmppStream sendElement:message]; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)sendMessageWithBody:(NSString *)messageBody +{ + if ([messageBody length] == 0) return; + + NSXMLElement *body = [NSXMLElement elementWithName:@"body" stringValue:messageBody]; + + XMPPMessage *message = [XMPPMessage message]; + [message addChild:body]; + + [self sendMessage:message]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender +{ + // This method is invoked on the moduleQueue. + + XMPPLogTrace(); + + state = kXMPPRoomStateNone; + + // Auto-rejoin? +} + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + NSString *type = [iq type]; + + if ([type isEqualToString:@"result"] || [type isEqualToString:@"error"]) + { + return [responseTracker invokeForID:[iq elementID] withObject:iq]; + } + + return NO; +} + +- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence +{ + // This method is invoked on the moduleQueue. + + XMPPJID *from = [presence from]; + + if (![roomJID isEqualToJID:from options:XMPPJIDCompareBare]) + { + return; // Stanza isn't for our room + } + + XMPPLogTrace(); + + [xmppRoomStorage handlePresence:presence room:self]; + + // My presence: + // + // + // + // + // + // + // + // + // + // + // Another's presence: + // + // + // + // + // + // + + NSXMLElement *x = [presence elementForName:@"x" xmlns:XMPPMUCUserNamespace]; + + // Process status codes. + // + // 110 - Inform user that presence refers to one of its own room occupants. + // 201 - Inform user that a new room has been created. + // 210 - Inform user that service has assigned or modified occupant's roomnick. + // 303 - Inform all occupants of new room nickname. + + BOOL isMyPresence = NO; + BOOL didCreateRoom = NO; + BOOL isNicknameChange = NO; + + for (NSXMLElement *status in [x elementsForName:@"status"]) + { + switch ([status attributeIntValueForName:@"code"]) + { + case 110 : isMyPresence = YES; break; + case 201 : didCreateRoom = YES; break; + case 210 : + case 303 : isNicknameChange = YES; break; + } + } + + // Extract presence type + + NSString *presenceType = [presence type]; + + BOOL isAvailable = [presenceType isEqualToString:@"available"]; + BOOL isUnavailable = [presenceType isEqualToString:@"unavailable"]; + + // Server's don't always properly send the statusCodes in every situation. + // So we have some extra checks to ensure the boolean variables are correct. + + if (didCreateRoom) + { + isMyPresence = YES; + } + if (!isMyPresence) + { + if ([[from resource] isEqualToString:myNickname]) + isMyPresence = YES; + } + if (!isMyPresence && isNicknameChange && myOldNickname) + { + if ([[from resource] isEqualToString:myOldNickname]) { + isMyPresence = YES; + myOldNickname = nil; + } + } + + XMPPLogVerbose(@"%@[%@] - isMyPresence = %@", THIS_FILE, roomJID, (isMyPresence ? @"YES" : @"NO")); + XMPPLogVerbose(@"%@[%@] - didCreateRoom = %@", THIS_FILE, roomJID, (didCreateRoom ? @"YES" : @"NO")); + XMPPLogVerbose(@"%@[%@] - isNicknameChange = %@", THIS_FILE, roomJID, (isNicknameChange ? @"YES" : @"NO")); + + // Process presence + + if (didCreateRoom) + { + state |= kXMPPRoomStateCreated; + + [multicastDelegate xmppRoomDidCreate:self]; + } + + if (isMyPresence) + { + if (isAvailable) + { + myRoomJID = from; + myNickname = [from resource]; + + if (state & kXMPPRoomStateJoining) + { + state &= ~kXMPPRoomStateJoining; + state |= kXMPPRoomStateJoined; + + if ([xmppRoomStorage respondsToSelector:@selector(handleDidJoinRoom:withNickname:)]) + [xmppRoomStorage handleDidJoinRoom:self withNickname:myNickname]; + [multicastDelegate xmppRoomDidJoin:self]; + } + } + else if (isUnavailable && !isNicknameChange) + { + state = kXMPPRoomStateNone; + [responseTracker removeAllIDs]; + + [xmppRoomStorage handleDidLeaveRoom:self]; + [multicastDelegate xmppRoomDidLeave:self]; + } + } + else + { + if (isAvailable) + { + [multicastDelegate xmppRoom:self occupantDidJoin:from withPresence:presence]; + } + else if (isUnavailable) + { + [multicastDelegate xmppRoom:self occupantDidLeave:from withPresence:presence]; + } + } +} + +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message +{ + // This method is invoked on the moduleQueue. + + XMPPJID *from = [message from]; + + if (![roomJID isEqualToJID:from options:XMPPJIDCompareBare]) + { + return; // Stanza isn't for our room + } + + XMPPLogTrace(); + + // Is this a message we need to store (a chat message)? + // + // A message to all recipients MUST be of type groupchat. + // A message to an individual recipient would have a . + + BOOL isChatMessage; + + if ([from isFull]) + isChatMessage = [message isGroupChatMessageWithBody]; + else + isChatMessage = [message isMessageWithBody]; + + if (isChatMessage) + { + [xmppRoomStorage handleIncomingMessage:message room:self]; + [multicastDelegate xmppRoom:self didReceiveMessage:message fromOccupant:from]; + } + else if ([message isGroupChatMessageWithSubject]) + { + roomSubject = [message subject]; + } + else + { + // Todo... Handle other types of messages. + } +} + +- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message +{ + // This method is invoked on the moduleQueue. + + XMPPJID *to = [message to]; + + if (![roomJID isEqualToJID:to options:XMPPJIDCompareBare]) + { + return; // Stanza isn't for our room + } + + XMPPLogTrace(); + + // Is this a message we need to store (a chat message)? + // + // A message to all recipients MUST be of type groupchat. + // A message to an individual recipient would have a . + + BOOL isChatMessage; + + if ([to isFull]) + isChatMessage = [message isGroupChatMessageWithBody]; + else + isChatMessage = [message isMessageWithBody]; + + if (isChatMessage) + { + [xmppRoomStorage handleOutgoingMessage:message room:self]; + } +} + +- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error +{ + // This method is invoked on the moduleQueue. + + XMPPLogTrace(); + + state = kXMPPRoomStateNone; + [responseTracker removeAllIDs]; + + [xmppRoomStorage handleDidLeaveRoom:self]; + [multicastDelegate xmppRoomDidLeave:self]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Class Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (NSXMLElement *)itemWithAffiliation:(NSString *)affiliation jid:(XMPPJID *)jid +{ + NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; + + if (affiliation) + [item addAttributeWithName:@"affiliation" stringValue:affiliation]; + + if (jid) + [item addAttributeWithName:@"jid" stringValue:[jid full]]; + + return item; +} + ++ (NSXMLElement *)itemWithRole:(NSString *)role jid:(XMPPJID *)jid +{ + NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; + + if (role) + [item addAttributeWithName:@"role" stringValue:role]; + + if (jid) + [item addAttributeWithName:@"jid" stringValue:[jid full]]; + + return item; +} + +@end diff --git a/Extensions/XEP-0045/XMPPRoomMessage.h b/Extensions/XEP-0045/XMPPRoomMessage.h new file mode 100644 index 0000000..6b395ea --- /dev/null +++ b/Extensions/XEP-0045/XMPPRoomMessage.h @@ -0,0 +1,58 @@ +#import + +@class XMPPJID; +@class XMPPMessage; + + +@protocol XMPPRoomMessage + +/** + * The raw message that was sent / received. +**/ +- (XMPPMessage *)message; + +/** + * The JID of the MUC room. +**/ +- (XMPPJID *)roomJID; + +/** + * Who sent the message. + * A typical MUC room jid is of the form "room_name@conference.domain.tld/some_nickname". +**/ +- (XMPPJID *)jid; + +/** + * The nickname of the user who sent the message. + * This is a convenience method for [jid resource]. +**/ +- (NSString *)nickname; + +/** + * Convenience method to access the body of the message. +**/ +- (NSString *)body; + +/** + * When the message was sent / received (as recorded by us). + * + * If the message was originally sent by us, the localTimestamp is recorded automatically. + * If the message was received, the server may have included a delayed delivery date timestamp. + * This is the case when first joining a room, and downloading the discussion history. + * In such a case, the localTimestamp will be a reflection of the serverTimestamp. +**/ +- (NSDate *)localTimestamp; + +/** + * When the message was sent / received (as recorded by the server). + * + * Only set when the server includes a delayedDelivery timestamp within the message. +**/ +- (NSDate *)remoteTimestamp; + +/** + * Whether or not the message was sent by us. +**/ +- (BOOL)isFromMe; + +@end diff --git a/Extensions/XEP-0045/XMPPRoomOccupant.h b/Extensions/XEP-0045/XMPPRoomOccupant.h new file mode 100644 index 0000000..1e7e77b --- /dev/null +++ b/Extensions/XEP-0045/XMPPRoomOccupant.h @@ -0,0 +1,57 @@ +#import + +@class XMPPJID; +@class XMPPPresence; + + +@protocol XMPPRoomOccupant + +/** + * Most recent presence message from occupant. +**/ +- (XMPPPresence *)presence; + +/** + * The MUC room the occupant is associated with. +**/ +- (XMPPJID *)roomJID; + +/** + * The JID of the occupant as reported by the room. + * A typical MUC room will use JIDs of the form: "room_name@conference.domain.tl/some_nickname". +**/ +- (XMPPJID *)jid; + +/** + * The nickname of the user. + * In other words, the resource portion of the occupants JID. +**/ +- (NSString *)nickname; + +/** + * The 'role' and 'affiliation' of the occupant within the MUC room. + * + * From XEP-0045, Section 5 - Roles and Affiliations: + * + * There are two dimensions along which we can measure a user's connection with or position in a room. + * One is the user's long-lived affiliation with a room -- e.g., a user's status as an owner or an outcast. + * The other is a user's role while an occupant of a room -- e.g., an occupant's position as a moderator with the + * ability to kick visitors and participants. These two dimensions are distinct from each other, since an affiliation + * lasts across visits, while a role lasts only for the duration of a visit. In addition, there is no one-to-one + * correspondence between roles and affiliations; for example, someone who is not affiliated with a room may be + * a (temporary) moderator, and a member may be a participant or a visitor in a moderated room. + * + * For more information, please see XEP-0045. +**/ +- (NSString *)role; +- (NSString *)affiliation; + +/** + * If the MUC room is non-anonymous, the real JID of the user will be broadcast. + * + * An anonymous room uses JID's of the form: "room_name@conference.domain.tld/some_nickname". + * A non-anonymous room also includes the occupants real full JID in the presence broadcast. +**/ +- (XMPPJID *)realJID; + +@end diff --git a/Extensions/XEP-0045/XMPPRoomPrivate.h b/Extensions/XEP-0045/XMPPRoomPrivate.h new file mode 100644 index 0000000..0240f93 --- /dev/null +++ b/Extensions/XEP-0045/XMPPRoomPrivate.h @@ -0,0 +1,19 @@ +#import "XMPPRoom.h" + + +@interface XMPPRoom (PrivateInternalAPI) + +/** + * XMPPRoomStorage classes may optionally use the same delegate(s) as their parent XMPPRoom. + * This method allows such storage classes to access the delegate(s). + * + * Note: If the storage class operates on a different queue than its parent, + * it MUST dispatch all calls to the multicastDelegate onto its parent's queue. + * The parent's dispatch queue is passed in the configureWithParent:queue: method, + * or may be obtained via the moduleQueue method below. +**/ +- (GCDMulticastDelegate *)multicastDelegate; + +- (dispatch_queue_t)moduleQueue; + +@end diff --git a/Extensions/XEP-0054/CoreDataStorage/XMPPvCard.xcdatamodeld/.xccurrentversion b/Extensions/XEP-0054/CoreDataStorage/XMPPvCard.xcdatamodeld/.xccurrentversion new file mode 100644 index 0000000..f9bae00 --- /dev/null +++ b/Extensions/XEP-0054/CoreDataStorage/XMPPvCard.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + XMPPvCard.xcdatamodel + + diff --git a/Extensions/XEP-0054/CoreDataStorage/XMPPvCard.xcdatamodeld/XMPPvCard.xcdatamodel/elements b/Extensions/XEP-0054/CoreDataStorage/XMPPvCard.xcdatamodeld/XMPPvCard.xcdatamodel/elements new file mode 100644 index 0000000000000000000000000000000000000000..ff516d13606774de5ea9b9e18329bf2ae705fb55 GIT binary patch literal 36011 zcmeIbcU)6v`v-hZ_96*e2qSF5kTAjy5CQ}Ugpfc81PCFEKo~OI)f2bYS!cD@Sx2>6 z+gi0&TkEQOZ`;~xYg?_gu4=XVo`eL#q4o(rzxVO|te7G#Qv$f3y z04FWq>!yz$#Dy zR)Y=TXz&ei3^*2qKod9)91l(a=YlK2E#OY@b8sKHA3Oja1iuC^fEU4=;3E_k<%VLS z*r-@k94a1_fa0JMQMsrpR41wnH3~HuwGg!iwH37;br5wPbq#d`brbanjYZ?o9%xVW zAhZ|S8_hubpkvW-=u~tXT7Xud8_^@sQ_ydr7o*psx1$fD&!c}t|A7H9C=42d!C)~s z3?Acw3Be>__!tRBg{j9hV#Z=1%oNN#%sR|A%mK_f%y*cZm`7MF)(snkjl*VOMc5i_ zJ9avD26irX8Fn*vH})9zB=$S(9qeQ5Gwcf-73Yid!-e9)aN#&EE(4c`li+l?F5Cp% zT-;LJd$><>Kq$>dq&CFITI-Q;8BtK^@^ zcgTNJC=>=If|5eXrIb@Nln%-`${fma%4W)kl#eN&P(G)8O}R?BMfuB#=;Z3eaEfqB zamsh9aMC%oJB@RiH+0E;W~0O4U+3spF|{QQx7i zrhZKQih7EAjru$FDUD3?rm<+rGyzRU)6#S_1Fe}hgw{oSg9gziln1wT8I2bJB0vJj z00nRYr~nP11I~a8;0phB2RxK?Pi1qfMr)LHdryHz+tO*!8MN(6y1P={+M+Y)yV_Nq zuw!*(j}WPvwbjbu8u&zWGkjr(ELd$+b#y4{bn{UQP_e2-rPsFC^c*n1NJ*!epSQQR zc8XQ%CinsfFo7K=u-yc9LI8w<(3SFHv3Mw7)voy#@B+L62H*qu0)Bu$+;Aok00aU- zKrj#jgaTneIKYDc(t#)-8W;@30Bp;%P$k_*o|E0BZqjz+Vi`q$yDNIz*!!mDiRfxE z4C&J58nxybD#%gN{p?=U(}3RBKv)ylW&&Fv5W<>grPTxup#u;O!kC{!Sh2J+S9%7X zg(M&uNC8rTj0%x-aFcdKM-}|n8f>BPRX`e$4sannM1Y8^fJ`6@;6Ws)3RlfDksG zBA^&|q32 z*zQzD!^m#8)pWs}saC;l@B40u%N%&Of9yK~#*u5RtyU-v9lZLE&UTf$6QV$aVROSm zXvhgV0so~yr(jc}n80R;OmGQ0zHvr{6}s0HHGmEZUjb+#>Iy&)(Q4&6N^$StTml+_ zCcp?=fjNK#T4%lmp6yIv2t4arIDq^g&b9pvymTVrZqeb+k)UXZ3IT-Pt_%alK;dhE z;lK!BBrpmX4ZHz4LoSdjyh5MQqn!F$IM*OIl>aE%u)V3It`c(%m8M>V?GO*4a|YZ{Vj{<%?0Mc z<3ArB@rCe@9#C5T4~Bog3-{Ul44#W}rP({=N{v>h>N0j#E8+Ovsy0}xxDvJ%X(#+p zZGKd432|ook!V$#k{08LD)@1ZwjIv)H0FmT-CkM^KjgPIx3zW{I<*z>lOnB32gkHQ z@DGgu|AQEiGyEGMobO{MbYKl^AZyKyg0J+3yx=R8bT@g9#n=sMiB=68qov=cfsMci zz$RdGRW}uyN|2WVx=Oxor*bA+QDb2>2NI1lZd5l#=e!d#PnA;Oi}8 z55L|#v>k0Q8Cj0Ot*{=`*kZi-Qh4Iv!@Wo3B58ME?QY;UU^~nWmTScAt!>)&&Ji%$ zYG4QO8L$)B`VZ$U{dMG>9GUgq{bqfHZPwud_O+W17t3^QvPmwH8ZW1)Km*0b5}*vOIO98T5Y57-`oG*u{FR!qw6YM=-DV!yILY8DNL$ zX2IP= z;FM%=caqjQ!a9=%b7YTw$+fw|)y6K3Hm|+4+0428233n*%QM0Tprrf1VVikOf0Bo%mNqdhuFrXm0 z-++#`8_)>50d=MnBLu>_DUJKs_v=4&^M}0}uhiVYu6Ud&viOuC-tk7!3{v zV?Z_-Yj&}JxaR-uD9~eUU;>x~g|7rTU?LO)u~&l0Udfcj_%SO z!$Gb&ih~(&9A^S^z|4M;-0VzdE>(hg@KBm#c`5u@2y)B=3znF<)q(uyKP5IVId9Dc z;CVCuhZ&Us#lsUvAwa)CG4O*pg6j+T-|x{0JA22V46HDZK{+Uc5+Ke>P!3i?iICVb zGfJ`9n|deE5)qYRiL&?5C9npp1(ki6q7+mCpw8PD*H0&6*w|aGxD6&6@Dt!6@Bqqzf}x;Q;C2hq*{Q6HYZ&Y zxbQJ{nWskF3KtV2da-wdd(1xd1^6YD2MJb!Ux9m}e5e4X1aZ4or)`G~hHo@ag}Lli z(rG@C$7>mRo0+|3bLEHO4t*`)}SS^ z8iSSAQF&%sM+v%V9hLtQTF(NMD44=~X&qH=#z(<{@_$L|RRj}gc4Z5%HxuAzsc^bS zM=4O%s2Wr);D=J8>HsE6g{n8#nJ6_%1J{|b!h_Pmxe2WBpc?HIo^<&55EN|T|Ek_t zQ;UDSrbl;iAj5G;80f1e_U0VD`lCGzLUZ~5til7!9gVPSnpK_Z9#sdPO%t4aD!MbY zd7b-*^{@$Q7;1z~(Ls%bYA&HhquxM`L4+F!p4hC{DdE(@J*q-VIH;+p+5aZtpyr_7 zLd`|ZL(NAmfRs=jR1c{kJ=EZUx0^*1RClFn_4Z!T1om{Zno-k7&2Rt{wXPqgeMzpe z!|YR%qqf1tp9A=2x(c>~O7(I@Ax-zSh>*wHZ-sqb{PZB5Q5bHz5~o$an!9T-5=<9?X-C9A=%v$m4;KPc!uZiXhH zX|SoHiD(k498Ezxp{Y<4WQ3Zb7N`|!TZ5*fol(hXSF{`29U217fhwW9CXi?X0~GL@ zN@sbS23C_bwU(N(TZ)I-vAU;(gr6wsv|%a(OpSVYAtui(LdqHq8fj;HWm`jQXRBFr z>?t(S=2f0qXuoboLi@j*kg`<5*_ouW+bSin~-KhP4I(uf=dE-2CH{$ z6VR4IyBwVeS7etIr=z*(jK1*=odtxV>94b*916#`A;^jn zU4WK2QcqB;ce~k0ynG` z!13O)8s%v&j4YSHrIAc!?9$rK!0Mj$jCIk+a+U`xP5oY}8vc@34mAT>6dzq{d$YFP zyj)^kqO!cF%C>fERv;X(adYN^PO*N6lO4sxl=YjVCw7>#?B-~S1=ATl!J^h<)EEscx{Hfp z)97B&UxU$NbQnFR0b{VN%GsT&HeYC4F8lA6$gI^jrpcV^V2s_l4yO6#xelfi?oW@X zjwYFVhk+Bo|84aR&4owLQhXcX2XA$kY#$!HDefUC%s9+Mdr=)T2{Rd*2Q9yZnTnZ) zd9!cu!_0t#pY!WXZRW$lZw@lG!7RWmb0oFFEXS4Lg_(6Or0r zE?}-8Qya`xOKP*YPilkt9&_VWQyW+aM)cS|wJ{69w$?eG+Vsb?Pi^cm2b9_%+KEhU zZ0#ISYJx=nnL|1Kx}k!*)2?-kbZ#u&ZD@TyFJdgba&?wM$?Y zR@qGi*t(Yx0S{1O&AVlKhyZKxLM&`W|4SlRO@PkWh+r%H;IvtU#dcsjv0d1qfFE`k zb~wPqj=+vIi?G;H*wNi0EOtz{2#XzOC&F@pQtVXhwEtg(eW~R8j{-D8@^$1It4Ixt zu%EzwVHRQ6^oX!T_~=RKJ%WquZCqy$nPX>R=h*aD>|4;95%J*!Qs;up6--U^hYQq4%MU&I&0Hk9onGW<7_x4E(p$lu+6wYXe;#TN~{SoIk;TtGw8r2oB)@PGjHIw+q#Pr0-?B&*I7L5gyYUOWbuG2!__!aJm6|^ zN?aXIg{#M@aT=T!`W)H??S}S1UqO4Jeb9afOww%TxE%X9)USB>>{W^f1SYb0uwgn} zJm4nvYo~qj@CEETHcUkEfSZqd8(BQy7F*)Pmwk!{+%nwCS1lg;kD`6?VAe%#t#iD1 z=#Ob%JlJCnsCYok46=BzwR1ql0|FCSJlHS?R6HOsk;Q`z(^@>>{=nVC-A8!$Lz{QM zE?xAX)w}!XqPXXH?7!)vcpM&&C*X;A5}piw4IP3GLr0*a&@l(R+04+mTzhZsr!yX~ zdh>vEMg%5OXS87sNM}S~B6UU^=74lY1SV2vv|&zil(7F{Jdrx14Rf-inE1+mb7ZeG z9*1Lv4ReYG(;5A9Rqu8TYZ`-Zgwq&{{D?Q=o2$&KVy@+r6KnA;_*Q%yeh9uD-vMm3 zI|FN!@ZNjdCkHHxhqmYcCm#=RK{3Lv_V6%%7=EOMhpjtf@uTpgp|jAnOZYMPv3RJj zr{c!}p>W~*8h5;&gFST>l85nc;^#TS!}$651^9*dMfkVzi}6eF??C6F3(!UA3Un3v z2KpBI&H>Lg^Dw?U^L?3!clK3y7=el8VH>8yJdEGfubuWhdI1TG-AcGz7mTD`fCh&Kne!)sP=wuyNEdA%>IA7-$H>FJhF zlvUYbN*(1Qg1R4Oh#lsSc9>-rOgiX5@BabSyvxS&fjk(|9<}_xXld9>FlCNnT4HYB zSqitqG-uehS*oyLTKJZ*jIi9Ya3?Od-@v(4cAJVLhH{`^JM9hSH@kLr8wwre2r6vUh?XKv#n#dR znF<0EX(~3%Zd2(|SP15#WESB;x57er_!5N$-W*MM1{ompd3H9`0zdeJ;3Amslifq&L=w@-lGRz&6(W^LgYH8wE)kuHE=1S9ai8c8FW$Jm z&aCbM9QW@cvpS+5G0c&yju=j45hI9^#3*7kaWF9kdI&v&{)GO5oqPx@qa zL<7pxOxvB3^VXhHNJDYV!y9Tr+h!TtJgC$yMAcr~e~|5Ggaoim!* z!`q~IGjEd;x_O(#c{y*B();kXli4yzun70RTunEBsEKSN?pE`Mnn>MiBfng{+}6Y1 zq(V}$J$sWxq!JSde{^QhB~mF#LX!6Na8fz!;cl;yzfGW%`J04@NCqd>lJt%+IH`eT zAT^SjNJdgKsf7f83lY@>(o7)T1iG3)HxuY?0zFKirvpZBW^$6q-szF+B=A7K3Y8;3 z5mep_>M)m+X7_8U9haLxXY&^_*_w)AbJ8->DkPheR-4(}1iJL0bJBX!##g0tSfoMp z+Mdo~k*2qO1LAW8D1y&>K?g+W2v7u}_ks?H(Gj2sM(+h35TzqP5tQBwnrVr3mq<@Z z&q#kGJpP5v<6j%6d)Yj`52=$0WXs2kUbdH*nMm*9nj(lqNNBK*~wht-+({kPIO>hm62C%2nvo!rq)>*UUt(>mF##t(*nO5sn0 zMWU%@+aOQ)-_v@Kjn>=Dv>w<^>&Kj(eS2t~Je54%p4Q1T$TLk~unA;eBF`qzA-~nv z$;tCzCwG6Xv>sx1@&F{Qlb4d$I6~{>wd8f=_2l=-8^{~UACNbhz)%wyW&*=aV5A9* zGJ(-1aIguCalq}(v`(I3@Amy^J@{2<9Rb>p)@`5;(>nP?zn1o)b(Y!gZKw!ZCx1)+ z0ZHrR>)o^-(TCQ_Ka+30Dy{b)N_$#|tMuOXIZo?vu7DUyds?^T3bsxUh}IFH{b=0= zIv`p{fcB$x8|Z*&9Rb>p)@`6xTBoE^(kSVDJ)V+b^Z3_B>#;VE??dYp0Y&t0wK}DQ zBBqp5Bory7%ml`nz<3jwU;;TNFwq1iIpE)BvLw&6_wW9+&VChIM}YRDbsOjeOS1K9 zE$v6^HqZglI-;fhXx#=nAX-O&_M>$hsKc~QS=n!*`p|l^IegeqduZLd8G`Z=ycxps zAQfdj^9rbWtU z_HNUk7U6IU+cpCl&^K5iK>N|64b)*;q&(}_(mu4vGuw>~6+w$mWGA?&==&vBPV{bC z%FUX>PM21E4Po)#?(W^3PoXb}P0j}~p9@Lzb_^s7yJKU%bb4u}>JE$v5( zHqZglA_BA@E!se>wCJRFYH%|2b$F+y9*2KD`w~oGfz{#rY)o+KbQ<+<8xx#HJH6pF z#%Zh*F50uwPyJ(vTU zBm^d6l6o+0b7cMKHZ>d$NVBM{ZZU!y@e(m26HroPVQ(D_|5(HbFwyM0)YSj27(s&N z2XhB3n*-of%mm81#fZ%(hM((6x~Q4dY>tczMht)(idbyO9#o~ov5OkkA>RG7eO6R0$SbtX_{0_#nn z+Cc)Q^6kTN|Ex=H^~?cfT?o*AS(gpe;jD`~5uhVL`(#};=3s9_MPyyndDKP7tc&_~ zch*(gC+niVOI`7*Sr?qZA{uC)by>s+Tl)r-bs<3eWnDJV0cBkX(0*B$4RkOF+VKd^cHYZD{1HjnQkMo|BzVg5~wpkZk^8lFa= z5osh7s560j6WCw^4JNSB1U5O~-)1p_nq%+Z{ly54)xQU{a{vL_PmHjE4oHkZfc6t3 zY@h=YBM_kd#0VScfW!y{Xg@K+2I{aFL6i5Js6JwZ(H!1vs8cMttTUcq6(eX3uo%%p zhqNXdyzObZ-GNqGTi*j6v`+H@%jQzrP}(r~o+FmK0a@=Rw1zgEHi9;iHi|YH*oru( zwBDn^dg*_Ag9lp=#`fvKINJE$_D!Tssx*Jt#L~i5w8^w7z}DUa71pN1?Gj0a+7a+w zZmjo9J4JgFV3nC~)&zk70lphkAP@=PD=ihsL`S3J(PH?HV&l-W&HDH^t&Dd7#5X-j-498A{?2i_zC!R_@nrX_{;F!!M?>`!~cN4f&U5rGyYfnZTublUHpCgL;NHB zQv#YmC%6#Y37!ORf)62*ASQGY77@N6>>=zW>?a%~93mVc93vbjTp@fz_?2**aEEZ0 zaF6hS@Q8>eV&S`q5s74?6Ol#?A@Yc|#5Upt;!@&O;t#}I#K$BQi9#Ag3L*_ArIGlg zT=-661tcMLhdWwP4NNJ)pQ(7n=!1o~A0^fsdE3C3@ zqim<_pzQ1x)hK%?Ur|m_PQfzNW%%A?-%@_0+@$>{4bZ@#3-Iwl1XVL@cLG%!MC_RkMqDRo<=~?t_x`19l7t+Oa z3B8PNppT}npzo*u;!JmTadva|a319B?d;?1=j`tszHoi)xn=LY9SXQOkAbDQ%_=Z(%^IG=I;*#+z3JGb~)m5)a97VahH=Wr(MptoOL3QZb&#vK ztBNd=6xZ4P~k#3{i#<-1jgWSfsjd%OXZJ*l#x3Ap}yB%>m>UPZSxZ6p$ z({5+n&bpm5beWd#+_tEZS+{e1FcK_P_wuh^SyN9QTmj}bc*Tdf2&%Jhged+a; z*FLWUUWdGncpddR=kMCzj)pDy5sHYo#d_X9_Ky5dy@AQ?`huCy=QvQ_MYSY zuJ>~954<;fZ}I-vd#m?0?;YL;yubE7?0wYxxc5o#(+p>ZE5n`P$?#%$GZ+jXhA+dP z5x@v!1TjJwp$siU&oD5W7|o0pMk}L@F@({<=wb|I3}cL7jAV>wOk_-A>}MQg9AX?{ z9Ag}3oM4<}oMN0|oMW74Twq*cT=wzuVfgs^`1=I-1o{N|1p9>eg!!<1B77o!qJ0MY zr1@}ta(wc9@_h<@N`0h0htEeopZI+0v)$)2pPfFR`|R@B z?enG2S3Y}v_W2y}Ip~Y?CHRtjDZW%+nlIhg+1JI_&DX=%(|3@sw=cst-M7G3=qvZF z@~!r*^{w-*_tp3g@$K;K@*U5&7yGXD-QoMC?_oc8zhpnT-*CT?exv=y z_(6W-{KorD^qb^2*>9@fG{5P7GyP`y&GviCZ?4}WzxVvs`fc>v_2$M{43bNuJ}&-Y*G|F-`U|E2!R{8#vYNqGv-d_F6M6L5#}-G zS>}1>MdoGZ_sr|eADK6q515abPnmx+Uj$$RZ~=q>K|n!(FrYY~BtRTc8XyUf29yU> z1jqv_0~7((0ZRgw1}qC$5wI#?b-;T8YXa5=tPj`_urc6+fXx9P2K*lIN5K7nhXH>E zJPvpg@K?aofWHG?1OkCzAUY5e$Pdg76a*Fo3ImG*ivvZ0C4r@Z(!jF7^1zBfdEmmp zw*!|1E)853xIA!0;L5;Nf$s&b4O|zvK5#?e#=uj74}-jdvVyXMa)R=L@`DP4ih@Kz zB|+6ewLwim%|Wd}LxMVjx`KuUO$eG4G$m+S(Db00L9>HC4muX}TQEL2EI2E8Y_KVK zeDK8J$-z^Crv^_8elvJR@T}n3!E=J=2G0vV6MQcCLhz;FE5TQTzX|>}_`BfmgRckQ z2>vnnX7JA;VIi!L$dKrem=JbIY)D*4dO1RDl|HDaA-_uY-oIFLMSIRDKt4WEi@-o5~>Vs4jmafBXo7>r(s}Na9B>5 zCQKLB5Y`xG3~LE%4I2{H9@Y`o6*e?%c-Y9WQDLLQ#)ORxn-VrZY+=|tVef`54|^|c zZP@y-<6-y0J;J%+nc=)}et2%UAiN-47+w@!8LkL7hPQ;bg|~-yh7S!N9zHUBboh+$ zS>bcS=Z4P@Ul{&&_>%CY;a9`I4ZjxtL->vGAH#nNzZw2>_^;u&!+#6E6Mi@R4;Gsh z$4X!&vXWUTtW;JSE1i|W%3|?Y*{mE^E^7j75^D-;8f!Xh25Tm37Hc-^E!I5NeAWWi zBG%iiHLP{4&8#h~k6Bw;pR;zezGQvHI>9={I?uYuy3G2H^*!r)L|{a4L})~KL_|bn zL{vm{#NY^aL|jCCL_$PjL{h}ih~W_b*5$7V#M_i1!9C0P$YQ(n@-$nXGG9v>cgCj#D!y>~YS&~7j{eMh}VZi0+CW7Ck(AWb~-$(a~d~ z$3~l?$45_yo)|qjdP?+>=-&ss4o)0gGx&|cs|Oz$d@F_)6CcBgNs38{NsHmeWXAAf z_%XRLf|!CBVN7vMNsKtAJf=QI6QhqY#28~*V&=wdi@Cx^u`z5Mo4_WqDQqg6&UR+| zv6<{>b__d~9na>llh`TjJa#_2kX^(UvBhi&dk%Xpdp>(1`)&4O_7e6x?4|5w>=o>l z>{aad*lXB7vTw3~Vc%l^#=gV;oqd=62m3zzA^Q>gPxce`U$N*|Y%DG|H&zf^5G#x= zjupk0#EN4}W2Ld>v9j2T*vi<_U&#@>v*8+$MIeq3B!LR?~8a$IU$T3mV@ zH!dSCD=s^ZAD0uC7bl3D6gMSqTHN%wnQ^n?X2;Emdn;~U+=94;af{*>$1REbD(*zw zskrlT7vnC+eHZtA-1WF$;_k<1#H->>@hjq2#lIK7Hhz8lhWL%~o8mXee;EH!{KxTI z zO2X8H1qq81)+cO8_#k0(!j^=O6SgL7OW2WcIN@l*@r08JrxVU5oKLuzaGB%9VQ_po z{+s|#ASZ|u%n9Lyaaf!PP9!IqGnmuNY2^&zba1*jLpj4Z!#N{3qd0GH#&E`ROq_9? zJ)FIq{hWiGL!85$Bb=k0W1JJ5Q=HSBGn{jr^PKCPA33)K8b#b%tU3PDp8%NP1GkgBpMPM6Pps76I&D85{D#qBz7i#oVYb{TjGwy zor#|(?n>O9_(kHL#J!38689$_O#C_tpF~U|Cpjh2lITg!NiIpQN$yFWNrRHSk{C%o zNs6SJBxRB+Nu8ug(kAJW^ht)KrX*uhb5d(kThivFElD3IZB5#iv^{A@(q~CKlXfM2 zk@RKKo}|4=`;vZ1c1n&-mM2#wS0~pd*Cp2{Ym#-z`sB{!p~>TuCnir$o|^n-@{HtJ z$?qh;o4h=EW%BCeHOcFeZzlhed@K34 zB4t&|>Xhp#Kc?JF`6cC6%I%cjQtqVup7KY^{gekO4^#e3d7PS*nv$B9%1zBo%}V8^ zW~cH~b5jMW`Kbk|!qlSFH&bV%&Ptt=IyZG*>ipCNsS8uzPF<4vPU_OsWvRF>7;Z@IyIf1?wszI&P{s`U5L*QT#ee?NUg`o{DR(l@7XN&hJQ$o?$zi@AHf8+knz03WBdyjjc`;hx5_c8Yg_bK;T zMp_0pBQt}S!OzIa$j!*h5M&f&2s4T@iZe!pz`xy^19%lTR@i;RnGbJ-Elbe~D znU%@Q%+BOz=4J{q^D_%Fg_%W}Lo(-Q?#R5B`9tQ7%%3uU&ipm=cIKVT-!q?OKF=a& zIc3qZoU>fB+_OBhg0n)i!m}c>qOt~Ov9nsUhGcbQb!8398lE*GYh>1_tT(d8W)>_qhVq8-hVw@7M)BU@jp2>unRw%Pdw6?!`*{a> zhj@p1M|ekh$9N}rr+BA%XL#p$=d<0jJ+r;C8QH$se%b!n%dpzM(B(Cn~mR(3>o ze0E;8G+US5kv%DUY4+yqJK1-$?`1#8ew6)Z_T%g)*?(m}%YL5yf)DUfd^A6spTp1N z=kp8sLVgjym@nds`4YaAU&fd5EBFiei};KA@9^K{FXJ!gui&rbuja4eujQ}fzt7*m z|A2p+e}{jUe~V7mg}DDnd_D7oy*Ae&GpL- z%4O%qxsUSjdBi+& zo>Lwz&pFRE&pppGFFY?IFDh?v9y>2CFCi~6FF9{m-iW+Wd2i&6&4cnxdE@fN=S|F; zoHr$JYTlc9)ANqx9m_kBcPj5p-r2l!dFS&k3rvW*L?SU&wQ_Z?|eqSPrh%ye||uI zV17`3NPcL(Hea7_$ZyJT&Tq+Y&2P&elHZZvl|M9pSpJCok@-9FcjoWP{~~`+{#W^X z^Y`WN&p()dDF1N&k^E!%#|xYaXa&v%t_AJ|9tEBSg9^L~7zMrseg*yo0R@2tf`X=k zSp}aKY%kbZu&dyUf;|O$3-%WrC^%DauHc7)8wEcV{9N#B!R>-O1y2i}7lMVDLVO{q z(5bMfP*f-`loXZ~mKVwjD+=X>RfW}sHHEc>b%mFC+>{LMLI7P$U!!CBiac zxlkso5XyyB!fIiSuvSu)_=#|<@PY7= z@QLuL@NeOB;fo@m2rNPuVT*7@_#$EvsmQ5FUNov`Nzu-tT}59M?J3$@w7=*;(V?Ql zMMsK`6&)`+S#-MSOwrk*3q==;zAw5{bhqeX(Vs<6ik=sP#h7ANadGjG;zh-aiTzcZ=^AKPrAw{H*we z2qof)_@Z2qKvXCaii$+VB9TZel8B_DGLcMFAzC0>Bw8$bN3=||T(m;8QnX6+o@lLT zooKyigJ`4ZmgqOpUC}+!L(wDApQ6X2C!(jKzeUeQFG|1?R0*xbxnxj@cZpAlUr9(w zcu7P_R7pxnT1i$(c1cc2L5Z-WxMX_C%#zt9Z#idJ1*Ok6sx~+6a z>CVz!rC*fpDcxIovh+;p`O-_JS4+Pu{Xr5S36g|J!Xy!rNJ*3=S~6I|mc&WoB?*#5 zNs?r!WVmFcWVB?g1d^B}<0RuH6D5-+QzTO*Z%U?1j!2G4PDoBk&PvWn&Py&xE=n#- zu1da$4Vio zNjgqCK{`n~Svo~JP5P$vu=J?(g!Gj3tn{4py!3+fqV%%#s`MM_x6*6U@1-}&V#*|C z?PYJ3%`00_wy11z**j%R%a)g|C|g;!y6nBOwPowe-Y?rwwyA7$+4i#iWe3ZSmK`rU zS$3}MV%e2)pgg!dzr3}4NO@=Z(DD)GqsreXA6pKU&nTZ&zPx;8`RejD~n#50)P;KURLS{7m`z@=LNoGH;oW%uf~|3zP-Pf@LAHFd0i0A&Znn%LdDu zWv#LyvJTl$*)Z8~*$CN4*=X4q*;pAQ8z&nt`%1P?c2IUmc2ssuc3gHsc2ag)c2;&y zc3yT-c1iY=>}T2UvOi?^We;V~WG^bf3Umd%!llBq!mEN&!K?_Z2(D09Xe;y;h6-av zb45!oxDNbBsa>NMe4qS){A>9k`C<7{`7!yuN{`CaN>!!0Qdikf*;LtF z*;+ZIvb}O_rKxgedf`;{M5epvZ&<)@W9Dj!$=Rr##)c@id^MXMXf5XDyb^1l2*y8DyrmFl~q+$)m61s%Bs4m`YLtR%Bs~> zYpT{&ZK&E<^+DC9s?Akfsy?pzq-tx`wyNz_e^x!IdRq0j0#JYolme~5C~yjbf~X)V zC<-S#8SLPpzI_J+pdt z^;^~Rsuxr*tX^Hcrg~fTj_RG&yQ;sa-c!A|dVlr7>WkG^s=uxNzWPS>&FWvPf2+P* z6H^mg6JNurNv=t$Nv%n%Nw3MM$*SSiWY^@>3ujD8bl}XAJWvX(7a+LB72)>o(SHs{62RSKXI&d+QF=9jZH8cS1!~(N!)gHNC~ns@jvREJeZRL4}u>z(Rp_0ILK^&a(}^@HlY>b>iI>iz2d>zVa|^+EOZ^_qHJeM5ay zy|KQzzNNmken@>seP?}F{jmDs_1o%q)bFg{RsUuEp8Buq_tx*LKT!X5{h|89^+)TE zsVQoznyz+HyQ$sP9%@hZAhoyJNA0WjQ!~{8>N<73TBFvf8`K7Mqq<3LRJW+x)I-$m z>P~f+`V;l1>h0>!)Ss(&sduZtP=BfZO1)3LUwuIRwfc~Tpdo1}8mfk_an`tKTs3YQ z56vKrm&RM;qw&>LYic!hntF{!qt)m%dQF3-QDf9JYg#mInjxCcHQ#AoX#KST+8}L+ zHcZRXMrfn8gS9c*SZ$m(L7S*e(k5%uwCUPh_@+DffLTcfSlYPAj88QLw{bJ}Ox z=Q>b_*5Pyn9Z5&gIqAH0KDuaKj4oCeujA;FbSb(t9aksPmFmiL6}l>2jjm3o*6DQb z>DKDj>o({%={D;=)NRpyr29noscxHYyY4gHPTgO+XS(NlK#$gA^jJMkkJl6RWIaXi zq^IfW`VxJqzDzIESL&-p(fToZNIy^vwn;IuKu3>f&P*HiT*GBQ~fjj-})B~U<0ZF-GFVtHRLu38VVYO z4WfpU2601agQTIXLDo>wAaAH@P&6!Vc&FjrhUE>b8df*F*RZBxZNvJ84GkL`K4{q7 z@L|L64SzJ;Z+O`7xZz2|Uky(io;5r-00z*2GGGi?LyjTOkZ&k76dOc_5`)-KYLFVr z4KhQ8q0&%g7;9K%IB2+IxNEp?cxZTR_{;Fj@Z9jCkhBYQM zCN-uuavQT6`Hgvv6B{QtPHlX%ac1MJ#@UT?8sBQ1*SMf@VdJ94#f?iEFE?Io{I>C0 z3!C0*4S?BG8pj(a8YdYi8)qAr88;et8jl&jH$HAAHM=&uH+wV(HHS2ZHitE{ in^T*4&BA7Bv$nahxfQVdL!r9=(q?r3wf$}$^8WzP!YS?m literal 0 HcmV?d00001 diff --git a/Extensions/XEP-0054/CoreDataStorage/XMPPvCard.xcdatamodeld/XMPPvCard.xcdatamodel/layout b/Extensions/XEP-0054/CoreDataStorage/XMPPvCard.xcdatamodeld/XMPPvCard.xcdatamodel/layout new file mode 100644 index 0000000000000000000000000000000000000000..14125f8425e589d8b022ac5dd8a87514e5071365 GIT binary patch literal 10268 zcmbta2Y6If+CF7^&-6Y+2%!dMCUrm{9YZ$>BtRex$q+`;m`os44^~7F5D`HIOlTs~ zK?DoPVp$ax&{aXjjup$U>mum?-8%_MFv;rvljmXPo_p`PUwPm6`_8#ctE~*xMLeFf z2qS_h5+ETG(P!wh#c{KP;kr;wb#YwHw3~xvk-7~;oAGYE2k*rP@L~Kk zeg;2}U&Ckc+xT7lA-;&e#^2+g@Xz=PQ4$r=lRl&`NhC=mm86pll1YY&--h#(jkQLdG9XXH_{@ln}s?il^XNLk6;XqYZO?4z(Q&|}d zm%vyc5-6yt50?ce7H1C&2Wn@8%Ie@}cCa#75r_oYVJ3ngp&f-|N^C$uJM+1-td65q#!d*!y8Ei>GsVEKk(Lgi^-GBz8 zA!sN{2kW#`u01%nGEh-hs?ii@7Y0I=U}rTzoI`tYc4kd|bvb9)xa!cX`e0smsYVS} zh68iJ=Yq)G%3$5(;_MrPf%1_Mnw;u#IN~O0KH?|k&bCepb8#mN=hi}4;gQYJDTqc| ztNCgrV%?dPX_$=ZnVt1uZr$(jVF)3Jh_F;+oIl^|OY7%O@h|M>p6}`BPEPHgyl`PO zP&p`rh{nOqqYI)}W>(gR^8@9f`nr;qq4B|RgekkHyTM5KbU)_iztL$_go@Dw zG;v}Sf7OAiU~%+ku&%h}?nX2Tm0UZ5A7gltiQYT~l|oG0nB9b?q5vvsKM2Weo0RCS zWvCngpAilR<~E`-RKi7QI;uc3#zQ{G7DF0uMYo~b z(H-bcAi|PyFjJrwxRh5Otd4}Hhk{|qA%`s(hIlkXkk2w)l^L7|SBworW)u}?7lq0r zGa&V2!l8;#HH2d{BnHL{Dr;(ku|pww%n*B=`?4@wYzFtk#Ed{VGfWHtt(nt2`#An! z!Zkn^%}?<5b9;C&-iubCm7p{Q!HO!du#Q>BXyT%{fPXPJYkK;%0kpQ+Z=hczdJvVg9(ESc^$^g9A)t>yKsTaId_bGp`R!`)+rj(o zEQz}4^`!vUuJ>~LpLXpG+5wpzf%>AI93eqV3IpMaV5C_BSvMBTB`8V{tLW_DrC1n^_Os|Y08^`+HKJpXm9`@-F81MEWN}?N z^a6Sjy@Xyye+P$NMXyC2YPDqBISENgzLbQF zj4W?LN_K`X*YEQuC#Lv2Bf!6b0RI63Tm}MML05qQ@ffuufTx83{eb}SY(Q58z`{RG zfKCj;Vl2T@G!o0uZCHVoSOo@Yq6Yb5Igwx~Ee0jAL@+41R1*V&6$uAI6*D5ua#Y-M z+^HgA6KAUSn(Xj0#2vMTZ9ETKNKPwR<9TYvcM2ig4aef{I1cx~@wg`%f_vjWJcJMj zp*%K_4Pk>=I?DzK-T2=il;mqu(6I*~guU#Bb`Zizs3$;ZF!Nu75KhDXE+B+&00<3j zh7cagLnt{p$&-?nlbYbo%}q;4%JF#NrgH-SL~U{E0#G#(f622E^d(6APRa(IJs zJ4Z?BpSGq2B%Iv!CMXaC3$m`GyI;;fVc+oz83QSAmn{LehBj33vXzb_i-(GFMzy{V1=FMz3ETqy)$dJ zfRJqiYqoP9Qtq?riN`YTAq_(aU@N-Pvh>zjpYzixT8=t^`<60AZ62E|7#4q8OQE&VTewA-h zpn#T#pohz-uBe1%3$!gQBhY!&g!zR`Zm6=dbzK2%EjPfQj_P-SO=H7b0apre!Kd)+ z_znCfJ`H^xe`!H&pe#6UbV2^GOg2@6$7t-H{;7Vqr+-SK*OTT;^Kw5^{r)shVscuv zQN(BQThQk7w>RRmsHE-0@35(3G(A22p&a@XeJM#PX)r-rYO2SVn&L@J#lU_B=YQ~VkJ z9QEcFHLysY8RWfzySci?H|x>G8-K1f$gDheD1*`%hq?h^4=#UCnowlzQnZDX79hj-(E92CZ(c< z*wH2>!=I6u=Fd(^$V!E}l9Zj8oRA55Nbsj-Ly^hKN%AJ8aCg8GAOFIZG~xf?%lKCo zVhh-<@8YZYH~c%ALlBY@ObDSwK!m6_5fcfK5*ZN^SPw%R(59Eoiv+7+B{MQGEm&Eq z(YHI-oJT&EZEk5^|C`%g%gr+?R9z672Y`o1WCqG+R)o3jfR^0kmDN<|Rl)WMo5>*X z>}G8`mx1|g9vq@l&4A+Uv18jjrupgIQmf5q>*@_MN7O_^V!$mOB%#ek+|CH>m}CVj zE7@F_z4AOU5F@@yOvFqqaG#afh@Ciy6Yh0GkYdqD(w)SS9wZ({dJ!Rngtv7PFa>6b zt_s`y_LXX^?HS0JLz`v!lnOX_#8?q|a!0K5w_&1B8v1h@pek6hPCj*Fw zc!_WP=mIWw@Wmwswh5;5Ua(qL(>*;NLJuE}retJ&S*Sb!!LBY3@@p27Oj6*cTyUU~ zB%_kINE%G-$EV0ZG6=sx29qIV=w$E^B3xNh!99bE5+`i@vbgy-3$v-LP9tKG@db11 zAUYtf5s1WfF7o^%Suk%7$pu|1nh}EStf?BB=|$H+c{n_I1V8U=HfI7i8UNI_Q;cC# zGd)pmlAB0=sm9P@mbQ=rQb@*;@th`gdB|>SoXty51CA+w1;maDdiQU4Wc{Zc7RL+ZkLlFaJz!zHoJu_ z0%9#}A=ct3v64F`Rz?K2TBg-Uf^}`4(@L+2t>@b^t%fHQ>xxie5+IT53Dtt#TVk$9 zZc%eEI&yIlxdmKYOl}1iZ|7aSgWV1;-qzycT~Qa)I(D($)@z4ePHuNeTSu4ij^5do zqbtZFvZ^yj@95OgXx1Mh4^O$a3&wA{HIaw-Z8x$31b!sh2zbj6R9Ard)mKN@(&%lE zl15%BnzzsLfVB+;!AKKnBAa=zU7NGz6o^sF&*&6e$+k|EzKLunk8!hfSO;2n2H=A0 z`Xp~+YokP-fJTYzBD>kWY-PJfiR>l&pi$~W4&qm%UCs*Va^gDfa>!GEr5S0{ACRNu zY5W;^7WHAP{!sAac(sW3QRMl*R`B_%O-@49Ufo=^$xD1Yo|T%Dn~~#5PRPyiCvxq0 zRzg~8a&m$v1-3+eiC%9~nzy-XlUJZ>lUK=WX{q#_WGK4dYK5h+#h_H6*+h-(#&P1o5+wY+^B zyReTMsEC?6vu{JE_ML7aG+Wx`0)zGd-q3j3lWk^O+wq3>rhR}neQ81qZ?*t$x^>JO z>ir*iLlbEd{*0!;tDu)o7l`7ngzU}*))gdvd7tOw!fpS-w3?vOT=^}WPLQB^_`IB zZmqo8b$!+g`K&+DRo08>jdVh1S>O5Bctgv8H+@MttpE!{yoGy!H{Du!v+sHfqXl;wyFmaff)~1UBi%%|v*X)f>-+$o3A@$+Gd{|wqme_ip=a2Th_{G?$y@%;jK=^9~IXlF6kAd*qCVH42;or`*jzw=d zLZ5ErPVwF6h~V5{c}DYp9C(vHM~@YTYic44L-%h%vG7(T8Q#+lh0TXk_#FNie}(^r z|BZhm2$p^VB8NBeYFHgv;4S=Ml22xkdcw#(@Sc4ISw&WpHDm+4Yu^nuc|SP_@7kY& zy7>(Gj$EZ0YN2s71=yBPgYX9ZHoAl^rOW8ObS1ry-cKKZH|dYkCc1?_OJAff(^u&! z`XT)%y($n3qyo7>DNqaS0F5v;;ap5b%_k>>vFA0AU z{w@-W6e5*KCo+gkBD=^ba*JX`aiT<#Uz8;pAu1G=iUOh~qE(^|q9)NE(Nm%mqL)Oc zMZb#$Vv$%bR*KbPz1S!=i>+e2xVP9V9w@#+Tqdp+-zvUce5ZJc_@MZ8@z>(Z5-Jf& z#1g4QE>TL160^iAu}hqio{}VqUy>^+kc^WQNhV0{l&p}flB|}jk*t-hlQc>;OSVe3 zOLj>1OO8uklDsXsAdQh4rDmyBYL_~tZfUGEP8u&AAoWTUr6Z-|q(#z+(h_Nvv_?8h zS|?@FTc!6%@0UIz-6(xjx8m+X1jE3&s_@5?@yU6fsw%j62VO0JPRDlAn;Dl%JCSLw-U2 zvHVl{xAIF0qasUjqhh#XjN&FmfudM3QBk6pqL`|vQq(IJDVSoL;;iCb#W}?V#m9j*DBX3A5v~s zZdGnq?ojSjzOVdP`Kj`Axfu8LRnQu$SbRD)GR zRU=iSRYj_)s*q}?s!BCm)u7s_dR6tN>Wu0g)qAS*s*hBks6JDDq54wwlj>KsKrK=y zs3)pR)dBT%^$hjR>KgSdb)C9iJx6`JdYO8SdaZh=`YH8M^)u?{)UT>fsozkaR$o;A zp#Dkyi~6$qipHSnsqt$@YKk?JHKm$>rc6_*sn*QbEYaMnS*f{CvsZIO^StIo%~{Rc zF(^hF(>ul&lOHoBrYh#vn3XXP$Lx*SA9Fb7NX&_tlQE}b&c^(v6>AMzyS9(kug%nE zYjd?@v^QxRw2QU3X_so3Y46pp)E?BntbJ2^QF~Qq*7eh+=?3a<&<)Y0>vD9%ba}cF zx>34Ybjx(>bz5}XbdTvC*PYjWs{3AdRd3bz(0lcR^rQ9D^t1GJ`Ud?x{Vn>%`rGt( z=4cVf_*P3H?d^Y5h6<$NDeyR}3WJ)pl zO@mB>O+!r?rYuvAX_#q@DPo#!YB0?+Eif%M-DbMObeHLF)89;|O&^**GJRtD%xpB9 z%?`869BYm<$D4bZ`)nfIBGnopbGHGglu zY>Ba0Ep|&cOLt2T%MeSJCC4(%l4r@c6j}n7GRtyHqh+(@G0WqY1C~RU6PA;fOO{_O zmn~PULaW%?-qVQ=rnbe{bT)&{YO~v%Hn%O-Ho)e!CEAj0(`_?s zRkj*i*cP$Pwl&!1*%sIq*_dsK?Frj%+g{s#+d`J@ZuC?p!M!VT=wcG7ZyW8H&?zJb{^X&QdLi+^!B>QChZ2NrsLi;WD#rCE4W%hOU zhwLZq@7d4WKeB&f|Hl5E{a5={M^A^x;d3N820Mm2raNjJvm6bMd5#5+MUE!N7RNTn zV~)oidmQ^52ONhShaD#!FFIa!yyCdx_{~Y3LZ{5BaH^adr`D-=8l7gR+nM7W=FD@B zaE@~3I}4rToyE?H&I)J9In!C?+~C~g+~VBk+~M5m+~wTk+~+*tJmfs=Jm&n&`GxaK z=hx0}oj*GN>HOLGi}SKe?2@|VE~RUTE8Ugj8s-}A8tEGC8tck;6}rZ|id|D(A=gY- z#C4ZznQNnKr)!t%8P{{JG r+)B6BZF9%D2f8!dquoXB5_h$`&fO5*J|nQx%6;hZ=y%J%?s@+QC20l8 literal 0 HcmV?d00001 diff --git a/Extensions/XEP-0054/CoreDataStorage/XMPPvCardAvatarCoreDataStorageObject.h b/Extensions/XEP-0054/CoreDataStorage/XMPPvCardAvatarCoreDataStorageObject.h new file mode 100644 index 0000000..812f6c9 --- /dev/null +++ b/Extensions/XEP-0054/CoreDataStorage/XMPPvCardAvatarCoreDataStorageObject.h @@ -0,0 +1,20 @@ +// +// XMPPvCardAvatarCoreDataStorageObject.h +// XEP-0054 vCard-temp +// +// Originally created by Eric Chamberlain on 3/18/11. +// +// This class is so that we don't load the photoData each time we need to touch the XMPPvCardCoreDataStorageObject. + +#import +#import + +@class XMPPvCardCoreDataStorageObject; + + +@interface XMPPvCardAvatarCoreDataStorageObject : NSManagedObject + +@property (nonatomic, strong) NSData * photoData; +@property (nonatomic, strong) XMPPvCardCoreDataStorageObject * vCard; + +@end diff --git a/Extensions/XEP-0054/CoreDataStorage/XMPPvCardAvatarCoreDataStorageObject.m b/Extensions/XEP-0054/CoreDataStorage/XMPPvCardAvatarCoreDataStorageObject.m new file mode 100644 index 0000000..b2bf002 --- /dev/null +++ b/Extensions/XEP-0054/CoreDataStorage/XMPPvCardAvatarCoreDataStorageObject.m @@ -0,0 +1,17 @@ +// +// XMPPvCardAvatarCoreDataStorageObject.m +// XEP-0054 vCard-temp +// +// Originally created by Eric Chamberlain on 3/18/11. +// + +#import "XMPPvCardAvatarCoreDataStorageObject.h" +#import "XMPPvCardCoreDataStorageObject.h" + + +@implementation XMPPvCardAvatarCoreDataStorageObject + +@dynamic photoData; +@dynamic vCard; + +@end diff --git a/Extensions/XEP-0054/CoreDataStorage/XMPPvCardCoreDataStorage.h b/Extensions/XEP-0054/CoreDataStorage/XMPPvCardCoreDataStorage.h new file mode 100644 index 0000000..830251d --- /dev/null +++ b/Extensions/XEP-0054/CoreDataStorage/XMPPvCardCoreDataStorage.h @@ -0,0 +1,47 @@ +// +// XMPPvCardCoreDataStorage.h +// XEP-0054 vCard-temp +// +// Originally created by Eric Chamberlain on 3/18/11. +// + +#import +#import + +#import "XMPPCoreDataStorage.h" +#import "XMPPvCardTempModule.h" +#import "XMPPvCardAvatarModule.h" + +/** + * This class is an example implementation of XMPPCapabilitiesStorage using core data. + * You are free to substitute your own storage class. + **/ + +@interface XMPPvCardCoreDataStorage : XMPPCoreDataStorage < +XMPPvCardAvatarStorage, +XMPPvCardTempModuleStorage +> { + // Inherits protected variables from XMPPCoreDataStorage +} + +/** + * XEP-0054 provides a mechanism for transmitting vCards via XMPP. + * Because the JID doesn't change very often and can be large with image data, + * it is safe to persistently store the JID and wait for a user to explicity ask for an update, + * or use XEP-0153 to monitor for JID changes. + * + * For this reason, it is recommended you use this sharedInstance across all your xmppStreams. + * This way all streams can shared a knowledgebase concerning known JIDs and Avatar photos. + * + * All other aspects of vCard handling (such as lookup failures, etc) are kept separate between streams. +**/ ++ (instancetype)sharedInstance; + +// +// This class inherits from XMPPCoreDataStorage. +// +// Please see the XMPPCoreDataStorage header file for more information. +// + + +@end diff --git a/Extensions/XEP-0054/CoreDataStorage/XMPPvCardCoreDataStorage.m b/Extensions/XEP-0054/CoreDataStorage/XMPPvCardCoreDataStorage.m new file mode 100644 index 0000000..221073c --- /dev/null +++ b/Extensions/XEP-0054/CoreDataStorage/XMPPvCardCoreDataStorage.m @@ -0,0 +1,235 @@ +// +// XMPPvCardCoreDataStorage.m +// XEP-0054 vCard-temp +// +// Originally created by Eric Chamberlain on 3/18/11. +// + +#import "XMPPvCardCoreDataStorage.h" +#import "XMPPvCardCoreDataStorageObject.h" +#import "XMPPvCardTempCoreDataStorageObject.h" +#import "XMPPvCardAvatarCoreDataStorageObject.h" +#import "XMPP.h" +#import "XMPPCoreDataStorageProtected.h" +#import "XMPPLogging.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +enum { + kXMPPvCardTempNetworkFetchTimeout = 10, +}; + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPvCardCoreDataStorage + +static XMPPvCardCoreDataStorage *sharedInstance; + ++ (instancetype)sharedInstance +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + sharedInstance = [[XMPPvCardCoreDataStorage alloc] initWithDatabaseFilename:nil storeOptions:nil]; + }); + + return sharedInstance; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Setup +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)configureWithParent:(XMPPvCardTempModule *)aParent queue:(dispatch_queue_t)queue +{ + return [super configureWithParent:aParent queue:queue]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Overrides +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)commonInit +{ + autoAllowExternalBinaryDataStorage = YES; + autoRecreateDatabaseFile = YES; + [super commonInit]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPvCardAvatarStorage protocol +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSData *)photoDataForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream +{ + // This is a public method. + // It may be invoked on any thread/queue. + + XMPPLogTrace(); + + __block NSData *result; + + [self executeBlock:^{ + + XMPPvCardCoreDataStorageObject *vCard; + vCard = [XMPPvCardCoreDataStorageObject fetchOrInsertvCardForJID:jid + inManagedObjectContext:[self managedObjectContext]]; + + result = vCard.photoData; + }]; + + return result; +} + +- (NSString *)photoHashForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream +{ + // This is a public method. + // It may be invoked on any thread/queue. + + XMPPLogTrace(); + + __block NSString *result; + + [self executeBlock:^{ + + XMPPvCardCoreDataStorageObject *vCard; + vCard = [XMPPvCardCoreDataStorageObject fetchOrInsertvCardForJID:jid + inManagedObjectContext:[self managedObjectContext]]; + + result = vCard.photoHash; + }]; + + return result; +} + +- (void)clearvCardTempForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream +{ + // This is a public method. + // It may be invoked on any thread/queue. + + XMPPLogTrace(); + + [self scheduleBlock:^{ + + XMPPvCardCoreDataStorageObject *vCard; + vCard = [XMPPvCardCoreDataStorageObject fetchOrInsertvCardForJID:jid + inManagedObjectContext:[self managedObjectContext]]; + + vCard.vCardTemp = nil; + vCard.lastUpdated = [NSDate date]; + }]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPvCardTempModuleStorage protocol +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (XMPPvCardTemp *)vCardTempForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream +{ + // This is a public method. + // It may be invoked on any thread/queue. + + XMPPLogTrace(); + + __block XMPPvCardTemp *result; + + [self executeBlock:^{ + + XMPPvCardCoreDataStorageObject *vCard; + vCard = [XMPPvCardCoreDataStorageObject fetchOrInsertvCardForJID:jid + inManagedObjectContext:[self managedObjectContext]]; + + result = vCard.vCardTemp; + }]; + + return result; +} + +- (void)setvCardTemp:(XMPPvCardTemp *)vCardTemp forJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream +{ + // This is a public method. + // It may be invoked on any thread/queue. + + XMPPLogTrace(); + + [self scheduleBlock:^{ + + XMPPvCardCoreDataStorageObject *vCard; + vCard = [XMPPvCardCoreDataStorageObject fetchOrInsertvCardForJID:jid + inManagedObjectContext:[self managedObjectContext]]; + + vCard.waitingForFetch = @NO; + vCard.vCardTemp = vCardTemp; + + // Update photo and photo hash + vCard.photoData = vCardTemp.photo; + + vCard.lastUpdated = [NSDate date]; + }]; +} + +- (XMPPvCardTemp *)myvCardTempForXMPPStream:(XMPPStream *)stream +{ + if(!stream) return nil; + + return [self vCardTempForJID:[[stream myJID] bareJID] xmppStream:stream]; +} + +- (BOOL)shouldFetchvCardTempForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream +{ + // This is a public method. + // It may be invoked on any thread/queue. + + XMPPLogTrace(); + + __block BOOL result; + + [self executeBlock:^{ + + XMPPvCardCoreDataStorageObject *vCard; + vCard = [XMPPvCardCoreDataStorageObject fetchOrInsertvCardForJID:jid + inManagedObjectContext:[self managedObjectContext]]; + + BOOL waitingForFetch = [vCard.waitingForFetch boolValue]; + + if(![stream isAuthenticated]) + { + result = NO; + } + else if (!waitingForFetch) + { + vCard.waitingForFetch = @YES; + vCard.lastUpdated = [NSDate date]; + + result = YES; + } + else if ([vCard.lastUpdated timeIntervalSinceNow] < -kXMPPvCardTempNetworkFetchTimeout) + { + // Our last request exceeded the timeout, send a new one + vCard.lastUpdated = [NSDate date]; + + result = YES; + } + else + { + // We already have an outstanding request, no need to send another one. + result = NO; + } + }]; + + return result; +} + +@end diff --git a/Extensions/XEP-0054/CoreDataStorage/XMPPvCardCoreDataStorageObject.h b/Extensions/XEP-0054/CoreDataStorage/XMPPvCardCoreDataStorageObject.h new file mode 100644 index 0000000..349925b --- /dev/null +++ b/Extensions/XEP-0054/CoreDataStorage/XMPPvCardCoreDataStorageObject.h @@ -0,0 +1,73 @@ +// +// XMPPvCardCoreDataStorageObject.h +// XEP-0054 vCard-temp +// +// Originally created by Eric Chamberlain on 3/18/11. +// + +#import +#import + + +@class XMPPJID; +@class XMPPvCardTemp; +@class XMPPvCardTempCoreDataStorageObject; +@class XMPPvCardAvatarCoreDataStorageObject; + + +@interface XMPPvCardCoreDataStorageObject : NSManagedObject + + +/* + * User's JID, indexed for lookups + */ +@property (nonatomic, strong) NSString * jidStr; + +/* + * User's photoHash used by XEP-0153 + */ +@property (nonatomic, strong, readonly) NSString * photoHash; + +/* + * The last time the record was modified, also used to determine if we need to fetch again + */ +@property (nonatomic, strong) NSDate * lastUpdated; + + +/* + * Flag indicating whether a get request is already pending, used in conjunction with lastUpdated + */ +@property (nonatomic, strong) NSNumber * waitingForFetch; + + +/* + * Relationship to the vCardTemp record. + * We use a relationship, so the vCardTemp stays faulted until we really need it. + */ +@property (nonatomic, strong) XMPPvCardTempCoreDataStorageObject * vCardTempRel; + + +/* + * Relationship to the vCardAvatar record. + * We use a relationship, so the vCardAvatar stays faulted until we really need it. + */ +@property (nonatomic, strong) XMPPvCardAvatarCoreDataStorageObject * vCardAvatarRel; + + +/* + * Accessor to retrieve photoData, so we can hide the underlying relationship implementation. + */ +@property (nonatomic, strong) NSData *photoData; + + +/* + * Accessor to retrieve vCardTemp, so we can hide the underlying relationship implementation. + */ +@property (nonatomic, strong) XMPPvCardTemp *vCardTemp; + + ++ (XMPPvCardCoreDataStorageObject *)fetchOrInsertvCardForJID:(XMPPJID *)jid + inManagedObjectContext:(NSManagedObjectContext *)moc; + + +@end diff --git a/Extensions/XEP-0054/CoreDataStorage/XMPPvCardCoreDataStorageObject.m b/Extensions/XEP-0054/CoreDataStorage/XMPPvCardCoreDataStorageObject.m new file mode 100755 index 0000000..cd9cea5 --- /dev/null +++ b/Extensions/XEP-0054/CoreDataStorage/XMPPvCardCoreDataStorageObject.m @@ -0,0 +1,180 @@ +// +// XMPPvCardCoreDataStorageObject.m +// XEP-0054 vCard-temp +// +// Originally created by Eric Chamberlain on 3/18/11. +// + +#import "XMPPvCardCoreDataStorageObject.h" +#import "XMPPvCardTempCoreDataStorageObject.h" +#import "XMPPvCardAvatarCoreDataStorageObject.h" + +#import "XMPPJID.h" +#import "XMPPStream.h" +#import "NSNumber+XMPP.h" +#import "NSData+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation XMPPvCardCoreDataStorageObject + ++ (XMPPvCardCoreDataStorageObject *)fetchvCardForJID:(XMPPJID *)jid + inManagedObjectContext:(NSManagedObjectContext *)moc +{ + NSString *entityName = NSStringFromClass([XMPPvCardCoreDataStorageObject class]); + + NSEntityDescription *entity = [NSEntityDescription entityForName:entityName + inManagedObjectContext:moc]; + + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"jidStr == %@", [jid bare]]; + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:entity]; + [fetchRequest setPredicate:predicate]; + [fetchRequest setIncludesPendingChanges:YES]; + [fetchRequest setFetchLimit:1]; + + NSArray *results = [moc executeFetchRequest:fetchRequest error:nil]; + + + return (XMPPvCardCoreDataStorageObject *)[results lastObject]; +} + + ++ (XMPPvCardCoreDataStorageObject *)insertEmptyvCardForJID:(XMPPJID *)jid + inManagedObjectContext:(NSManagedObjectContext *)moc +{ + NSString *entityName = NSStringFromClass([XMPPvCardCoreDataStorageObject class]); + + XMPPvCardCoreDataStorageObject *vCard = [NSEntityDescription insertNewObjectForEntityForName:entityName + inManagedObjectContext:moc]; + + vCard.jidStr = [jid bare]; + return vCard; +} + ++ (XMPPvCardCoreDataStorageObject *)fetchOrInsertvCardForJID:(XMPPJID *)jid + inManagedObjectContext:(NSManagedObjectContext *)moc +{ + XMPPvCardCoreDataStorageObject *vCard = [self fetchvCardForJID:jid inManagedObjectContext:moc]; + if (vCard == nil) + { + vCard = [self insertEmptyvCardForJID:jid inManagedObjectContext:moc]; + } + + return vCard; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark NSManagedObject methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)awakeFromInsert +{ + [super awakeFromInsert]; + [self setPrimitiveValue:[NSDate date] forKey:@"lastUpdated"]; +} + + +- (void)willSave +{ + /* + if (![self isDeleted] && [self isUpdated]) { + [self setPrimitiveValue:[NSDate date] forKey:@"lastUpdated"]; + } + */ + + [super willSave]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Getter/setter methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@dynamic jidStr; +@dynamic photoHash; +@dynamic lastUpdated; +@dynamic waitingForFetch; +@dynamic vCardTempRel; +@dynamic vCardAvatarRel; + + +- (NSData *)photoData { + return self.vCardAvatarRel.photoData; +} + + +- (void)setPhotoData:(NSData *)photoData +{ + if ([photoData length] == 0) + { + if (self.vCardAvatarRel != nil) + { + [[self managedObjectContext] deleteObject:self.vCardAvatarRel]; + [self setPrimitiveValue:nil forKey:@"photoHash"]; + } + + return; + } + + if (self.vCardAvatarRel == nil) + { + NSString *entityName = NSStringFromClass([XMPPvCardAvatarCoreDataStorageObject class]); + + self.vCardAvatarRel = [NSEntityDescription insertNewObjectForEntityForName:entityName + inManagedObjectContext:[self managedObjectContext]]; + } + + [self willChangeValueForKey:@"photoData"]; + self.vCardAvatarRel.photoData = photoData; + [self didChangeValueForKey:@"photoData"]; + + [self setPrimitiveValue:[[photoData xmpp_sha1Digest] xmpp_hexStringValue] forKey:@"photoHash"]; +} + + +- (XMPPvCardTemp *)vCardTemp { + return self.vCardTempRel.vCardTemp; +} + + +- (void)setVCardTemp:(XMPPvCardTemp *)vCardTemp +{ + if (vCardTemp == nil && self.vCardTempRel != nil) + { + [[self managedObjectContext] deleteObject:self.vCardTempRel]; + + return; + } + + if (self.vCardTempRel == nil) + { + NSString *entityName = NSStringFromClass([XMPPvCardTempCoreDataStorageObject class]); + + self.vCardTempRel = [NSEntityDescription insertNewObjectForEntityForName:entityName + inManagedObjectContext:[self managedObjectContext]]; + } + + [self willChangeValueForKey:@"vCardTemp"]; + self.vCardTempRel.vCardTemp = vCardTemp; + [self didChangeValueForKey:@"vCardTemp"]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark KVO methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (NSSet *)keyPathsForValuesAffectingPhotoHash +{ + return [NSSet setWithObjects:@"vCardAvatarRel", @"photoData", nil]; +} + ++ (NSSet *)keyPathsForValuesAffectingVCardTemp +{ + return [NSSet setWithObject:@"vCardTempRel"]; +} + +@end diff --git a/Extensions/XEP-0054/CoreDataStorage/XMPPvCardTempCoreDataStorageObject.h b/Extensions/XEP-0054/CoreDataStorage/XMPPvCardTempCoreDataStorageObject.h new file mode 100644 index 0000000..2638c32 --- /dev/null +++ b/Extensions/XEP-0054/CoreDataStorage/XMPPvCardTempCoreDataStorageObject.h @@ -0,0 +1,23 @@ +// +// XMPPvCardTempCoreDataStorageObject.h +// XEP-0054 vCard-temp +// +// Oringally created by Eric Chamberlain on 3/18/11. +// +// This class is so that we don't load the vCardTemp each time we need to touch the XMPPvCardCoreDataStorageObject. +// The vCardTemp abstraction also makes it easier to eventually add support for vCard4 over XMPP (XEP-0292). + +#import +#import + +#import "XMPPvcardTemp.h" + +@class XMPPvCardCoreDataStorageObject; + + +@interface XMPPvCardTempCoreDataStorageObject : NSManagedObject + +@property (nonatomic, strong) XMPPvCardTemp * vCardTemp; +@property (nonatomic, strong) XMPPvCardCoreDataStorageObject * vCard; + +@end diff --git a/Extensions/XEP-0054/CoreDataStorage/XMPPvCardTempCoreDataStorageObject.m b/Extensions/XEP-0054/CoreDataStorage/XMPPvCardTempCoreDataStorageObject.m new file mode 100644 index 0000000..7d9fe85 --- /dev/null +++ b/Extensions/XEP-0054/CoreDataStorage/XMPPvCardTempCoreDataStorageObject.m @@ -0,0 +1,17 @@ +// +// XMPPvCardTempCoreDataStorageObject.m +// XEP-0054 vCard-temp +// +// Originally created by Eric Chamberlain on 3/18/11. +// + +#import "XMPPvCardTempCoreDataStorageObject.h" +#import "XMPPvCardCoreDataStorageObject.h" + + +@implementation XMPPvCardTempCoreDataStorageObject + +@dynamic vCardTemp; +@dynamic vCard; + +@end diff --git a/Extensions/XEP-0054/XMPPvCardTemp.h b/Extensions/XEP-0054/XMPPvCardTemp.h new file mode 100644 index 0000000..8e419d0 --- /dev/null +++ b/Extensions/XEP-0054/XMPPvCardTemp.h @@ -0,0 +1,120 @@ +// +// XMPPvCardTemp.h +// XEP-0054 vCard-temp +// +// Created by Eric Chamberlain on 3/9/11. +// Copyright 2011 RF.com. All rights reserved. +// Copyright 2010 Martin Morrison. All rights reserved. +// + + +#import +#import + +#import "XMPPIQ.h" +#import "XMPPJID.h" +#import "XMPPUser.h" +#import "XMPPvCardTempAdr.h" +#import "XMPPvCardTempBase.h" +#import "XMPPvCardTempEmail.h" +#import "XMPPvCardTempLabel.h" +#import "XMPPvCardTempTel.h" + + +typedef enum _XMPPvCardTempClass { + XMPPvCardTempClassNone, + XMPPvCardTempClassPublic, + XMPPvCardTempClassPrivate, + XMPPvCardTempClassConfidential, +} XMPPvCardTempClass; + + +extern NSString *const kXMPPNSvCardTemp; +extern NSString *const kXMPPvCardTempElement; + + +/* + * Note: according to the DTD, every fields bar N and FN can appear multiple times. + * The provided accessors only support this for the field types where multiple + * entries make sense - for the others, if required, the NSXMLElement accessors + * must be used. + */ +@interface XMPPvCardTemp : XMPPvCardTempBase + + +@property (nonatomic, strong) NSDate *bday; +@property (nonatomic, strong) NSData *photo; +@property (nonatomic, strong) NSString *nickname; +@property (nonatomic, strong) NSString *formattedName; +@property (nonatomic, strong) NSString *familyName; +@property (nonatomic, strong) NSString *givenName; +@property (nonatomic, strong) NSString *middleName; +@property (nonatomic, strong) NSString *prefix; +@property (nonatomic, strong) NSString *suffix; + +@property (nonatomic, strong) NSArray *addresses; +@property (nonatomic, strong) NSArray *labels; +@property (nonatomic, strong) NSArray *telecomsAddresses; +@property (nonatomic, strong) NSArray *emailAddresses; + +@property (nonatomic, strong) XMPPJID *jid; +@property (nonatomic, strong) NSString *mailer; + +@property (nonatomic, strong) NSTimeZone *timeZone; +@property (nonatomic, strong) CLLocation *location; + +@property (nonatomic, strong) NSString *title; +@property (nonatomic, strong) NSString *role; +@property (nonatomic, strong) NSData *logo; +@property (nonatomic, strong) XMPPvCardTemp *agent; +@property (nonatomic, strong) NSString *orgName; + +/* + * ORGUNITs can only be set if there is already an ORGNAME. Otherwise, changes are ignored. + */ +@property (nonatomic, strong) NSArray *orgUnits; + +@property (nonatomic, strong) NSArray *categories; +@property (nonatomic, strong) NSString *note; +@property (nonatomic, strong) NSString *prodid; +@property (nonatomic, strong) NSDate *revision; +@property (nonatomic, strong) NSString *sortString; +@property (nonatomic, strong) NSString *phoneticSound; +@property (nonatomic, strong) NSData *sound; +@property (nonatomic, strong) NSString *uid; +@property (nonatomic, strong) NSString *url; +@property (nonatomic, strong) NSString *version; +@property (nonatomic, strong) NSString *desc; + +@property (nonatomic, assign) XMPPvCardTempClass privacyClass; +@property (nonatomic, strong) NSData *key; +@property (nonatomic, strong) NSString *keyType; + ++ (XMPPvCardTemp *)vCardTempFromElement:(NSXMLElement *)element; ++ (XMPPvCardTemp *)vCardTemp; ++ (XMPPvCardTemp *)vCardTempSubElementFromIQ:(XMPPIQ *)iq; ++ (XMPPvCardTemp *)vCardTempCopyFromIQ:(XMPPIQ *)iq; ++ (XMPPIQ *)iqvCardRequestForJID:(XMPPJID *)jid; + + +- (void)addAddress:(XMPPvCardTempAdr *)adr; +- (void)removeAddress:(XMPPvCardTempAdr *)adr; +- (void)clearAddresses; + + +- (void)addLabel:(XMPPvCardTempLabel *)label; +- (void)removeLabel:(XMPPvCardTempLabel *)label; +- (void)clearLabels; + + +- (void)addTelecomsAddress:(XMPPvCardTempTel *)tel; +- (void)removeTelecomsAddress:(XMPPvCardTempTel *)tel; +- (void)clearTelecomsAddresses; + + +- (void)addEmailAddress:(XMPPvCardTempEmail *)email; +- (void)removeEmailAddress:(XMPPvCardTempEmail *)email; +- (void)clearEmailAddresses; + + +@end diff --git a/Extensions/XEP-0054/XMPPvCardTemp.m b/Extensions/XEP-0054/XMPPvCardTemp.m new file mode 100644 index 0000000..db593f5 --- /dev/null +++ b/Extensions/XEP-0054/XMPPvCardTemp.m @@ -0,0 +1,932 @@ +// +// XMPPvCardTemp.m +// XEP-0054 vCard-temp +// +// Created by Eric Chamberlain on 3/9/11. +// Copyright 2011 RF.com. All rights reserved. +// Copyright 2010 Martin Morrison. All rights reserved. +// + + +#import "XMPPvCardTemp.h" +#import "XMPPLogging.h" +#import "XMPPDateTimeProfiles.h" +#import "NSData+XMPP.h" + +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_ERROR; +#endif + +NSString *const kXMPPNSvCardTemp = @"vcard-temp"; +NSString *const kXMPPvCardTempElement = @"vCard"; + + +@implementation XMPPvCardTemp + +#if DEBUG + ++ (void)initialize { + // We use the object_setClass method below to dynamically change the class from a standard NSXMLElement. + // The size of the two classes is expected to be the same. + // + // If a developer adds instance methods to this class, bad things happen at runtime that are very hard to debug. + // This check is here to aid future developers who may make this mistake. + // + // For Fearless And Experienced Objective-C Developers: + // It may be possible to support adding instance variables to this class if you seriously need it. + // To do so, try realloc'ing self after altering the class, and then initialize your variables. + + size_t superSize = class_getInstanceSize([NSXMLElement class]); + size_t ourSize = class_getInstanceSize([XMPPvCardTemp class]); + + if (superSize != ourSize) + { + XMPPLogError(@"Adding instance variables to XMPPvCardTemp is not currently supported!"); + + [DDLog flushLog]; + exit(15); + } +} + +#endif + ++ (XMPPvCardTemp *)vCardTempFromElement:(NSXMLElement *)elem { + object_setClass(elem, [XMPPvCardTemp class]); + + return (XMPPvCardTemp *)elem; +} + ++ (XMPPvCardTemp *)vCardTemp{ + NSXMLElement *vCardTempElement = [NSXMLElement elementWithName:kXMPPvCardTempElement xmlns:kXMPPNSvCardTemp]; + return [XMPPvCardTemp vCardTempFromElement:vCardTempElement]; +} + ++ (XMPPvCardTemp *)vCardTempSubElementFromIQ:(XMPPIQ *)iq +{ + if ([iq isResultIQ]) + { + NSXMLElement *query = [iq elementForName:kXMPPvCardTempElement xmlns:kXMPPNSvCardTemp]; + if (query) + { + return [self vCardTempFromElement:query]; + } + } + + return nil; +} + ++ (XMPPvCardTemp *)vCardTempCopyFromIQ:(XMPPIQ *)iq +{ + return [[self vCardTempSubElementFromIQ:iq] copy]; +} + + ++ (XMPPIQ *)iqvCardRequestForJID:(XMPPJID *)jid { + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:[jid bareJID] elementID:[XMPPStream generateUUID]]; + NSXMLElement *vCardElem = [NSXMLElement elementWithName:kXMPPvCardTempElement xmlns:kXMPPNSvCardTemp]; + + [iq addChild:vCardElem]; + return iq; +} + + +#pragma mark - +#pragma mark Identification Types + + +- (NSDate *)bday { + NSDate *bday = nil; + NSXMLElement *elem = [self elementForName:@"BDAY"]; + + if (elem != nil) { + bday = [NSDate dateWithXmppDateString:[elem stringValue]]; + } + + return bday; +} + + +- (void)setBday:(NSDate *)bday { + NSXMLElement *elem = [self elementForName:@"BDAY"]; + + if (elem == nil) { + elem = [NSXMLElement elementWithName:@"BDAY"]; + [self addChild:elem]; + } + + [elem setStringValue:[bday xmppDateString]]; +} + + +- (NSData *)photo { + NSData *decodedData = nil; + NSXMLElement *photo = [self elementForName:@"PHOTO"]; + + if (photo != nil) { + // There is a PHOTO element. It should have a TYPE and a BINVAL + //NSXMLElement *fileType = [photo elementForName:@"TYPE"]; + NSXMLElement *binval = [photo elementForName:@"BINVAL"]; + + if (binval) { + NSData *base64Data = [[binval stringValue] dataUsingEncoding:NSASCIIStringEncoding]; + decodedData = [base64Data xmpp_base64Decoded]; + } + } + + return decodedData; +} + + +- (void)setPhoto:(NSData *)data { + + NSXMLElement *photo = [self elementForName:@"PHOTO"]; + + if(photo) + { + [self removeChildAtIndex:[[self children] indexOfObject:photo]]; + } + + if([data length]) + { + NSXMLElement *photo = [NSXMLElement elementWithName:@"PHOTO"]; + [self addChild:photo]; + + NSString *imageType = [data xmpp_imageType]; + + if([imageType length]) + { + NSXMLElement *type = [NSXMLElement elementWithName:@"TYPE"]; + [photo addChild:type]; + [type setStringValue:imageType]; + } + + NSXMLElement *binval = [NSXMLElement elementWithName:@"BINVAL"]; + [photo addChild:binval]; + [binval setStringValue:[data xmpp_base64Encoded]]; + } +} + + +- (NSString *)nickname { + return [[self elementForName:@"NICKNAME"] stringValue]; +} + + +- (void)setNickname:(NSString *)nick { + XMPP_VCARD_SET_STRING_CHILD(nick, @"NICKNAME"); +} + + +- (NSString *)formattedName { + return [[self elementForName:@"FN"] stringValue]; +} + + +- (void)setFormattedName:(NSString *)fn { + XMPP_VCARD_SET_STRING_CHILD(fn, @"FN"); +} + + +- (NSString *)familyName { + NSString *result = nil; + NSXMLElement *name = [self elementForName:@"N"]; + + if (name != nil) { + NSXMLElement *part = [name elementForName:@"FAMILY"]; + + if (part != nil) { + result = [part stringValue]; + } + } + + return result; +} + + +- (void)setFamilyName:(NSString *)family { + XMPP_VCARD_SET_N_CHILD(family, @"FAMILY"); +} + + +- (NSString *)givenName { + NSString *result = nil; + NSXMLElement *name = [self elementForName:@"N"]; + + if (name != nil) { + NSXMLElement *part = [name elementForName:@"GIVEN"]; + + if (part != nil) { + result = [part stringValue]; + } + } + + return result; +} + + +- (void)setGivenName:(NSString *)given { + XMPP_VCARD_SET_N_CHILD(given, @"GIVEN"); +} + + +- (NSString *)middleName { + NSString *result = nil; + NSXMLElement *name = [self elementForName:@"N"]; + + if (name != nil) { + NSXMLElement *part = [name elementForName:@"MIDDLE"]; + + if (part != nil) { + result = [part stringValue]; + } + } + + return result; +} + + +- (void)setMiddleName:(NSString *)middle { + XMPP_VCARD_SET_N_CHILD(middle, @"MIDDLE"); +} + + +- (NSString *)prefix { + NSString *result = nil; + NSXMLElement *name = [self elementForName:@"N"]; + + if (name != nil) { + NSXMLElement *part = [name elementForName:@"PREFIX"]; + + if (part != nil) { + result = [part stringValue]; + } + } + + return result; +} + + +- (void)setPrefix:(NSString *)prefix { + XMPP_VCARD_SET_N_CHILD(prefix, @"PREFIX"); +} + + +- (NSString *)suffix { + NSString *result = nil; + NSXMLElement *name = [self elementForName:@"N"]; + + if (name != nil) { + NSXMLElement *part = [name elementForName:@"SUFFIX"]; + + if (part != nil) { + result = [part stringValue]; + } + } + + return result; +} + + +- (void)setSuffix:(NSString *)suffix { + XMPP_VCARD_SET_N_CHILD(suffix, @"SUFFIX"); +} + + +#pragma mark Delivery Addressing Types + + +- (NSArray *)addresses { return nil; } +- (void)addAddress:(XMPPvCardTempAdr *)adr { } +- (void)removeAddress:(XMPPvCardTempAdr *)adr { } +- (void)setAddresses:(NSArray *)adrs { } +- (void)clearAddresses { } + + +- (NSArray *)labels { return nil; } +- (void)addLabel:(XMPPvCardTempLabel *)label { } +- (void)removeLabel:(XMPPvCardTempLabel *)label { } +- (void)setLabels:(NSArray *)labels { } +- (void)clearLabels { } + + +- (NSArray *)telecomsAddresses { return nil; } +- (void)addTelecomsAddress:(XMPPvCardTempTel *)tel { } +- (void)removeTelecomsAddress:(XMPPvCardTempTel *)tel { } +- (void)setTelecomsAddresses:(NSArray *)tels { } +- (void)clearTelecomsAddresses { } + + +- (NSArray *)emailAddresses { return nil; } +- (void)addEmailAddress:(XMPPvCardTempEmail *)email { } +- (void)removeEmailAddress:(XMPPvCardTempEmail *)email { } +- (void)setEmailAddresses:(NSArray *)emails { } +- (void)clearEmailAddresses { } + + +- (XMPPJID *)jid { + XMPPJID *jid = nil; + NSXMLElement *elem = [self elementForName:@"JABBERID"]; + + if (elem != nil) { + jid = [XMPPJID jidWithString:[elem stringValue]]; + } + + return jid; +} + + +- (void)setJid:(XMPPJID *)jid { + NSXMLElement *elem = [self elementForName:@"JABBERID"]; + + if (elem == nil && jid != nil) { + elem = [NSXMLElement elementWithName:@"JABBERID"]; + [self addChild:elem]; + } + + if (jid != nil) { + [elem setStringValue:[jid full]]; + } else if (elem != nil) { + [self removeChildAtIndex:[[self children] indexOfObject:elem]]; + } +} + + +- (NSString *)mailer { + return [[self elementForName:@"MAILER"] stringValue]; +} + + +- (void)setMailer:(NSString *)mailer { + XMPP_VCARD_SET_STRING_CHILD(mailer, @"MAILER"); +} + + +#pragma mark Geographical Types + + +- (NSTimeZone *)timeZone { + // Turns out this is hard. Being lazy for now (not like anyone actually uses this, right?) + NSXMLElement *tz = [self elementForName:@"TZ"]; + if (tz != nil) { + // This is unlikely to work. :-( + return [NSTimeZone timeZoneWithName:[tz stringValue]]; + } else { + return nil; + } +} + + +- (void)setTimeZone:(NSTimeZone *)tz { + NSXMLElement *elem = [self elementForName:@"TZ"]; + + if (elem == nil && tz != nil) { + elem = [NSXMLElement elementWithName:@"TZ"]; + [self addChild:elem]; + } + + if (tz != nil) { + NSInteger offset = [tz secondsFromGMT]; + [elem setStringValue:[NSString stringWithFormat:@"%02ld:%02ld", + (long)(offset / 3600), (long)((offset % 3600) / 60)]]; + } else if (elem != nil) { + [self removeChildAtIndex:[[self children] indexOfObject:elem]]; + } +} + + +- (CLLocation *)location { + CLLocation *loc = nil; + NSXMLElement *geo = [self elementForName:@"GEO"]; + + if (geo != nil) { + NSXMLElement *lat = [geo elementForName:@"LAT"]; + NSXMLElement *lon = [geo elementForName:@"LON"]; + + loc = [[CLLocation alloc] initWithLatitude:[[lat stringValue] doubleValue] longitude:[[lon stringValue] doubleValue]]; + } + + return loc; +} + + +- (void)setLocation:(CLLocation *)geo { + NSXMLElement *elem = [self elementForName:@"GEO"]; + NSXMLElement *lat; + NSXMLElement *lon; + + if (geo != nil) { + CLLocationCoordinate2D coord = [geo coordinate]; + if (elem == nil) { + elem = [NSXMLElement elementWithName:@"GEO"]; + [self addChild:elem]; + + lat = [NSXMLElement elementWithName:@"LAT"]; + [elem addChild:lat]; + lon = [NSXMLElement elementWithName:@"LON"]; + [elem addChild:lon]; + } else { + lat = [elem elementForName:@"LAT"]; + lon = [elem elementForName:@"LON"]; + } + + [lat setStringValue:[NSString stringWithFormat:@"%.6f", coord.latitude]]; + [lon setStringValue:[NSString stringWithFormat:@"%.6f", coord.longitude]]; + } else if (elem != nil) { + [self removeChildAtIndex:[[self children] indexOfObject:elem]]; + } +} + + +#pragma mark Organizational Types + + +- (NSString *)title { + return [[self elementForName:@"TITLE"] stringValue]; +} + + +- (void)setTitle:(NSString *)title { + XMPP_VCARD_SET_STRING_CHILD(title, @"TITLE"); +} + + +- (NSString *)role { + return [[self elementForName:@"ROLE"] stringValue]; +} + + +- (void)setRole:(NSString *)role { + XMPP_VCARD_SET_STRING_CHILD(role, @"ROLE"); +} + + +- (NSData *)logo { + NSData *decodedData = nil; + NSXMLElement *logo = [self elementForName:@"LOGO"]; + + if (logo != nil) { + // There is a LOGO element. It should have a TYPE and a BINVAL + //NSXMLElement *fileType = [photo elementForName:@"TYPE"]; + NSXMLElement *binval = [logo elementForName:@"BINVAL"]; + + if (binval) { + NSData *base64Data = [[binval stringValue] dataUsingEncoding:NSASCIIStringEncoding]; + decodedData = [base64Data xmpp_base64Decoded]; + } + } + + return decodedData; +} + + +- (void)setLogo:(NSData *)data { + NSXMLElement *logo = [self elementForName:@"LOGO"]; + + if (logo == nil) { + logo = [NSXMLElement elementWithName:@"LOGO"]; + [self addChild:logo]; + } + + NSXMLElement *binval = [logo elementForName:@"BINVAL"]; + + if (binval == nil) { + binval = [NSXMLElement elementWithName:@"BINVAL"]; + [logo addChild:binval]; + } + + [binval setStringValue:[data xmpp_base64Encoded]]; +} + + +- (XMPPvCardTemp *)agent { + XMPPvCardTemp *agent = nil; + NSXMLElement *elem = [self elementForName:@"AGENT"]; + + if (elem != nil) { + agent = [XMPPvCardTemp vCardTempFromElement:elem]; + } + + return agent; +} + + +- (void)setAgent:(XMPPvCardTemp *)agent { + NSXMLElement *elem = [self elementForName:@"AGENT"]; + + if (elem != nil) { + [self removeChildAtIndex:[[self children] indexOfObject:elem]]; + } + + if (agent != nil) { + [self addChild:agent]; + } +} + + +- (NSString *)orgName { + NSString *result = nil; + NSXMLElement *org = [self elementForName:@"ORG"]; + + if (org != nil) { + NSXMLElement *orgname = [org elementForName:@"ORGNAME"]; + + if (orgname != nil) { + result = [orgname stringValue]; + } + } + + return result; +} + + +- (void)setOrgName:(NSString *)orgname { + NSXMLElement *org = [self elementForName:@"ORG"]; + NSXMLElement *elem = nil; + + if (orgname != nil) { + if (org == nil) { + org = [NSXMLElement elementWithName:@"ORG"]; + [self addChild:org]; + } else { + elem = [org elementForName:@"ORGNAME"]; + } + + if (elem == nil) { + elem = [NSXMLElement elementWithName:@"ORGNAME"]; + [org addChild:elem]; + } + + [elem setStringValue:orgname]; + } else if (org != nil) { + // This implicitly removes all orgunits too, as per the spec + [self removeChildAtIndex:[[self children] indexOfObject:org]]; + } +} + + +- (NSArray *)orgUnits { + NSArray *result = nil; + NSXMLElement *org = [self elementForName:@"ORG"]; + + if (org != nil) { + NSArray *elems = [org elementsForName:@"ORGUNIT"]; + NSMutableArray *arr = [[NSMutableArray alloc] initWithCapacity:[elems count]]; + + for (NSXMLElement *elem in elems) { + [arr addObject:[elem stringValue]]; + } + + result = [NSArray arrayWithArray:arr]; + } + + return result; +} + + +- (void)setOrgUnits:(NSArray *)orgunits { + NSXMLElement *org = [self elementForName:@"ORG"]; + + // If there is no org, then there is nothing to do (need ORGNAME first) + if (org != nil) { + NSArray *elems = [org elementsForName:@"ORGUNIT"]; + for (NSXMLElement *elem in elems) { + [org removeChildAtIndex:[[org children] indexOfObject:elem]]; + } + + for (NSString *unit in orgunits) { + NSXMLElement *elem = [NSXMLElement elementWithName:@"ORGUNIT"]; + [elem setStringValue:unit]; + + [org addChild:elem]; + } + } +} + + +#pragma mark Explanatory Types + + +- (NSArray *)categories { + NSArray *result = nil; + NSXMLElement *categories = [self elementForName:@"CATEGORIES"]; + + if (categories != nil) { + NSArray *elems = [categories elementsForName:@"KEYWORD"]; + NSMutableArray *arr = [[NSMutableArray alloc] initWithCapacity:[elems count]]; + + for (NSXMLElement *elem in elems) { + [arr addObject:[elem stringValue]]; + } + + result = [NSArray arrayWithArray:arr]; + } + + return result; +} + + +- (void)setCategories:(NSArray *)categories { + NSXMLElement *cat = [self elementForName:@"CATEGORIES"]; + + if (categories != nil) { + if (cat == nil) { + cat = [NSXMLElement elementWithName:@"CATEGORIES"]; + [self addChild:cat]; + } + + NSArray *elems = [cat elementsForName:@"KEYWORD"]; + for (NSXMLElement *elem in elems) { + [cat removeChildAtIndex:[[cat children] indexOfObject:elem]]; + } + + for (NSString *kw in categories) { + NSXMLElement *elem = [NSXMLElement elementWithName:@"KEYWORD"]; + [elem setStringValue:kw]; + + [cat addChild:elem]; + } + } else if (cat != nil) { + [self removeChildAtIndex:[[self children] indexOfObject:cat]]; + } +} + + +- (NSString *)note { + return [[self elementForName:@"NOTE"] stringValue]; +} + + +- (void)setNote:(NSString *)note { + XMPP_VCARD_SET_STRING_CHILD(note, @"NOTE"); +} + + +- (NSString *)prodid { + return [[self elementForName:@"PRODID"] stringValue]; +} + + +- (void)setProdid:(NSString *)prodid { + XMPP_VCARD_SET_STRING_CHILD(prodid, @"PRODID"); +} + + +- (NSDate *)revision { + NSDate *rev = nil; + NSXMLElement *elem = [self elementForName:@"REV"]; + + if (elem != nil) { + rev = [NSDate dateWithXmppDateTimeString:[elem stringValue]]; + } + + return rev; +} + + +- (void)setRevision:(NSDate *)rev { + NSXMLElement *elem = [self elementForName:@"REV"]; + + if (elem == nil) { + elem = [NSXMLElement elementWithName:@"REV"]; + [self addChild:elem]; + } + + [elem setStringValue:[rev xmppDateTimeString]]; +} + + +- (NSString *)sortString { + return [[self elementForName:@"SORT-STRING"] stringValue]; +} +- (void)setSortString:(NSString *)sortString { + XMPP_VCARD_SET_STRING_CHILD(sortString, @"SORT-STRING"); +} + + +- (NSString *)phoneticSound { + NSString *phon = nil; + NSXMLElement *sound = [self elementForName:@"SOUND"]; + + if (sound != nil) { + NSXMLElement *elem = [sound elementForName:@"PHONETIC"]; + + if (elem != nil) { + phon = [elem stringValue]; + } + } + + return phon; +} + + +- (void)setPhoneticSound:(NSString *)phonetic { + NSXMLElement *sound = [self elementForName:@"SOUND"]; + NSXMLElement *elem = nil; + + if (sound == nil && phonetic != nil) { + sound = [NSXMLElement elementWithName:@"SOUND"]; + [self addChild:sound]; + } + + if (sound != nil) { + elem = [sound elementForName:@"PHONETIC"]; + + if (elem != nil && phonetic != nil) { + elem = [NSXMLElement elementWithName:@"PHONETIC"]; + [sound addChild:elem]; + } + } + + if (phonetic != nil) { + [elem setStringValue:phonetic]; + } else if (sound != nil) { + [self removeChildAtIndex:[[self children] indexOfObject:phonetic]]; + } +} + + +- (NSData *)sound { + NSData *decodedData = nil; + NSXMLElement *sound = [self elementForName:@"SOUND"]; + + if (sound != nil) { + NSXMLElement *binval = [sound elementForName:@"BINVAL"]; + + if (binval) { + NSData *base64Data = [[binval stringValue] dataUsingEncoding:NSASCIIStringEncoding]; + decodedData = [base64Data xmpp_base64Decoded]; + } + } + + return decodedData; +} + + +- (void)setSound:(NSData *)data { + NSXMLElement *sound = [self elementForName:@"SOUND"]; + + if (sound == nil) { + sound = [NSXMLElement elementWithName:@"SOUND"]; + [self addChild:sound]; + } + + NSXMLElement *binval = [sound elementForName:@"BINVAL"]; + + if (binval == nil) { + binval = [NSXMLElement elementWithName:@"BINVAL"]; + [sound addChild:binval]; + } + + [binval setStringValue:[data xmpp_base64Encoded]]; +} + + +- (NSString *)uid { + return [[self elementForName:@"UID"] stringValue]; +} + + +- (void)setUid:(NSString *)uid { + XMPP_VCARD_SET_STRING_CHILD(uid, @"UID"); +} + + +- (NSString *)url { + return [[self elementForName:@"URL"] stringValue]; +} + + +- (void)setUrl:(NSString *)url { + XMPP_VCARD_SET_STRING_CHILD(url, @"URL"); +} + + +- (NSString *)version { + return [self attributeStringValueForName:@"version"]; +} + + +- (void)setVersion:(NSString *)version { + [self addAttributeWithName:@"version" stringValue:version]; +} + + +- (NSString *)desc { + return [[self elementForName:@"DESC"] stringValue]; +} + + +- (void)setDesc:(NSString *)desc { + XMPP_VCARD_SET_STRING_CHILD(desc, @"DESC"); +} + + +#pragma mark Security Types + + +- (XMPPvCardTempClass)privacyClass { + XMPPvCardTempClass priv = XMPPvCardTempClassNone; + NSXMLElement *elem = [self elementForName:@"CLASS"]; + + if (elem != nil) { + if ([elem elementForName:@"PUBLIC"] != nil) { + priv = XMPPvCardTempClassPublic; + } else if ([elem elementForName:@"PRIVATE"] != nil) { + priv = XMPPvCardTempClassPrivate; + } else if ([elem elementForName:@"CONFIDENTIAL"] != nil) { + priv = XMPPvCardTempClassConfidential; + } + } + + return priv; +} + + +- (void)setPrivacyClass:(XMPPvCardTempClass)privacyClass { + NSXMLElement *elem = [self elementForName:@"CLASS"]; + + if (elem == nil && privacyClass != XMPPvCardTempClassNone) { + elem = [NSXMLElement elementWithName:@"CLASS"]; + } + + if (elem != nil) { + for (NSString *cls in @[@"PUBLIC", @"PRIVATE", @"CONFIDENTIAL"]) { + NSXMLElement *priv = [elem elementForName:cls]; + if (priv != nil) { + [elem removeChildAtIndex:[[elem children] indexOfObject:priv]]; + } + } + + switch (privacyClass) { + case XMPPvCardTempClassPublic: + [elem addChild:[NSXMLElement elementWithName:@"PUBLIC"]]; + break; + case XMPPvCardTempClassPrivate: + [elem addChild:[NSXMLElement elementWithName:@"PRIVATE"]]; + break; + case XMPPvCardTempClassConfidential: + [elem addChild:[NSXMLElement elementWithName:@"CONFIDENTIAL"]]; + break; + default: + case XMPPvCardTempClassNone: + // Remove the whole element + [self removeChildAtIndex:[[self children] indexOfObject:elem]]; + break; + } + } +} + + +- (NSData *)key { return nil; } +- (void)setKey:(NSData *)key { } + + +- (NSString *)keyType { + NSString *typ = nil; + NSXMLElement *key = [self elementForName:@"KEY"]; + + if (key != nil) { + NSXMLElement *elem = [key elementForName:@"TYPE"]; + + if (elem != nil) { + typ = [elem stringValue]; + } + } + + return typ; +} + + +- (void)setKeyType:(NSString *)type { + NSXMLElement *key = [self elementForName:@"KEY"]; + NSXMLElement *elem = nil; + + if (key == nil && type != nil) { + key = [NSXMLElement elementWithName:@"KEY"]; + [self addChild:key]; + } + + if (key != nil) { + elem = [key elementForName:@"TYPE"]; + + if (elem != nil && type != nil) { + elem = [NSXMLElement elementWithName:@"TYPE"]; + [key addChild:elem]; + } + } + + if (type != nil) { + [elem setStringValue:type]; + } else if (key != nil) { + [self removeChildAtIndex:[[self children] indexOfObject:key]]; + } +} + + +@end diff --git a/Extensions/XEP-0054/XMPPvCardTempAdr.h b/Extensions/XEP-0054/XMPPvCardTempAdr.h new file mode 100644 index 0000000..212a38a --- /dev/null +++ b/Extensions/XEP-0054/XMPPvCardTempAdr.h @@ -0,0 +1,28 @@ +// +// XMPPvCardTempAdr.h +// XEP-0054 vCard-temp +// +// Created by Eric Chamberlain on 3/9/11. +// Copyright 2011 RF.com. All rights reserved. +// Copyright 2010 Martin Morrison. All rights reserved. +// + + +#import + +#import "XMPPvCardTempAdrTypes.h" + + +@interface XMPPvCardTempAdr : XMPPvCardTempAdrTypes + ++ (XMPPvCardTempAdr *)vCardAdrFromElement:(NSXMLElement *)elem; + +@property (nonatomic, weak) NSString *pobox; +@property (nonatomic, weak) NSString *extendedAddress; +@property (nonatomic, weak) NSString *street; +@property (nonatomic, weak) NSString *locality; +@property (nonatomic, weak) NSString *region; +@property (nonatomic, weak) NSString *postalCode; +@property (nonatomic, weak) NSString *country; + +@end diff --git a/Extensions/XEP-0054/XMPPvCardTempAdr.m b/Extensions/XEP-0054/XMPPvCardTempAdr.m new file mode 100644 index 0000000..24eb38f --- /dev/null +++ b/Extensions/XEP-0054/XMPPvCardTempAdr.m @@ -0,0 +1,136 @@ +// +// XMPPvCardTempAdr.m +// XEP-0054 vCard-temp +// +// Created by Eric Chamberlain on 3/9/11. +// Copyright 2011 RF.com. All rights reserved. +// Copyright 2010 Martin Morrison. All rights reserved. +// + + +#import "XMPPvCardTempAdr.h" +#import "XMPPLogging.h" + +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_ERROR; +#endif + + +@implementation XMPPvCardTempAdr + +#if DEBUG + ++ (void)initialize +{ + // We use the object_setClass method below to dynamically change the class from a standard NSXMLElement. + // The size of the two classes is expected to be the same. + // + // If a developer adds instance methods to this class, bad things happen at runtime that are very hard to debug. + // This check is here to aid future developers who may make this mistake. + // + // For Fearless And Experienced Objective-C Developers: + // It may be possible to support adding instance variables to this class if you seriously need it. + // To do so, try realloc'ing self after altering the class, and then initialize your variables. + + size_t superSize = class_getInstanceSize([NSXMLElement class]); + size_t ourSize = class_getInstanceSize([XMPPvCardTempAdr class]); + + if (superSize != ourSize) + { + XMPPLogError(@"Adding instance variables to XMPPvCardTempAdr is not currently supported!"); + + [DDLog flushLog]; + exit(15); + } +} + +#endif + ++ (XMPPvCardTempAdr *)vCardAdrFromElement:(NSXMLElement *)elem { + object_setClass(elem, [XMPPvCardTempAdr class]); + + return (XMPPvCardTempAdr *)elem; +} + + +#pragma mark - +#pragma mark Getter/setter methods + + +- (NSString *)pobox { + return [[self elementForName:@"POBOX"] stringValue]; +} + + +- (void)setPobox:(NSString *)pobox { + XMPP_VCARD_SET_STRING_CHILD(pobox, @"POBOX"); +} + + +- (NSString *)extendedAddress { + return [[self elementForName:@"EXTADD"] stringValue]; +} + + +- (void)setExtendedAddress:(NSString *)extadd { + XMPP_VCARD_SET_STRING_CHILD(extadd, @"EXTADD"); +} + + +- (NSString *)street { + return [[self elementForName:@"STREET"] stringValue]; +} + + +- (void)setStreet:(NSString *)street { + XMPP_VCARD_SET_STRING_CHILD(street, @"STREET"); +} + + +- (NSString *)locality { + return [[self elementForName:@"LOCALITY"] stringValue]; +} + + +- (void)setLocality:(NSString *)locality { + XMPP_VCARD_SET_STRING_CHILD(locality, @"LOCALITY"); +} + + +- (NSString *)region { + return [[self elementForName:@"REGION"] stringValue]; +} + + +- (void)setRegion:(NSString *)region { + XMPP_VCARD_SET_STRING_CHILD(region, @"REGION"); +} + + +- (NSString *)postalCode { + return [[self elementForName:@"PCODE"] stringValue]; +} + + +- (void)setPostalCode:(NSString *)pcode { + XMPP_VCARD_SET_STRING_CHILD(pcode, @"PCODE"); +} + + +- (NSString *)country { + return [[self elementForName:@"CTRY"] stringValue]; +} + + +- (void)setCountry:(NSString *)ctry { + XMPP_VCARD_SET_STRING_CHILD(ctry, @"CTRY"); +} + + +@end diff --git a/Extensions/XEP-0054/XMPPvCardTempAdrTypes.h b/Extensions/XEP-0054/XMPPvCardTempAdrTypes.h new file mode 100644 index 0000000..db2b481 --- /dev/null +++ b/Extensions/XEP-0054/XMPPvCardTempAdrTypes.h @@ -0,0 +1,28 @@ +// +// XMPPvCardTempAdrTypes.h +// XEP-0054 vCard-temp +// +// Created by Eric Chamberlain on 3/9/11. +// Copyright 2011 RF.com. All rights reserved. +// Copyright 2010 Martin Morrison. All rights reserved. +// + + +#import + +#import "XMPPvCardTempBase.h" + + +@interface XMPPvCardTempAdrTypes : XMPPvCardTempBase + + +@property (nonatomic, assign, setter=setHome:) BOOL isHome; +@property (nonatomic, assign, setter=setWork:) BOOL isWork; +@property (nonatomic, assign, setter=setParcel:) BOOL isParcel; +@property (nonatomic, assign, setter=setPostal:) BOOL isPostal; +@property (nonatomic, assign, setter=setDomestic:) BOOL isDomestic; +@property (nonatomic, assign, setter=setInternational:) BOOL isInternational; +@property (nonatomic, assign, setter=setPreferred:) BOOL isPreferred; + + +@end diff --git a/Extensions/XEP-0054/XMPPvCardTempAdrTypes.m b/Extensions/XEP-0054/XMPPvCardTempAdrTypes.m new file mode 100644 index 0000000..02e5924 --- /dev/null +++ b/Extensions/XEP-0054/XMPPvCardTempAdrTypes.m @@ -0,0 +1,103 @@ +// +// XMPPvCardTempAdrTypes.m +// XEP-0054 vCard-temp +// +// Created by Eric Chamberlain on 3/9/11. +// Copyright 2011 RF.com. All rights reserved. +// Copyright 2010 Martin Morrison. All rights reserved. +// + + +#import "XMPPvCardTempAdrTypes.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation XMPPvCardTempAdrTypes + + +#pragma mark - +#pragma mark Getter/setter methods + + +- (BOOL)isHome { + return [self elementForName:@"HOME"] != nil; +} + + +- (void)setHome:(BOOL)home { + XMPP_VCARD_SET_EMPTY_CHILD(home && ![self isHome], @"HOME"); +} + + +- (BOOL)isWork { + return [self elementForName:@"WORK"] != nil; +} + + +- (void)setWork:(BOOL)work { + XMPP_VCARD_SET_EMPTY_CHILD(work && ![self isWork], @"WORK"); +} + + +- (BOOL)isParcel { + return [self elementForName:@"PARCEL"] != nil; +} + + +- (void)setParcel:(BOOL)parcel { + XMPP_VCARD_SET_EMPTY_CHILD(parcel && ![self isParcel], @"PARCEL"); +} + + +- (BOOL)isPostal { + return [self elementForName:@"POSTAL"] != nil; +} + + +- (void)setPostal:(BOOL)postal { + XMPP_VCARD_SET_EMPTY_CHILD(postal && ![self isPostal], @"POSTAL"); +} + + +- (BOOL)isDomestic { + return [self elementForName:@"DOM"] != nil; +} + + +- (void)setDomestic:(BOOL)dom { + XMPP_VCARD_SET_EMPTY_CHILD(dom && ![self isDomestic], @"DOM"); + // INTL and DOM are mutually exclusive + if (dom) { + [self setInternational:NO]; + } +} + + +- (BOOL)isInternational { + return [self elementForName:@"INTL"] != nil; +} + + +- (void)setInternational:(BOOL)intl { + XMPP_VCARD_SET_EMPTY_CHILD(intl && ![self isInternational], @"INTL"); + // INTL and DOM are mutually exclusive + if (intl) { + [self setDomestic:NO]; + } +} + + +- (BOOL)isPreferred { + return [self elementForName:@"PREF"] != nil; +} + + +- (void)setPreferred:(BOOL)pref { + XMPP_VCARD_SET_EMPTY_CHILD(pref && ![self isPreferred], @"PREF"); +} + + +@end diff --git a/Extensions/XEP-0054/XMPPvCardTempBase.h b/Extensions/XEP-0054/XMPPvCardTempBase.h new file mode 100644 index 0000000..588dc88 --- /dev/null +++ b/Extensions/XEP-0054/XMPPvCardTempBase.h @@ -0,0 +1,69 @@ +// +// XMPPvCardTempBase.h +// XEP-0054 vCard-temp +// +// Created by Eric Chamberlain on 3/9/11. +// Copyright 2011 RF.com. All rights reserved. +// Copyright 2010 Martin Morrison. All rights reserved. +// + + +#import +#import "NSXMLElement+XMPP.h" + + +#define XMPP_VCARD_SET_EMPTY_CHILD(Set, Name) \ + if (Set) { \ + [self addChild:[NSXMLElement elementWithName:(Name)]]; \ + } \ + else if (!(Set)) { \ + [self removeChildAtIndex:[[self children] indexOfObject:[self elementForName:(Name)]]]; \ + } + + +#define XMPP_VCARD_SET_STRING_CHILD(Value, Name) \ + NSXMLElement *elem = [self elementForName:(Name)]; \ + if ((Value) != nil) \ + { \ + if (elem == nil) { \ + elem = [NSXMLElement elementWithName:(Name)]; \ + [self addChild:elem]; \ + } \ + [elem setStringValue:(Value)]; \ + } \ + else if (elem != nil) { \ + [self removeChildAtIndex:[[self children] indexOfObject:elem]]; \ + } + + +#define XMPP_VCARD_SET_N_CHILD(Value, Name) \ + NSXMLElement *name = [self elementForName:@"N"]; \ + if ((Value) != nil && name == nil) \ + { \ + name = [NSXMLElement elementWithName:@"N"]; \ + [self addChild:name]; \ + } \ + \ + NSXMLElement *part = [name elementForName:(Name)]; \ + if ((Value) != nil && part == nil) \ + { \ + part = [NSXMLElement elementWithName:(Name)]; \ + [name addChild:part]; \ + } \ + \ + if (Value) \ + { \ + [part setStringValue:(Value)]; \ + } \ + else if (part != nil) \ + { \ + /* N is mandatory, so we leave it in. */ \ + [name removeChildAtIndex:[[self children] indexOfObject:part]]; \ + } + + +@interface XMPPvCardTempBase : NSXMLElement { + +} + +@end diff --git a/Extensions/XEP-0054/XMPPvCardTempBase.m b/Extensions/XEP-0054/XMPPvCardTempBase.m new file mode 100644 index 0000000..777328c --- /dev/null +++ b/Extensions/XEP-0054/XMPPvCardTempBase.m @@ -0,0 +1,102 @@ +// +// XMPPvCardTempBase.m +// XEP-0054 vCard-temp +// +// Created by Eric Chamberlain on 3/9/11. +// Copyright 2011 RF.com. All rights reserved. +// Copyright 2010 Martin Morrison. All rights reserved. +// + +#import "XMPPvCardTempBase.h" + +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +@implementation XMPPvCardTempBase + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark NSCoding protocol +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + +#if ! TARGET_OS_IPHONE +- (id)replacementObjectForPortCoder:(NSPortCoder *)encoder +{ + if([encoder isBycopy]) + return self; + else + return [super replacementObjectForPortCoder:encoder]; + // return [NSDistantObject proxyWithLocal:self connection:[encoder connection]]; +} +#endif + + +- (id)initWithCoder:(NSCoder *)coder +{ + NSString *xmlString; + if([coder allowsKeyedCoding]) + { + if([coder respondsToSelector:@selector(requiresSecureCoding)] && + [coder requiresSecureCoding]) + { + xmlString = [coder decodeObjectOfClass:[NSString class] forKey:@"xmlString"]; + } + else + { + xmlString = [coder decodeObjectForKey:@"xmlString"]; + } + } + else + { + xmlString = [coder decodeObject]; + } + + // The method [super initWithXMLString:error:] may return a different self. + // In other words, it may [self release], and alloc/init/return a new self. + // + // So to maintain the proper class (XMPPvCardTempEmail, XMPPvCardTempTel, etc) + // we need to get a reference to the class before invoking super. + + Class selfClass = [self class]; + + if ((self = [super initWithXMLString:xmlString error:nil])) + { + object_setClass(self, selfClass); + } + return self; +} + + +- (void)encodeWithCoder:(NSCoder *)coder +{ + NSString *xmlString = [self XMLString]; + + if([coder allowsKeyedCoding]) + { + [coder encodeObject:xmlString forKey:@"xmlString"]; + } + else + { + [coder encodeObject:xmlString]; + } +} + ++ (BOOL) supportsSecureCoding +{ + return YES; +} + +- (id)copyWithZone:(NSZone *)zone +{ + NSXMLElement *elementCopy = [super copyWithZone:zone]; + object_setClass(elementCopy, [self class]); + + return elementCopy; +} + +@end diff --git a/Extensions/XEP-0054/XMPPvCardTempEmail.h b/Extensions/XEP-0054/XMPPvCardTempEmail.h new file mode 100644 index 0000000..214efff --- /dev/null +++ b/Extensions/XEP-0054/XMPPvCardTempEmail.h @@ -0,0 +1,29 @@ +// +// XMPPvCardTempEmail.h +// XEP-0054 vCard-temp +// +// Created by Eric Chamberlain on 3/9/11. +// Copyright 2011 RF.com. All rights reserved. +// Copyright 2010 Martin Morrison. All rights reserved. +// + + +#import + +#import "XMPPvCardTempBase.h" + + +@interface XMPPvCardTempEmail : XMPPvCardTempBase + ++ (XMPPvCardTempEmail *)vCardEmailFromElement:(NSXMLElement *)elem; + +@property (nonatomic, assign, setter=setHome:) BOOL isHome; +@property (nonatomic, assign, setter=setWork:) BOOL isWork; +@property (nonatomic, assign, setter=setInternet:) BOOL isInternet; +@property (nonatomic, assign, setter=setX400:) BOOL isX400; +@property (nonatomic, assign, setter=setPreferred:) BOOL isPreferred; + +@property (nonatomic, weak) NSString *userid; + + +@end diff --git a/Extensions/XEP-0054/XMPPvCardTempEmail.m b/Extensions/XEP-0054/XMPPvCardTempEmail.m new file mode 100644 index 0000000..f3dd91e --- /dev/null +++ b/Extensions/XEP-0054/XMPPvCardTempEmail.m @@ -0,0 +1,125 @@ +// +// XMPPvCardTempEmail.m +// XEP-0054 vCard-temp +// +// Created by Eric Chamberlain on 3/9/11. +// Copyright 2011 RF.com. All rights reserved. +// Copyright 2010 Martin Morrison. All rights reserved. +// + + +#import "XMPPvCardTempEmail.h" +#import "XMPPLogging.h" + +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_ERROR; +#endif + + +@implementation XMPPvCardTempEmail + +#if DEBUG + ++ (void)initialize { + // We use the object_setClass method below to dynamically change the class from a standard NSXMLElement. + // The size of the two classes is expected to be the same. + // + // If a developer adds instance methods to this class, bad things happen at runtime that are very hard to debug. + // This check is here to aid future developers who may make this mistake. + // + // For Fearless And Experienced Objective-C Developers: + // It may be possible to support adding instance variables to this class if you seriously need it. + // To do so, try realloc'ing self after altering the class, and then initialize your variables. + + size_t superSize = class_getInstanceSize([NSXMLElement class]); + size_t ourSize = class_getInstanceSize([XMPPvCardTempEmail class]); + + if (superSize != ourSize) + { + XMPPLogError(@"Adding instance variables to XMPPvCardTempEmail is not currently supported!"); + + [DDLog flushLog]; + exit(15); + } +} + +#endif + ++ (XMPPvCardTempEmail *)vCardEmailFromElement:(NSXMLElement *)elem { + object_setClass(elem, [XMPPvCardTempEmail class]); + + return (XMPPvCardTempEmail *)elem; +} + + +#pragma mark - +#pragma mark Getter/setter methods + + +- (BOOL)isHome { + return [self elementForName:@"HOME"] != nil; +} + + +- (void)setHome:(BOOL)home { + XMPP_VCARD_SET_EMPTY_CHILD(home && ![self isHome], @"HOME"); +} + + +- (BOOL)isWork { + return [self elementForName:@"WORK"] != nil; +} + + +- (void)setWork:(BOOL)work { + XMPP_VCARD_SET_EMPTY_CHILD(work && ![self isWork], @"WORK"); +} + + +- (BOOL)isInternet { + return [self elementForName:@"INTERNET"] != nil; +} + + +- (void)setInternet:(BOOL)internet { + XMPP_VCARD_SET_EMPTY_CHILD(internet && ![self isInternet], @"INTERNET"); +} + + +- (BOOL)isX400 { + return [self elementForName:@"X400"] != nil; +} + + +- (void)setX400:(BOOL)x400 { + XMPP_VCARD_SET_EMPTY_CHILD(x400 && ![self isX400], @"X400"); +} + + +- (BOOL)isPreferred { + return [self elementForName:@"PREF"] != nil; +} + + +- (void)setPreferred:(BOOL)pref { + XMPP_VCARD_SET_EMPTY_CHILD(pref && ![self isPreferred], @"PREF"); +} + + +- (NSString *)userid { + return [[self elementForName:@"USERID"] stringValue]; +} + + +- (void)setUserid:(NSString *)userid { + XMPP_VCARD_SET_STRING_CHILD(userid, @"USERID"); +} + + +@end diff --git a/Extensions/XEP-0054/XMPPvCardTempLabel.h b/Extensions/XEP-0054/XMPPvCardTempLabel.h new file mode 100644 index 0000000..3924061 --- /dev/null +++ b/Extensions/XEP-0054/XMPPvCardTempLabel.h @@ -0,0 +1,25 @@ +// +// XMPPvCardTempLabel.h +// XEP-0054 vCard-temp +// +// Created by Eric Chamberlain on 3/9/11. +// Copyright 2011 RF.com. All rights reserved. +// Copyright 2010 Martin Morrison. All rights reserved. +// + + +#import + +#import "XMPPvCardTempAdrTypes.h" + + +@interface XMPPvCardTempLabel : XMPPvCardTempAdrTypes + + +@property (nonatomic, weak) NSArray *lines; + + ++ (XMPPvCardTempLabel *)vCardLabelFromElement:(NSXMLElement *)elem; + + +@end diff --git a/Extensions/XEP-0054/XMPPvCardTempLabel.m b/Extensions/XEP-0054/XMPPvCardTempLabel.m new file mode 100644 index 0000000..2e4875c --- /dev/null +++ b/Extensions/XEP-0054/XMPPvCardTempLabel.m @@ -0,0 +1,93 @@ +// +// XMPPvCardTempLabel.m +// XEP-0054 vCard-temp +// +// Created by Eric Chamberlain on 3/9/11. +// Copyright 2011 RF.com. All rights reserved. +// Copyright 2010 Martin Morrison. All rights reserved. +// + + +#import "XMPPvCardTempLabel.h" +#import "XMPPLogging.h" + +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_ERROR; +#endif + + +@implementation XMPPvCardTempLabel + +#if DEBUG + ++ (void)initialize { + // We use the object_setClass method below to dynamically change the class from a standard NSXMLElement. + // The size of the two classes is expected to be the same. + // + // If a developer adds instance methods to this class, bad things happen at runtime that are very hard to debug. + // This check is here to aid future developers who may make this mistake. + // + // For Fearless And Experienced Objective-C Developers: + // It may be possible to support adding instance variables to this class if you seriously need it. + // To do so, try realloc'ing self after altering the class, and then initialize your variables. + + size_t superSize = class_getInstanceSize([NSXMLElement class]); + size_t ourSize = class_getInstanceSize([XMPPvCardTempLabel class]); + + if (superSize != ourSize) + { + XMPPLogError(@"Adding instance variables to XMPPvCardTempLabel is not currently supported!"); + + [DDLog flushLog]; + exit(15); + } +} + +#endif + ++ (XMPPvCardTempLabel *)vCardLabelFromElement:(NSXMLElement *)elem { + object_setClass(elem, [XMPPvCardTempLabel class]); + + return (XMPPvCardTempLabel *)elem; +} + + +#pragma mark - +#pragma mark Getter/setter methods + + +- (NSArray *)lines { + NSArray *elems = [self elementsForName:@"LINE"]; + NSMutableArray *lines = [[NSMutableArray alloc] initWithCapacity:[elems count]]; + + for (NSXMLElement *elem in elems) { + [lines addObject:[elem stringValue]]; + } + + NSArray *result = [NSArray arrayWithArray:lines]; + return result; +} + + +- (void)setLines:(NSArray *)lines { + NSArray *elems = [self elementsForName:@"LINE"]; + + for (NSXMLElement *elem in elems) { + [self removeChildAtIndex:[[self children] indexOfObject:elem]]; + } + + for (NSString *line in lines) { + NSXMLElement *elem = [NSXMLElement elementWithName:@"LINE"]; + [elem setStringValue:line]; + [self addChild:elem]; + } +} + + +@end diff --git a/Extensions/XEP-0054/XMPPvCardTempModule.h b/Extensions/XEP-0054/XMPPvCardTempModule.h new file mode 100755 index 0000000..01c1bc0 --- /dev/null +++ b/Extensions/XEP-0054/XMPPvCardTempModule.h @@ -0,0 +1,117 @@ +// +// XMPPvCardTempModule.h +// XEP-0054 vCard-temp +// +// Created by Eric Chamberlain on 3/17/11. +// Copyright 2011 RF.com. All rights reserved. +// + + +#import +#import "XMPP.h" + +@class XMPPvCardTemp; +@class XMPPIDTracker; + +#define _XMPP_VCARD_TEMP_MODULE_H + +@protocol XMPPvCardTempModuleStorage; + + +@interface XMPPvCardTempModule : XMPPModule +{ + id __strong _xmppvCardTempModuleStorage; + XMPPIDTracker *_myvCardTracker; +} + + +@property(nonatomic, strong, readonly) id xmppvCardTempModuleStorage; +@property(nonatomic, strong, readonly) XMPPvCardTemp *myvCardTemp; + +- (id)initWithvCardStorage:(id )storage; +- (id)initWithvCardStorage:(id )storage dispatchQueue:(dispatch_queue_t)queue; + +/** + * Fetches the vCardTemp for the given JID if it is not in the storage +**/ +- (void)fetchvCardTempForJID:(XMPPJID *)jid; + +/** + * Fetches the vCardTemp for the given JID, optionally ignoring the storage +**/ +- (void)fetchvCardTempForJID:(XMPPJID *)jid ignoreStorage:(BOOL)ignoreStorage; + +/** + * Returns the vCardTemp for the given JID, this is the equivalent of calling the vCardTempForJID:xmppStream: on the moduleStorage + * If there is no vCardTemp in the storage for the given jid and shouldFetch is YES, it will automatically fetch it from the network +**/ +- (XMPPvCardTemp *)vCardTempForJID:(XMPPJID *)jid shouldFetch:(BOOL)shouldFetch; + +/** + * Updates myvCard in storage and sends it to the server +**/ +- (void)updateMyvCardTemp:(XMPPvCardTemp *)vCardTemp; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPvCardTempModuleDelegate +@optional + +- (void)xmppvCardTempModule:(XMPPvCardTempModule *)vCardTempModule + didReceivevCardTemp:(XMPPvCardTemp *)vCardTemp + forJID:(XMPPJID *)jid; + +- (void)xmppvCardTempModuleDidUpdateMyvCard:(XMPPvCardTempModule *)vCardTempModule; + +- (void)xmppvCardTempModule:(XMPPvCardTempModule *)vCardTempModule failedToUpdateMyvCard:(NSXMLElement *)error; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPvCardTempModuleStorage + +/** + * Configures the storage class, passing its parent and parent's dispatch queue. + * + * This method is called by the init methods of the XMPPvCardTempModule class. + * This method is designed to inform the storage class of its parent + * and of the dispatch queue the parent will be operating on. + * + * The storage class may choose to operate on the same queue as its parent, + * or it may operate on its own internal dispatch queue. + * + * This method should return YES if it was configured properly. + * The parent class is configured to ignore the passed + * storage class in its init method if this method returns NO. +**/ +- (BOOL)configureWithParent:(XMPPvCardTempModule *)aParent queue:(dispatch_queue_t)queue; + +/** + * Returns a vCardTemp object or nil +**/ +- (XMPPvCardTemp *)vCardTempForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream; + +/** + * Used to set the vCardTemp object when we get it from the XMPP server. +**/ +- (void)setvCardTemp:(XMPPvCardTemp *)vCardTemp forJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream; + +/** + * Returns My vCardTemp object or nil +**/ +- (XMPPvCardTemp *)myvCardTempForXMPPStream:(XMPPStream *)stream; + +/** + * Asks the backend if we should fetch the vCardTemp from the network. + * This is used so that we don't request the vCardTemp multiple times. +**/ +- (BOOL)shouldFetchvCardTempForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream; + +@end diff --git a/Extensions/XEP-0054/XMPPvCardTempModule.m b/Extensions/XEP-0054/XMPPvCardTempModule.m new file mode 100755 index 0000000..38d5021 --- /dev/null +++ b/Extensions/XEP-0054/XMPPvCardTempModule.m @@ -0,0 +1,286 @@ +// +// XMPPvCardTempModule.m +// XEP-0054 vCard-temp +// +// Created by Eric Chamberlain on 3/17/11. +// Copyright 2011 RF.com. All rights reserved. +// + +#import "XMPP.h" +#import "XMPPLogging.h" +#import "XMPPvCardTempModule.h" +#import "XMPPvCardTemp.h" +#import "XMPPIDTracker.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +@interface XMPPvCardTempModule() + +- (void)_updatevCardTemp:(XMPPvCardTemp *)vCardTemp forJID:(XMPPJID *)jid; +- (void)_fetchvCardTempForJID:(XMPPJID *)jid; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPvCardTempModule + +@synthesize xmppvCardTempModuleStorage = _xmppvCardTempModuleStorage; + +- (id)init +{ + // This will cause a crash - it's designed to. + // Only the init methods listed in XMPPvCardTempModule.h are supported. + + return [self initWithvCardStorage:nil dispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + // This will cause a crash - it's designed to. + // Only the init methods listed in XMPPvCardTempModule.h are supported. + + return [self initWithvCardStorage:nil dispatchQueue:NULL]; +} + +- (id)initWithvCardStorage:(id )storage +{ + return [self initWithvCardStorage:storage dispatchQueue:NULL]; +} + +- (id)initWithvCardStorage:(id )storage dispatchQueue:(dispatch_queue_t)queue +{ + NSParameterAssert(storage != nil); + + if ((self = [super initWithDispatchQueue:queue])) + { + if ([storage configureWithParent:self queue:moduleQueue]) + { + _xmppvCardTempModuleStorage = storage; + } + else + { + XMPPLogError(@"%@: %@ - Unable to configure storage!", THIS_FILE, THIS_METHOD); + } + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + if ([super activate:aXmppStream]) + { + // Custom code goes here (if needed) + + _myvCardTracker = [[XMPPIDTracker alloc] initWithStream:xmppStream dispatchQueue:moduleQueue]; + + return YES; + } + + return NO; +} + +- (void)deactivate +{ + // Custom code goes here (if needed) + + dispatch_block_t block = ^{ @autoreleasepool { + + [_myvCardTracker removeAllIDs]; + _myvCardTracker = nil; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + [super deactivate]; +} + +- (void)dealloc +{ + _xmppvCardTempModuleStorage = nil; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Fetch vCardTemp methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)fetchvCardTempForJID:(XMPPJID *)jid +{ + return [self fetchvCardTempForJID:jid ignoreStorage:NO]; +} + +- (void)fetchvCardTempForJID:(XMPPJID *)jid ignoreStorage:(BOOL)ignoreStorage +{ + dispatch_block_t block = ^{ @autoreleasepool { + + XMPPvCardTemp *vCardTemp = nil; + + if (!ignoreStorage) + { + // Try loading from storage + vCardTemp = [_xmppvCardTempModuleStorage vCardTempForJID:jid xmppStream:xmppStream]; + } + + if (vCardTemp == nil && [_xmppvCardTempModuleStorage shouldFetchvCardTempForJID:jid xmppStream:xmppStream]) + { + [self _fetchvCardTempForJID:jid]; + } + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (XMPPvCardTemp *)vCardTempForJID:(XMPPJID *)jid shouldFetch:(BOOL)shouldFetch{ + + __block XMPPvCardTemp *result; + + dispatch_block_t block = ^{ @autoreleasepool { + + XMPPvCardTemp *vCardTemp = [_xmppvCardTempModuleStorage vCardTempForJID:jid xmppStream:xmppStream]; + + if (vCardTemp == nil && shouldFetch && [_xmppvCardTempModuleStorage shouldFetchvCardTempForJID:jid xmppStream:xmppStream]) + { + [self _fetchvCardTempForJID:jid]; + } + + result = vCardTemp; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (XMPPvCardTemp *)myvCardTemp +{ + return [self vCardTempForJID:[xmppStream myJID] shouldFetch:YES]; +} + +- (void)updateMyvCardTemp:(XMPPvCardTemp *)vCardTemp +{ + + dispatch_block_t block = ^{ @autoreleasepool { + + XMPPvCardTemp *newvCardTemp = [vCardTemp copy]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:nil elementID:[xmppStream generateUUID] child:newvCardTemp]; + [xmppStream sendElement:iq]; + + [_myvCardTracker addElement:iq + target:self + selector:@selector(handleMyvcard:withInfo:) + timeout:600]; + + [self _updatevCardTemp:newvCardTemp forJID:[xmppStream myJID]]; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Private +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)_updatevCardTemp:(XMPPvCardTemp *)vCardTemp forJID:(XMPPJID *)jid +{ + if(!jid) return; + + // this method could be called from anywhere + dispatch_block_t block = ^{ @autoreleasepool { + + XMPPLogVerbose(@"%@: %s %@", THIS_FILE, __PRETTY_FUNCTION__, [jid bare]); + + [_xmppvCardTempModuleStorage setvCardTemp:vCardTemp forJID:jid xmppStream:xmppStream]; + + [(id )multicastDelegate xmppvCardTempModule:self + didReceivevCardTemp:vCardTemp + forJID:jid]; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)_fetchvCardTempForJID:(XMPPJID *)jid{ + if(!jid) return; + + [xmppStream sendElement:[XMPPvCardTemp iqvCardRequestForJID:jid]]; +} + +- (void)handleMyvcard:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)trackerInfo{ + + if([iq isResultIQ]) + { + [(id )multicastDelegate xmppvCardTempModuleDidUpdateMyvCard:self]; + } + else if([iq isErrorIQ]) + { + NSXMLElement *errorElement = [iq elementForName:@"error"]; + [(id )multicastDelegate xmppvCardTempModule:self failedToUpdateMyvCard:errorElement]; + } + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStreamDelegate methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + // This method is invoked on the moduleQueue. + + [_myvCardTracker invokeForElement:iq withObject:iq]; + + // Remember XML heirarchy memory management rules. + // The passed parameter is a subnode of the IQ, and we need to pass it to an asynchronous operation. + // + // Therefore we use vCardTempCopyFromIQ instead of vCardTempSubElementFromIQ. + + + XMPPvCardTemp *vCardTemp = [XMPPvCardTemp vCardTempCopyFromIQ:iq]; + if (vCardTemp != nil) + { + [self _updatevCardTemp:vCardTemp forJID:[iq from]]; + + return YES; + } + + return NO; +} + +- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error +{ + [_myvCardTracker removeAllIDs]; +} + +@end diff --git a/Extensions/XEP-0054/XMPPvCardTempTel.h b/Extensions/XEP-0054/XMPPvCardTempTel.h new file mode 100644 index 0000000..ba6df21 --- /dev/null +++ b/Extensions/XEP-0054/XMPPvCardTempTel.h @@ -0,0 +1,37 @@ +// +// XMPPvCardTempTel.h +// XEP-0054 vCard-temp +// +// Created by Eric Chamberlain on 3/9/11. +// Copyright 2011 RF.com. All rights reserved. +// Copyright 2010 Martin Morrison. All rights reserved. +// + + +#import + +#import "XMPPvCardTempBase.h" + + +@interface XMPPvCardTempTel : XMPPvCardTempBase + + ++ (XMPPvCardTempTel *)vCardTelFromElement:(NSXMLElement *)elem; + +@property (nonatomic, assign, setter=setHome:) BOOL isHome; +@property (nonatomic, assign, setter=setWork:) BOOL isWork; +@property (nonatomic, assign, setter=setVoice:) BOOL isVoice; +@property (nonatomic, assign, setter=setFax:) BOOL isFax; +@property (nonatomic, assign, setter=setPager:) BOOL isPager; +@property (nonatomic, assign, setter=setMessaging:) BOOL hasMessaging; +@property (nonatomic, assign, setter=setCell:) BOOL isCell; +@property (nonatomic, assign, setter=setVideo:) BOOL isVideo; +@property (nonatomic, assign, setter=setBBS:) BOOL isBBS; +@property (nonatomic, assign, setter=setModem:) BOOL isModem; +@property (nonatomic, assign, setter=setISDN:) BOOL isISDN; +@property (nonatomic, assign, setter=setPCS:) BOOL isPCS; +@property (nonatomic, assign, setter=setPreferred:) BOOL isPreferred; + +@property (nonatomic, weak) NSString *number; + +@end diff --git a/Extensions/XEP-0054/XMPPvCardTempTel.m b/Extensions/XEP-0054/XMPPvCardTempTel.m new file mode 100644 index 0000000..fa5a943 --- /dev/null +++ b/Extensions/XEP-0054/XMPPvCardTempTel.m @@ -0,0 +1,204 @@ +// +// XMPPvCardTempTel.m +// XEP-0054 vCard-temp +// +// Created by Eric Chamberlain on 3/9/11. +// Copyright 2011 RF.com. All rights reserved. +// Copyright 2010 Martin Morrison. All rights reserved. +// + + +#import "XMPPvCardTempTel.h" +#import "XMPPLogging.h" + +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_ERROR; +#endif + + +@implementation XMPPvCardTempTel + +#if DEBUG + ++ (void)initialize { + // We use the object_setClass method below to dynamically change the class from a standard NSXMLElement. + // The size of the two classes is expected to be the same. + // + // If a developer adds instance methods to this class, bad things happen at runtime that are very hard to debug. + // This check is here to aid future developers who may make this mistake. + // + // For Fearless And Experienced Objective-C Developers: + // It may be possible to support adding instance variables to this class if you seriously need it. + // To do so, try realloc'ing self after altering the class, and then initialize your variables. + + size_t superSize = class_getInstanceSize([NSXMLElement class]); + size_t ourSize = class_getInstanceSize([XMPPvCardTempTel class]); + + if (superSize != ourSize) + { + XMPPLogError(@"Adding instance variables to XMPPvCardTempTel is not currently supported!"); + + [DDLog flushLog]; + exit(15); + } +} + +#endif + ++ (XMPPvCardTempTel *)vCardTelFromElement:(NSXMLElement *)elem { + object_setClass(elem, [XMPPvCardTempTel class]); + + return (XMPPvCardTempTel *)elem; +} + + +#pragma mark - +#pragma mark Getter/setter methods + +- (BOOL)isHome { + return [self elementForName:@"HOME"] != nil; +} + + +- (void)setHome:(BOOL)home { + XMPP_VCARD_SET_EMPTY_CHILD(home && ![self isHome], @"HOME"); +} + + +- (BOOL)isWork { + return [self elementForName:@"WORK"] != nil; +} + + +- (void)setWork:(BOOL)work { + XMPP_VCARD_SET_EMPTY_CHILD(work && ![self isWork], @"WORK"); +} + + +- (BOOL)isVoice { + return [self elementForName:@"VOICE"] != nil; +} + + +- (void)setVoice:(BOOL)voice { + XMPP_VCARD_SET_EMPTY_CHILD(voice && ![self isVoice], @"VOICE"); +} + + +- (BOOL)isFax { + return [self elementForName:@"FAX"] != nil; +} + + +- (void)setFax:(BOOL)fax { + XMPP_VCARD_SET_EMPTY_CHILD(fax && ![self isFax], @"FAX"); +} + + +- (BOOL)isPager { + return [self elementForName:@"PAGER"] != nil; +} + + +- (void)setPager:(BOOL)pager { + XMPP_VCARD_SET_EMPTY_CHILD(pager && ![self isPager], @"PAGER"); +} + + +- (BOOL)hasMessaging { + return [self elementForName:@"MSG"] != nil; +} + + +- (void)setMessaging:(BOOL)msg { + XMPP_VCARD_SET_EMPTY_CHILD(msg && ![self hasMessaging], @"MSG"); +} + + +- (BOOL)isCell { + return [self elementForName:@"CELL"] != nil; +} + + +- (void)setCell:(BOOL)cell { + XMPP_VCARD_SET_EMPTY_CHILD(cell && ![self isCell], @"CELL"); +} + + +- (BOOL)isVideo { + return [self elementForName:@"VIDEO"] != nil; +} + + +- (void)setVideo:(BOOL)video { + XMPP_VCARD_SET_EMPTY_CHILD(video && ![self isVideo], @"VIDEO"); +} + + +- (BOOL)isBBS { + return [self elementForName:@"BBS"] != nil; +} + + +- (void)setBBS:(BOOL)bbs { + XMPP_VCARD_SET_EMPTY_CHILD(bbs && ![self isBBS], @"BBS"); +} + + +- (BOOL)isModem { + return [self elementForName:@"MODEM"] != nil; +} + + +- (void)setModem:(BOOL)modem { + XMPP_VCARD_SET_EMPTY_CHILD(modem && ![self isModem], @"MODEM"); +} + + +- (BOOL)isISDN { + return [self elementForName:@"ISDN"] != nil; +} + + +- (void)setISDN:(BOOL)isdn { + XMPP_VCARD_SET_EMPTY_CHILD(isdn && ![self isISDN], @"ISDN"); +} + + +- (BOOL)isPCS { + return [self elementForName:@"PCS"] != nil; +} + + +- (void)setPCS:(BOOL)pcs { + XMPP_VCARD_SET_EMPTY_CHILD(pcs && ![self isPCS], @"PCS"); +} + + +- (BOOL)isPreferred { + return [self elementForName:@"PREF"] != nil; +} + + +- (void)setPreferred:(BOOL)pref { + XMPP_VCARD_SET_EMPTY_CHILD(pref && ![self isPreferred], @"PREF"); +} + + +- (NSString *)number { + return [[self elementForName:@"NUMBER"] stringValue]; +} + + +- (void)setNumber:(NSString *)number { + XMPP_VCARD_SET_STRING_CHILD(number, @"NUMBER"); +} + + +@end diff --git a/Extensions/XEP-0059/NSXMLElement+XEP_0059.h b/Extensions/XEP-0059/NSXMLElement+XEP_0059.h new file mode 100644 index 0000000..fa8db51 --- /dev/null +++ b/Extensions/XEP-0059/NSXMLElement+XEP_0059.h @@ -0,0 +1,16 @@ +#import + +#if TARGET_OS_IPHONE + #import "DDXML.h" +#endif + +@class XMPPResultSet; + + +@interface NSXMLElement (XEP_0059) + +- (BOOL)isResultSet; +- (BOOL)hasResultSet; +- (XMPPResultSet *)resultSet; + +@end diff --git a/Extensions/XEP-0059/NSXMLElement+XEP_0059.m b/Extensions/XEP-0059/NSXMLElement+XEP_0059.m new file mode 100644 index 0000000..a48d6ee --- /dev/null +++ b/Extensions/XEP-0059/NSXMLElement+XEP_0059.m @@ -0,0 +1,47 @@ +#import "NSXMLElement+XEP_0059.h" +#import "NSXMLElement+XMPP.h" +#import "XMPPResultSet.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +#define XMLNS_XMPP_RESULT_SET @"http://jabber.org/protocol/rsm" +#define NAME_XMPP_RESULT_SET @"set" + +@implementation NSXMLElement (XEP_0059) + + +- (BOOL)isResultSet +{ + if([[self name] isEqualToString:NAME_XMPP_RESULT_SET] && [[self xmlns] isEqualToString:XMLNS_XMPP_RESULT_SET]) + { + return YES; + } + else + { + return NO; + } +} + +- (BOOL)hasResultSet +{ + if([self resultSet]) + { + return YES; + } + else + { + return NO; + } +} + +- (XMPPResultSet *)resultSet +{ + NSXMLElement *resultSetElement = [self elementForName:NAME_XMPP_RESULT_SET xmlns:XMLNS_XMPP_RESULT_SET]; + XMPPResultSet *resultSet = [XMPPResultSet resultSetFromElement:resultSetElement]; + return resultSet; +} + +@end diff --git a/Extensions/XEP-0059/XMPPResultSet.h b/Extensions/XEP-0059/XMPPResultSet.h new file mode 100644 index 0000000..c72c763 --- /dev/null +++ b/Extensions/XEP-0059/XMPPResultSet.h @@ -0,0 +1,96 @@ +#import + +#if TARGET_OS_IPHONE + #import "DDXML.h" +#endif + +/** + * The XMPPResultSet class represents an element form XEP-0059. + * It extends NSXMLElement. + * + * Calling resultSet on an NSXMLElement returns an XMPPResultSet if it exists (see NSXMLElement+XEP_0059.h) + * + * This class exists to provide developers an easy way to add functionality to Result Set processing. + * Simply add your own category to XMPPResultSet to extend it with your own custom methods. + * + * XMPPResultSet uses NSNotFound to Specify undefined integer values i.e. max,firstIndex and count. + * XMPPResultSet uses a NSString of 0 length but not nil to supply empty elements i.e. before and after. + * + * Example: + * + * To fetch the last 10 items in a result set you need the following XML: + * + * + * 10 + * + * + * + * This Result Set can be created by: + * + * [XMPPResultSet resultSetWithMax:10 firstIndex:NSNotFound after:nil before:@""]; +**/ + +@interface XMPPResultSet : NSXMLElement + +/** + * Converts an NSXMLElement to an XMPPResultSet element in place (no memory allocations or copying) + **/ ++ (XMPPResultSet *)resultSetFromElement:(NSXMLElement *)element; + +/** + * Creates and returns a new autoreleased XMPPResultSet element. +**/ ++ (XMPPResultSet *)resultSet; + ++ (XMPPResultSet *)resultSetWithMax:(NSInteger)max; + ++ (XMPPResultSet *)resultSetWithMax:(NSInteger)max + firstIndex:(NSInteger)index; + ++ (XMPPResultSet *)resultSetWithMax:(NSInteger)max + after:(NSString *)after; + ++ (XMPPResultSet *)resultSetWithMax:(NSInteger)max + before:(NSString *)before; + ++ (XMPPResultSet *)resultSetWithMax:(NSInteger)max + firstIndex:(NSInteger)firstIndex + after:(NSString *)after + before:(NSString *)before; + + +/** + * Creates and returns a new XMPPResultSet element. +**/ +- (id)init; + +- (id)initWithMax:(NSInteger)max; + +- (id)initWithMax:(NSInteger)max + firstIndex:(NSInteger)firstIndex; + +- (id)initWithMax:(NSInteger)max + after:(NSString *)after; + +- (id)initWithMax:(NSInteger)max + before:(NSString *)before; + +- (id)initWithMax:(NSInteger)max + firstIndex:(NSInteger)firstIndex + after:(NSString *)after + before:(NSString *)before; + + +- (NSInteger)max; + +- (NSInteger)firstIndex; + +- (NSString *)after; +- (NSString *)before; + +- (NSInteger)count; + +- (NSString *)first; +- (NSString *)last; + +@end diff --git a/Extensions/XEP-0059/XMPPResultSet.m b/Extensions/XEP-0059/XMPPResultSet.m new file mode 100644 index 0000000..87ceb03 --- /dev/null +++ b/Extensions/XEP-0059/XMPPResultSet.m @@ -0,0 +1,230 @@ +#import "XMPPResultSet.h" +#import "NSXMLElement+XMPP.h" + +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#define XMLNS_XMPP_RESULT_SET @"http://jabber.org/protocol/rsm" +#define NAME_XMPP_RESULT_SET @"set" + +@implementation XMPPResultSet + ++ (void)initialize +{ + // We use the object_setClass method below to dynamically change the class from a standard NSXMLElement. + // The size of the two classes is expected to be the same. + // + // If a developer adds instance methods to this class, bad things happen at runtime that are very hard to debug. + // This check is here to aid future developers who may make this mistake. + // + // For Fearless And Experienced Objective-C Developers: + // It may be possible to support adding instance variables to this class if you seriously need it. + // To do so, try realloc'ing self after altering the class, and then initialize your variables. + + size_t superSize = class_getInstanceSize([NSXMLElement class]); + size_t ourSize = class_getInstanceSize([XMPPResultSet class]); + + if (superSize != ourSize) + { + NSLog(@"Adding instance variables to XMPPResultSet is not currently supported!"); + exit(15); + } +} + ++ (XMPPResultSet *)resultSetFromElement:(NSXMLElement *)element +{ + object_setClass(element, [XMPPResultSet class]); + + return (XMPPResultSet *)element; +} + ++ (XMPPResultSet *)resultSet +{ + return [[XMPPResultSet alloc] init]; +} + ++ (XMPPResultSet *)resultSetWithMax:(NSInteger)max +{ + return [[XMPPResultSet alloc] initWithMax:max]; +} + ++ (XMPPResultSet *)resultSetWithMax:(NSInteger)max + firstIndex:(NSInteger)firstIndex +{ + return [[XMPPResultSet alloc] initWithMax:max firstIndex:firstIndex]; +} + ++ (XMPPResultSet *)resultSetWithMax:(NSInteger)max + after:(NSString *)after +{ + return [[XMPPResultSet alloc] initWithMax:max after:after]; +} + ++ (XMPPResultSet *)resultSetWithMax:(NSInteger)max + before:(NSString *)before +{ + return [[XMPPResultSet alloc] initWithMax:max before:before]; +} + ++ (XMPPResultSet *)resultSetWithMax:(NSInteger)max + firstIndex:(NSInteger)firstIndex + after:(NSString *)after + before:(NSString *)before +{ + return [[XMPPResultSet alloc] initWithMax:max + firstIndex:firstIndex + after:after + before:before]; +} + +- (id)init +{ + return [self initWithMax:NSNotFound firstIndex:NSNotFound after:nil before:nil]; +} + +- (id)initWithMax:(NSInteger)max +{ + return [self initWithMax:max firstIndex:NSNotFound after:nil before:nil]; +} + +- (id)initWithMax:(NSInteger)max + firstIndex:(NSInteger)firstIndex +{ + return [self initWithMax:max firstIndex:firstIndex after:nil before:nil]; +} + +- (id)initWithMax:(NSInteger)max + after:(NSString *)after +{ + return [self initWithMax:max firstIndex:NSNotFound after:after before:nil]; +} + +- (id)initWithMax:(NSInteger)max + before:(NSString *)before +{ + return [self initWithMax:max firstIndex:NSNotFound after:nil before:before]; +} + +- (id)initWithMax:(NSInteger)max + firstIndex:(NSInteger)firstIndex + after:(NSString *)after + before:(NSString *)before +{ + if ((self = [super initWithName:NAME_XMPP_RESULT_SET xmlns:XMLNS_XMPP_RESULT_SET])) + { + if(max != NSNotFound) + { + NSXMLElement *maxElement = [NSXMLElement elementWithName:@"max" stringValue:[@(max) stringValue]]; + [self addChild:maxElement]; + } + + if(firstIndex != NSNotFound) + { + NSXMLElement *maxElement = [NSXMLElement elementWithName:@"index" stringValue:[@(firstIndex) stringValue]]; + [self addChild:maxElement]; + } + + if(after != nil) + { + if([after length]) + { + NSXMLElement *afterElement = [NSXMLElement elementWithName:@"after" stringValue:after]; + [self addChild:afterElement]; + } + else + { + NSXMLElement *afterElement = [NSXMLElement elementWithName:@"after"]; + [self addChild:afterElement]; + } + } + + if(before != nil) + { + if([before length]) + { + NSXMLElement *beforeElement = [NSXMLElement elementWithName:@"before" stringValue:before]; + [self addChild:beforeElement]; + } + else + { + NSXMLElement *beforeElement = [NSXMLElement elementWithName:@"before"]; + [self addChild:beforeElement]; + } + } + } + + return self; +} + +- (id)initWithXMLString:(NSString *)string error:(NSError *__autoreleasing *)error +{ + if((self = [super initWithXMLString:string error:error])) + { + self = [XMPPResultSet resultSetFromElement:self]; + } + return self; +} + +- (NSInteger)max +{ + if([self elementForName:@"max"]) + { + return [[[self elementForName:@"max"] stringValue] intValue]; + }else{ + return NSNotFound; + } +} + +- (NSInteger)firstIndex +{ + if([[self elementForName:@"first"] attributeForName:@"index"]) + { + return [[self elementForName:@"first"] attributeIntegerValueForName:@"index"]; + } + else if([self elementForName:@"index"]) + { + return [[[self elementForName:@"index"] stringValue] intValue]; + } + else + { + return NSNotFound; + } +} + + +- (NSString *)after +{ + return [[self elementForName:@"after"] stringValue]; +} + +- (NSString *)before +{ + return [[self elementForName:@"before"] stringValue]; +} + +- (NSInteger)count +{ + if([self elementForName:@"count"]) + { + return [[[self elementForName:@"count"] stringValue] intValue]; + } + else + { + return NSNotFound; + } +} + +- (NSString *)first +{ + return [[self elementForName:@"first"] stringValue]; +} + +- (NSString *)last +{ + return [[self elementForName:@"last"] stringValue]; +} + +@end diff --git a/Extensions/XEP-0060/XMPPIQ+XEP_0060.h b/Extensions/XEP-0060/XMPPIQ+XEP_0060.h new file mode 100644 index 0000000..2ea6932 --- /dev/null +++ b/Extensions/XEP-0060/XMPPIQ+XEP_0060.h @@ -0,0 +1,35 @@ +#import +#import "XMPPIQ.h" + +#define XMLNS_PUBSUB @"http://jabber.org/protocol/pubsub" +#define XMLNS_PUBSUB_OWNER @"http://jabber.org/protocol/pubsub#owner" +#define XMLNS_PUBSUB_EVENT @"http://jabber.org/protocol/pubsub#event" +#define XMLNS_PUBSUB_NODE_CONFIG @"http://jabber.org/protocol/pubsub#node_config" +#define XMLNS_PUBSUB_PUBLISH_OPTIONS @"http://jabber.org/protocol/pubsub#publish-options" +#define XMLNS_PUBSUB_SUBSCRIBE_OPTIONS @"http://jabber.org/protocol/pubsub#subscribe_options" + +@interface XMPPIQ (XEP_0060) + +/** + * Extracts the 'subid' from a PubSub subscription response. + * + * For example, if we sent a PubSub subscription request for node "princely_musings", + * and the server returned this response: + * + * + * + * + * + * + * + * Then this method would return "ba49252aaa4f5d320c24d3766f0bdcade78c78d3". + * + * It is common to store the subid as it often a required attribute when unsubscribing. +**/ +- (NSString *)pubsubid; + +@end diff --git a/Extensions/XEP-0060/XMPPIQ+XEP_0060.m b/Extensions/XEP-0060/XMPPIQ+XEP_0060.m new file mode 100644 index 0000000..6d17fb1 --- /dev/null +++ b/Extensions/XEP-0060/XMPPIQ+XEP_0060.m @@ -0,0 +1,25 @@ +#import "XMPPIQ+XEP_0060.h" +#import "NSXMLElement+XMPP.h" + + +@implementation XMPPIQ (XEP_0060) + +- (NSString *)pubsubid +{ + // + // + // + // + // + + NSXMLElement *pubsub = [self elementForName:@"pubsub" xmlns:XMLNS_PUBSUB]; + NSXMLElement *subscription = [pubsub elementForName:@"subscription"]; + + return [subscription attributeStringValueForName:@"subid"]; +} + +@end diff --git a/Extensions/XEP-0060/XMPPPubSub.h b/Extensions/XEP-0060/XMPPPubSub.h new file mode 100644 index 0000000..c065825 --- /dev/null +++ b/Extensions/XEP-0060/XMPPPubSub.h @@ -0,0 +1,374 @@ +#import +#import "XMPP.h" + +#define _XMPP_PUB_SUB_H + +@interface XMPPPubSub : XMPPModule + +/** + * Returns whether or not the given message is a PubSub event message. +**/ ++ (BOOL)isPubSubMessage:(XMPPMessage *)message; + +/** + * Creates a PubSub module with the JID of the PubSub service. + * This JID will be the 'to' attribute of outgoing element(s). + * + * If you're creating a PEP module, you should pass nil as the serviceJID. + * If you're creating a normal PubSub module, you should pass the JID of the PubSub service. + * + * If you're connected to server 'domain.tld', then the PubSub JID is typically something like 'pubsub.domain.tld'. + * However, the exact format of the JID varies from server to server, and is often configurable. + * If you don't know the PubSub JID beforehand, you may need to use service discovery to find it. +**/ +- (id)initWithServiceJID:(XMPPJID *)aServiceJID; +- (id)initWithServiceJID:(XMPPJID *)aServiceJID dispatchQueue:(dispatch_queue_t)queue; + +/** + * The JID of the PubSub server the module is to communicate with. +**/ +@property (nonatomic, strong, readonly) XMPPJID *serviceJID; + +/** + * Sends a subscription request for the given node name. + * + * @param node + * + * The name of the node to subscribe to. + * This may be a leaf node or, if supported by the server, a collection node. + * + * @param myBareOrFullJid + * + * When you subscribe to a PubSub node, you can subscribe with either + * your bare jid (username@domain.tld) or your full jid (username@domain.tld/resource). + * + * If you subscribe with your bare jid, then all resources are subscribed. + * For example, if you subscribe to "/MyTown/CornerCoffeeShop" with your bare jid, + * then your "home" resource will be subscribed, as well as your "work" and "mobile" resources. + * No matter what device you sign into your account with, you'll receive pubsub updates. + * + * Contrast this with subscribing with your full jid. This subscribes you only for the current resource. + * Using the example from above, only the "home" resource would receive pubsub updates for the node. + * + * If you don't pass a JID, the method defaults to using the full JID. + * + * @param options + * + * The optional options dictionary allows you to provide the subscription options in the subscription stanza. + * This corresponds to XEP-0060, Section 6.3.7: Subscribe and Configure + * + * To use example 71 from the spec, you would pass the following dictionary: + * + * @{ @"pubsub#deliver" : @(YES), + * @"pubsub#digest" : @(NO), + * @"pubsub#include_body" : @(NO), + * @"pubsub#show-values" : @[ @"chat", @"online", @"away" ] } + * + * @return uuid + * + * The return value is the unique elementID of the IQ stanza that was sent. + * + * The server's response to the request will be reported via the appropriate delegate methods. + * + * @see xmppPubSub:didSubscribeToNode:withResult: + * @see xmppPubSub:didNotSubscribeToNode:withError: +**/ +- (NSString *)subscribeToNode:(NSString *)node; +- (NSString *)subscribeToNode:(NSString *)node withJID:(XMPPJID *)myBareOrFullJid; +- (NSString *)subscribeToNode:(NSString *)node withJID:(XMPPJID *)myBareOrFullJid options:(NSDictionary *)options; + +/** + * Sends an unsubscribe request for the given node name. + * + * @param node + * + * The name of the node to unsubscribe from. + * This should be the same node name you used when you subscribed. + * + * @param myBareOrFullJid + * + * The appropriate jid that matches the subscription. + * This should be the same jid you used when you subscribed. + * + * If you don't pass a JID, the method defaults to using the full JID. + * + * @param subid + * + * If a subscription identifier (subid) is associated with the subscription, + * the unsubscribe request may be required to include the appropriate 'subid' attribute. + * The subid value was returned by the server in the original subscribe response, + * and can also be obtained by retrieving the subscription(s) from the server. + * + * XMPPIQ+XEP_0060 has a category method named "pubsubid" which conveniently + * extracts the subid value from a subscription response. + * + * @return uuid + * + * The return value is the unique elementID of the IQ stanza that was sent. + * + * The server's response to the request will be reported via the appropriate delegate methods. + * + * @see xmppPubSub:didUnsubscribeFromNode:withResult: + * @see xmppPubSub:didNotUnsubscribeFromNode:(NSString *)node withError: +**/ +- (NSString *)unsubscribeFromNode:(NSString *)node; +- (NSString *)unsubscribeFromNode:(NSString *)node withJID:(XMPPJID *)myBareOrFullJid; +- (NSString *)unsubscribeFromNode:(NSString *)node withJID:(XMPPJID *)myBareOrFullJid subid:(NSString *)subid; + +/** + * Fetches the current PubSub subscriptions from the server. + * You can fetch all subscriptions, or just subscriptions for a particular node. + * + * Keep in mind that your PubSub subscriptions don't typically disappear when you disconnect. + * That is, your subscriptions remain intact as the client connects and disconnects. + * + * @param node + * + * Optional node name if you wish to only retrieve subscriptions for a particular node. + * + * @return uuid + * + * The return value is the unique elementID of the IQ stanza that was sent. + * + * The server's response to the request will be reported via the appropriate delegate methods. + * + * @see xmppPubSub:didRetrieveSubscriptions: + * @see xmppPubSub:didNotRetrieveSubscriptions: + * + * @see xmppPubSub:didRetrieveSubscriptions:forNode: + * @see xmppPubSub:didNotRetrieveSubscriptions:forNode: +**/ +- (NSString *)retrieveSubscriptions; +- (NSString *)retrieveSubscriptionsForNode:(NSString *)node; + +/** + * @param node + * + * The name of the subscibed node for to configure the subscription. + * This should be the same node name you used when you subscribed. + * + * @param myBareOrFullJid + * + * The appropriate jid that matches the subscription. + * This should be the same jid you used when you subscribed. + * + * If you don't pass a JID, the method defaults to using the full JID. + * + * @param subid + * + * If a subscription identifier (subid) is associated with the subscription, + * the configure request may be required to include the appropriate 'subid' attribute. + * The subid value was returned by the server in the original subscribe response, + * and can also be obtained by retrieving the subscription(s) from the server. + * + * XMPPIQ+XEP_0060 has a category method named "pubsubid" which conveniently + * extracts the subid value from a subscription response. + * + * @return uuid + * + * The return value is the unique elementID of the IQ stanza that was sent. + * + * The server's response to the request will be reported via the appropriate delegate methods. + * + * @see xmppPubSub:didConfigureSubscriptionToNode:withResult: + * @see xmppPubSub:didNotConfigureSubscriptionToNode:withError: +**/ +- (NSString *)configureSubscriptionToNode:(NSString *)node + withJID:(XMPPJID *)myBareOrFullJid + subid:(NSString *)subid + options:(NSDictionary *)options; + +/** + * Publishes the entry to the given node. + * + * If the server supports automatic node creation, and the node does not yet exist, + * the server may automatically create the node with the default configuration. + * + * @param node + * + * The name of the node to publish to. + * + * @param entry + * + * The entry you wish to publish. + * This is the xml tree that will go inside the . + * + * @param itemID + * + * This corresponds to the unique id of the published item. + * If you pass the same itemID as a previously published item, then the new entry will replace the old one. + * If you don't pass an itemID, the the server will automatically generate a unique itemID for you. + * + * @param options + * + * You may optionally pass publish options as well. + * This corresponds with section 7.1.5 of XEP-0060. + * Options are passed as a dictionary of key:value(s) pairs. + * + * For example, if you wanted to include the following publish options (from XEP-0223): + * + * + * + * http://jabber.org/protocol/pubsub#publish-options + * + * + * true + * + * + * whitelist + * + * + * + * + * Then you would simply pass the following dictionary: + * @{ @"pubsub#persist_items" : @(YES), + * @"pubsub#access_model " : @"whitelist" } + * + * @return uuid + * + * The return value is the unique elementID of the IQ stanza that was sent. + * + * The server's response to the request will be reported via the appropriate delegate methods. + * + * @see xmppPubSub:didPublishToNode:withResult: + * @see xmppPubSub:didNotPublishToNode:withError: +**/ +- (NSString *)publishToNode:(NSString *)node entry:(NSXMLElement *)entry; +- (NSString *)publishToNode:(NSString *)node entry:(NSXMLElement *)entry withItemID:(NSString *)itemId; +- (NSString *)publishToNode:(NSString *)node + entry:(NSXMLElement *)entry + withItemID:(NSString *)itemId + options:(NSDictionary *)options; + +/** + * Creates the given node with optional options. + * + * @param node + * + * The name of the node to create. + * + * @param options + * + * You may optionally pass configure options as well. + * This corresponds with section 8.1.3 of XEP-0060. + * Options are passed as a dictionary of key:value(s) pairs. + * + * @return uuid + * + * The return value is the unique elementID of the IQ stanza that was sent. + * + * The server's response to the request will be reported via the appropriate delegate methods. + * + * @see xmppPubSub:didCreateNode:withIQ: + * @see xmppPubSub:didNotCreateNode:withError: +**/ +- (NSString *)createNode:(NSString *)node; +- (NSString *)createNode:(NSString *)node withOptions:(NSDictionary *)options; + +/** + * Deletes the given node. + * + * @param node + * + * The name of the node to delete. + * This should be the same node name you used when you created it. + * + * @return uuid + * + * The return value is the unique elementID of the IQ stanza that was sent. + * + * The server's response to the request will be reported via the appropriate delegate methods. + * + * @see xmppPubSub:didDeleteNode:withIQ: + * @see xmppPubSub:didNotDeleteNode:withError: +**/ +- (NSString *)deleteNode:(NSString *)node; + +/** + * Configures the given node. + * + * @param node + * + * The name of the node to configure. + * This should be the same node name you used when you created it. + * + * @param options + * + * Options are passed as a dictionary of key:value(s) pairs. + * + * @return uuid + * + * The return value is the unique elementID of the IQ stanza that was sent. + * + * The server's response to the request will be reported via the appropriate delegate methods. + * + * @see xmppPubSub:didConfigureNode:withIQ: + * @see xmppPubSub:didNotConfigureNode:withError: +**/ +- (NSString *)configureNode:(NSString *)node withOptions:(NSDictionary *)options; + +/** + * Retrieves items from given node. + * + * @param node + * + * The name of the node to retrieve items from. + * This should be the same node name you used when you created it. + * + * @param withItemIDs + * + * This corresponds to a list of unique ids of previously published items. + * The server will return the previously published items for those that exists. + * If none of the items exists, an empty items list will be returned. + * If you don't pass any itemIDs, the server will retrieve all items on the given node + * + * @return uuid + * + * The return value is the unique elementID of the IQ stanza that was sent. + * + * The server's response to the request will be reported via the appropriate delegate methods. + * + * @see xmppPubSub:didRetrieveItems:fromNode: + * @see xmppPubSub:didNotRetrieveItems:fromNode: + **/ +- (NSString *)retrieveItemsFromNode:(NSString *)node; +- (NSString *)retrieveItemsFromNode:(NSString *)node withItemIDs:(NSArray *)itemIds; + +@end + +@protocol XMPPPubSubDelegate +@optional + +- (void)xmppPubSub:(XMPPPubSub *)sender didSubscribeToNode:(NSString *)node withResult:(XMPPIQ *)iq; +- (void)xmppPubSub:(XMPPPubSub *)sender didNotSubscribeToNode:(NSString *)node withError:(XMPPIQ *)iq; + +- (void)xmppPubSub:(XMPPPubSub *)sender didUnsubscribeFromNode:(NSString *)node withResult:(XMPPIQ *)iq; +- (void)xmppPubSub:(XMPPPubSub *)sender didNotUnsubscribeFromNode:(NSString *)node withError:(XMPPIQ *)iq; + +- (void)xmppPubSub:(XMPPPubSub *)sender didRetrieveSubscriptions:(XMPPIQ *)iq; +- (void)xmppPubSub:(XMPPPubSub *)sender didNotRetrieveSubscriptions:(XMPPIQ *)iq; + +- (void)xmppPubSub:(XMPPPubSub *)sender didRetrieveSubscriptions:(XMPPIQ *)iq forNode:(NSString *)node; +- (void)xmppPubSub:(XMPPPubSub *)sender didNotRetrieveSubscriptions:(XMPPIQ *)iq forNode:(NSString *)node; + +- (void)xmppPubSub:(XMPPPubSub *)sender didConfigureSubscriptionToNode:(NSString *)node withResult:(XMPPIQ *)iq; +- (void)xmppPubSub:(XMPPPubSub *)sender didNotConfigureSubscriptionToNode:(NSString *)node withError:(XMPPIQ *)iq; + +- (void)xmppPubSub:(XMPPPubSub *)sender didPublishToNode:(NSString *)node withResult:(XMPPIQ *)iq; +- (void)xmppPubSub:(XMPPPubSub *)sender didNotPublishToNode:(NSString *)node withError:(XMPPIQ *)iq; + +- (void)xmppPubSub:(XMPPPubSub *)sender didCreateNode:(NSString *)node withResult:(XMPPIQ *)iq; +- (void)xmppPubSub:(XMPPPubSub *)sender didNotCreateNode:(NSString *)node withError:(XMPPIQ *)iq; + +- (void)xmppPubSub:(XMPPPubSub *)sender didDeleteNode:(NSString *)node withResult:(XMPPIQ *)iq; +- (void)xmppPubSub:(XMPPPubSub *)sender didNotDeleteNode:(NSString *)node withError:(XMPPIQ *)iq; + +- (void)xmppPubSub:(XMPPPubSub *)sender didConfigureNode:(NSString *)node withResult:(XMPPIQ *)iq; +- (void)xmppPubSub:(XMPPPubSub *)sender didNotConfigureNode:(NSString *)node withError:(XMPPIQ *)iq; + +- (void)xmppPubSub:(XMPPPubSub *)sender didRetrieveItems:(XMPPIQ *)iq fromNode:(NSString *)node; +- (void)xmppPubSub:(XMPPPubSub *)sender didNotRetrieveItems:(XMPPIQ *)iq fromNode:(NSString *)node; + +- (void)xmppPubSub:(XMPPPubSub *)sender didReceiveMessage:(XMPPMessage *)message; + +@end diff --git a/Extensions/XEP-0060/XMPPPubSub.m b/Extensions/XEP-0060/XMPPPubSub.m new file mode 100644 index 0000000..01a5d35 --- /dev/null +++ b/Extensions/XEP-0060/XMPPPubSub.m @@ -0,0 +1,1053 @@ +#import "XMPPPubSub.h" +#import "XMPPIQ+XEP_0060.h" +#import "XMPPInternal.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Defined in XMPPIQ+XEP_0060.h +// +// #define XMLNS_PUBSUB @"http://jabber.org/protocol/pubsub" +// #define XMLNS_PUBSUB_OWNER @"http://jabber.org/protocol/pubsub#owner" +// #define XMLNS_PUBSUB_EVENT @"http://jabber.org/protocol/pubsub#event" +// #define XMLNS_PUBSUB_NODE_CONFIG @"http://jabber.org/protocol/pubsub#node_config" + + +@implementation XMPPPubSub +{ + XMPPJID *serviceJID; + XMPPJID *myJID; + + NSMutableDictionary *subscribeDict; + NSMutableDictionary *unsubscribeDict; + NSMutableDictionary *retrieveSubsDict; + NSMutableDictionary *configSubDict; + NSMutableDictionary *createDict; + NSMutableDictionary *deleteDict; + NSMutableDictionary *configNodeDict; + NSMutableDictionary *publishDict; + NSMutableDictionary *retrieveItemsDict; +} + ++ (BOOL)isPubSubMessage:(XMPPMessage *)message +{ + NSXMLElement *event = [message elementForName:@"event" xmlns:XMLNS_PUBSUB_EVENT]; + return (event != nil); +} + +@synthesize serviceJID; + +- (id)init +{ + // This will cause a crash - it's designed to. + // Only the init methods listed in XMPPPubSub.h are supported. + + return [self initWithServiceJID:nil dispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + // This will cause a crash - it's designed to. + // Only the init methods listed in XMPPPubSub.h are supported. + + return [self initWithServiceJID:nil dispatchQueue:NULL]; +} + +- (id)initWithServiceJID:(XMPPJID *)aServiceJID +{ + return [self initWithServiceJID:aServiceJID dispatchQueue:NULL]; +} + +- (id)initWithServiceJID:(XMPPJID *)aServiceJID dispatchQueue:(dispatch_queue_t)queue +{ + // If aServiceJID is nil, we won't include a 'to' attribute in the element(s) we send. + // This is the proper configuration for PEP, as it uses the bare JID as the pubsub node. + + if ((self = [super initWithDispatchQueue:queue])) + { + serviceJID = [aServiceJID copy]; + + subscribeDict = [[NSMutableDictionary alloc] init]; + unsubscribeDict = [[NSMutableDictionary alloc] init]; + retrieveSubsDict = [[NSMutableDictionary alloc] init]; + configSubDict = [[NSMutableDictionary alloc] init]; + createDict = [[NSMutableDictionary alloc] init]; + deleteDict = [[NSMutableDictionary alloc] init]; + configNodeDict = [[NSMutableDictionary alloc] init]; + publishDict = [[NSMutableDictionary alloc] init]; + retrieveItemsDict = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + if ([super activate:aXmppStream]) + { + if (serviceJID == nil) + { + myJID = xmppStream.myJID; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(myJIDDidChange:) + name:XMPPStreamDidChangeMyJIDNotification + object:nil]; + } + + return YES; + } + + return NO; +} + +- (void)deactivate +{ + [subscribeDict removeAllObjects]; + [unsubscribeDict removeAllObjects]; + [retrieveSubsDict removeAllObjects]; + [configSubDict removeAllObjects]; + [createDict removeAllObjects]; + [deleteDict removeAllObjects]; + [configNodeDict removeAllObjects]; + [publishDict removeAllObjects]; + [retrieveItemsDict removeAllObjects]; + + if (serviceJID == nil) { + [[NSNotificationCenter defaultCenter] removeObserver:self name:XMPPStreamDidChangeMyJIDNotification object:nil]; + } + [super deactivate]; +} + +- (void)myJIDDidChange:(NSNotification *)notification +{ + // Notifications are delivered on the thread/queue that posted them. + // In this case, they are delivered on xmppStream's internal processing queue. + + XMPPStream *stream = (XMPPStream *)[notification object]; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (xmppStream == stream) + { + myJID = xmppStream.myJID; + } + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Delegate method to receive incoming IQ stanzas. +**/ +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + // Check to see if IQ is from our PubSub/PEP service + if (serviceJID) { + if (![serviceJID isEqualToJID:[iq from]]) return NO; + } + else { + if (![myJID isEqualToJID:[iq from] options:XMPPJIDCompareBare]) return NO; + } + + NSString *elementID = [iq elementID]; + NSString *node = nil; + + if ((node = subscribeDict[elementID])) + { + // Example subscription success response: + // + // + // + // + // + // + // + // Example subscription error response: + // + // + // + // + // + // + // + // + // XEP-0060 provides many other example responses, but + // not all of them fit perfectly into the subscribed/not-subscribed categories. + // For example, the subscription could be: + // + // - pending, approval required + // - unconfigured, configuration required + // - unconfigured, configuration supported + // + // However, in the general sense, the subscription request was accepted. + // So these special cases will still be broadcast as "subscibed", + // and it is the delegates responsibility to handle these special cases if the server is configured as such. + + if ([[iq type] isEqualToString:@"result"]) + [multicastDelegate xmppPubSub:self didSubscribeToNode:node withResult:iq]; + else + [multicastDelegate xmppPubSub:self didNotSubscribeToNode:node withError:iq]; + + [subscribeDict removeObjectForKey:elementID]; + return YES; + } + else if ((node = unsubscribeDict[elementID])) + { + // Example unsubscribe success response: + // + // + // + // Example unsubscribe error response: + // + // + // + // + // + // + // + // + // XEP-0060 provides many other example responses, but + // not all of them fit perfectly into the unsubscribed/not-unsubscribed categories. + // + // For example, there's an error that gets returned if the client wasn't subscribed. + // Depending on the client, this could possibly get treated as a successful unsubscribe action. + // + // It is the delegates responsibility to handle these special cases. + + if ([[iq type] isEqualToString:@"result"]) + [multicastDelegate xmppPubSub:self didUnsubscribeFromNode:node withResult:iq]; + else + [multicastDelegate xmppPubSub:self didNotUnsubscribeFromNode:node withError:iq]; + + [unsubscribeDict removeObjectForKey:elementID]; + return YES; + } + else if ((node = retrieveSubsDict[elementID])) + { + // Example retrieve success response: + // + // + // + // + // + // + // + // + // + // + // + // + // + // Example retrieve error response: + // + // + // + // + // + // + // + + if ([[iq type] isEqualToString:@"result"]) + { + if ([node isKindOfClass:[NSNull class]]) + [multicastDelegate xmppPubSub:self didRetrieveSubscriptions:iq]; + else + [multicastDelegate xmppPubSub:self didRetrieveSubscriptions:iq forNode:node]; + } + else + { + if ([node isKindOfClass:[NSNull class]]) + [multicastDelegate xmppPubSub:self didNotRetrieveSubscriptions:iq]; + else + [multicastDelegate xmppPubSub:self didNotRetrieveSubscriptions:iq forNode:node]; + } + + [retrieveSubsDict removeObjectForKey:elementID]; + return YES; + } + else if ((node = configSubDict[elementID])) + { + // Example configure subscription success response: + // + // + // + // Example configure subscription error response: + // + // + // + // + // + // + // + + if ([[iq type] isEqualToString:@"result"]) + [multicastDelegate xmppPubSub:self didConfigureSubscriptionToNode:node withResult:iq]; + else + [multicastDelegate xmppPubSub:self didNotConfigureSubscriptionToNode:node withError:iq]; + + [configSubDict removeObjectForKey:elementID]; + return YES; + } + else if ((node = publishDict[elementID])) + { + // Example publish success response: + // + // + // + // + // + // + // + // + // + // Example publish error response: + // + // + // + // + // + // + + if ([[iq type] isEqualToString:@"result"]) + [multicastDelegate xmppPubSub:self didPublishToNode:node withResult:iq]; + else + [multicastDelegate xmppPubSub:self didNotPublishToNode:node withError:iq]; + + [publishDict removeObjectForKey:elementID]; + return YES; + } + else if ((node = createDict[elementID])) + { + // Example create success response: + // + // + // + // Example create error response: + // + // + // + // + // + // + // + + if ([[iq type] isEqualToString:@"result"]) + [multicastDelegate xmppPubSub:self didCreateNode:node withResult:iq]; + else + [multicastDelegate xmppPubSub:self didNotCreateNode:node withError:iq]; + + [createDict removeObjectForKey:elementID]; + return YES; + } + else if ((node = deleteDict[elementID])) + { + // Example delete success response: + // + // + // + // Example delete error response: + // + // + // + // + // + // + + if ([[iq type] isEqualToString:@"result"]) + [multicastDelegate xmppPubSub:self didDeleteNode:node withResult:iq]; + else + [multicastDelegate xmppPubSub:self didNotDeleteNode:node withError:iq]; + + [deleteDict removeObjectForKey:elementID]; + return YES; + } + else if ((node = configNodeDict[elementID])) + { + // Example configure node success response: + // + // + // + // Example configure node error response: + // + // + // + // + // + // + + if ([[iq type] isEqualToString:@"result"]) + [multicastDelegate xmppPubSub:self didConfigureNode:node withResult:iq]; + else + [multicastDelegate xmppPubSub:self didNotConfigureNode:node withError:iq]; + + [configNodeDict removeObjectForKey:elementID]; + return YES; + } + else if ((node = retrieveItemsDict[elementID])) + { + // Example retrieve from node success response: + // + // + // + // + // + // ... + // + // + // + // + // + // Example delete error response: + // + // + // + // + // + // + // + + if ([[iq type] isEqualToString:@"result"]) + [multicastDelegate xmppPubSub:self didRetrieveItems:iq fromNode:node]; + else + [multicastDelegate xmppPubSub:self didNotRetrieveItems:iq fromNode:node]; + + [retrieveItemsDict removeObjectForKey:elementID]; + return YES; + } + return NO; +} + +/** + * Delegate method to receive incoming message stanzas. +**/ +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message +{ + // Check to see if message is from our PubSub/PEP service + if (serviceJID) { + if (![serviceJID isEqualToJID:[message from]]) return; + } + else { + if ([myJID isEqualToJID:[message from] options:XMPPJIDCompareBare]) return; + } + + // + // + // + // + // [... entry ...] + // + // + // + // + + NSXMLElement *event = [message elementForName:@"event" xmlns:XMLNS_PUBSUB_EVENT]; + if (event) + { + [multicastDelegate xmppPubSub:self didReceiveMessage:message]; + } +} + +- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error +{ + [subscribeDict removeAllObjects]; + [unsubscribeDict removeAllObjects]; + [retrieveSubsDict removeAllObjects]; + [configSubDict removeAllObjects]; + [createDict removeAllObjects]; + [deleteDict removeAllObjects]; + [configNodeDict removeAllObjects]; + [publishDict removeAllObjects]; + [retrieveItemsDict removeAllObjects]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utility Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSXMLElement *)formForOptions:(NSDictionary *)options withFromType:(NSString *)formTypeValue +{ + // + // + // http://jabber.org/protocol/pubsub#subscribe_options + // + // 1 + // 0 + // false + // + // chat + // online + // away + // + // + + NSXMLElement *x = [NSXMLElement elementWithName:@"x" xmlns:@"jabber:x:data"]; + [x addAttributeWithName:@"type" stringValue:@"submit"]; + + NSXMLElement *formTypeField = [NSXMLElement elementWithName:@"field"]; + [formTypeField addAttributeWithName:@"var" stringValue:@"FORM_TYPE"]; + [formTypeField addAttributeWithName:@"type" stringValue:@"hidden"]; + [formTypeField addChild:[NSXMLElement elementWithName:@"value" stringValue:formTypeValue]]; + + [x addChild:formTypeField]; + + [options enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop){ + + NSAssert([key isKindOfClass:[NSString class]], @"The keys within an options dictionary must be strings"); + + NSXMLElement *field = [NSXMLElement elementWithName:@"field"]; + + NSString *var = (NSString *)key; + [field addAttributeWithName:@"var" stringValue:var]; + + if ([obj isKindOfClass:[NSArray class]]) + { + NSArray *values = (NSArray *)obj; + for (id value in values) + { + [field addChild:[NSXMLElement elementWithName:@"value" objectValue:value]]; + } + } + else + { + [field addChild:[NSXMLElement elementWithName:@"value" objectValue:obj]]; + } + + [x addChild:field]; + }]; + return x; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Subscription Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSString *)subscribeToNode:(NSString *)node +{ + return [self subscribeToNode:node withJID:nil options:nil]; +} + +- (NSString *)subscribeToNode:(NSString *)node withJID:(XMPPJID *)myBareOrFullJid +{ + return [self subscribeToNode:node withJID:myBareOrFullJid options:nil]; +} + +- (NSString *)subscribeToNode:(NSString *)aNode withJID:(XMPPJID *)myBareOrFullJid options:(NSDictionary *)options +{ + if (aNode == nil) return nil; + + // In-case aNode is mutable + NSString *node = [aNode copy]; + + // We default to using the full JID + NSString *jidStr = myBareOrFullJid ? [myBareOrFullJid full] : [xmppStream.myJID full]; + + // Generate uuid and add to dict + NSString *uuid = [xmppStream generateUUID]; + dispatch_async(moduleQueue, ^{ + subscribeDict[uuid] = node; + }); + + // Example from XEP-0060 section 6.1.1: + // + // + // + // + // + // + + NSXMLElement *subscribe = [NSXMLElement elementWithName:@"subscribe"]; + [subscribe addAttributeWithName:@"node" stringValue:node]; + [subscribe addAttributeWithName:@"jid" stringValue:jidStr]; + + NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; + [pubsub addChild:subscribe]; + + if (options) + { + // Example from XEP-0060 section 6.3.7: + // + // + // + // + // http://jabber.org/protocol/pubsub#subscribe_options + // + // 1 + // 0 + // false + // + // chat + // online + // away + // + // + // + + NSXMLElement *x = [self formForOptions:options withFromType:XMLNS_PUBSUB_SUBSCRIBE_OPTIONS]; + + NSXMLElement *optionsStanza = [NSXMLElement elementWithName:@"options"]; + [optionsStanza addChild:x]; + + [pubsub addChild:optionsStanza]; + } + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:serviceJID elementID:uuid]; + [iq addChild:pubsub]; + + [xmppStream sendElement:iq]; + return uuid; +} + +- (NSString *)unsubscribeFromNode:(NSString *)node +{ + return [self unsubscribeFromNode:node withJID:nil subid:nil]; +} + +- (NSString *)unsubscribeFromNode:(NSString *)node withJID:(XMPPJID *)myBareOrFullJid +{ + return [self unsubscribeFromNode:node withJID:myBareOrFullJid subid:nil]; +} + +- (NSString *)unsubscribeFromNode:(NSString *)aNode withJID:(XMPPJID *)myBareOrFullJid subid:(NSString *)subid +{ + if (aNode == nil) return nil; + + // In-case aNode is mutable + NSString *node = [aNode copy]; + + // We default to using the full JID + NSString *jidStr = myBareOrFullJid ? [myBareOrFullJid full] : [xmppStream.myJID full]; + + // Generate uuid and add to dict + NSString *uuid = [xmppStream generateUUID]; + dispatch_async(moduleQueue, ^{ + unsubscribeDict[uuid] = node; + }); + + // Example from XEP-0060 section 6.2.1: + // + // + // + // + // + // + + NSXMLElement *unsubscribe = [NSXMLElement elementWithName:@"unsubscribe"]; + [unsubscribe addAttributeWithName:@"node" stringValue:node]; + [unsubscribe addAttributeWithName:@"jid" stringValue:jidStr]; + if (subid) + [unsubscribe addAttributeWithName:@"subid" stringValue:subid]; + + NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; + [pubsub addChild:unsubscribe]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:serviceJID elementID:uuid]; + [iq addChild:pubsub]; + + [xmppStream sendElement:iq]; + return uuid; +} + +- (NSString *)retrieveSubscriptions +{ + return [self retrieveSubscriptionsForNode:nil]; +} + +- (NSString *)retrieveSubscriptionsForNode:(NSString *)aNode +{ + // Parameter aNode is optional + + // In-case aNode is mutable + NSString *node = [aNode copy]; + + // Generate uuid and add to dict + NSString *uuid = [xmppStream generateUUID]; + dispatch_async(moduleQueue, ^{ + if (node) + retrieveSubsDict[uuid] = node; + else + retrieveSubsDict[uuid] = [NSNull null]; + }); + + // Get subscriptions for all nodes: + // + // + // + // + // + // + // + // + // Get subscriptions for a specific node: + // + // + // + // + // + // + + NSXMLElement *subscriptions = [NSXMLElement elementWithName:@"subscriptions"]; + if (node) { + [subscriptions addAttributeWithName:@"node" stringValue:node]; + } + + NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; + [pubsub addChild:subscriptions]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:serviceJID elementID:uuid]; + [iq addChild:pubsub]; + + [xmppStream sendElement:iq]; + return uuid; +} + +- (NSString *)configureSubscriptionToNode:(NSString *)aNode + withJID:(XMPPJID *)myBareOrFullJid + subid:(NSString *)subid + options:(NSDictionary *)options +{ + if (aNode == nil) return nil; + + // In-case aNode is mutable + NSString *node = [aNode copy]; + + // We default to using the full JID + NSString *jidStr = myBareOrFullJid ? [myBareOrFullJid full] : [xmppStream.myJID full]; + + // Generate uuid and add to dict + NSString *uuid = [xmppStream generateUUID]; + dispatch_async(moduleQueue, ^{ + configSubDict[uuid] = node; + }); + + // Example from XEP-0060 section 6.3.5: + // + // + // + // + // + // + // http://jabber.org/protocol/pubsub#subscribe_options + // + // 1 + // 0 + // false + // + // chat + // online + // away + // + // + // + // + // + + NSXMLElement *optionsStanza = [NSXMLElement elementWithName:@"options"]; + [optionsStanza addAttributeWithName:@"node" stringValue:node]; + [optionsStanza addAttributeWithName:@"jid" stringValue:jidStr]; + if (subid) { + [optionsStanza addAttributeWithName:@"subid" stringValue:subid]; + } + if (options) { + NSXMLElement *x = [self formForOptions:options withFromType:XMLNS_PUBSUB_NODE_CONFIG]; + [optionsStanza addChild:x]; + } + + NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB_OWNER]; + [pubsub addChild:optionsStanza]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:serviceJID elementID:uuid]; + [iq addChild:pubsub]; + + [xmppStream sendElement:iq]; + return uuid; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Node Admin +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSString *)createNode:(NSString *)node +{ + return [self createNode:node withOptions:nil]; +} + +- (NSString *)createNode:(NSString *)aNode withOptions:(NSDictionary *)options +{ + if (aNode == nil) return nil; + + // In-case aNode is mutable + NSString *node = [aNode copy]; + + // Generate uuid and add to dict + NSString *uuid = [xmppStream generateUUID]; + dispatch_async(moduleQueue, ^{ + createDict[uuid] = node; + }); + + // + // + // + // + // + // + // http://jabber.org/protocol/pubsub#node_config + // + // Princely Musings (Atom) + // 1 + // 1 + // 1 + // 10 + // ... + // + // + // + // + + NSXMLElement *create = [NSXMLElement elementWithName:@"create"]; + [create addAttributeWithName:@"node" stringValue:node]; + + NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; + [pubsub addChild:create]; + + if (options) + { + // Example from XEP-0060 section 8.1.3 show above + + NSXMLElement *x = [self formForOptions:options withFromType:XMLNS_PUBSUB_NODE_CONFIG]; + + NSXMLElement *configure = [NSXMLElement elementWithName:@"configure"]; + [configure addChild:x]; + + [pubsub addChild:configure]; + } + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:serviceJID elementID:uuid]; + [iq addChild:pubsub]; + + [xmppStream sendElement:iq]; + return uuid; +} + +/** + * This method currently does not support redirection +**/ +- (NSString *)deleteNode:(NSString *)aNode +{ + if (aNode == nil) return nil; + + // In-case aNode is mutable + NSString *node = [aNode copy]; + + // Generate uuid and add to dict + NSString *uuid = [xmppStream generateUUID]; + dispatch_async(moduleQueue, ^{ + deleteDict[uuid] = node; + }); + + // Example XEP-0060 section 8.4.1: + // + // + // + // + // + // + + NSXMLElement *delete = [NSXMLElement elementWithName:@"delete"]; + [delete addAttributeWithName:@"node" stringValue:node]; + + NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB_OWNER]; + [pubsub addChild:delete]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:serviceJID elementID:uuid]; + [iq addChild:pubsub]; + + [xmppStream sendElement:iq]; + return uuid; +} + +- (NSString *)configureNode:(NSString *)node +{ + return [self configureNode:node withOptions:nil]; +} + +- (NSString *)configureNode:(NSString *)aNode withOptions:(NSDictionary *)options +{ + if (aNode == nil) return nil; + + // In-case aNode is mutable + NSString *node = [aNode copy]; + + // Generate uuid and add to dict + NSString *uuid = [xmppStream generateUUID]; + dispatch_async(moduleQueue, ^{ + configNodeDict[uuid] = node; + }); + + // + // + // + // + // + + NSXMLElement *configure = [NSXMLElement elementWithName:@"configure"]; + [configure addAttributeWithName:@"node" stringValue:node]; + if (options) + { + NSXMLElement *x = [self formForOptions:options withFromType:XMLNS_PUBSUB_NODE_CONFIG]; + [configure addChild:x]; + } + + NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB_OWNER]; + [pubsub addChild:configure]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:serviceJID elementID:uuid]; + [iq addChild:pubsub]; + + [xmppStream sendElement:iq]; + return uuid; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Publication methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSString *)publishToNode:(NSString *)node entry:(NSXMLElement *)entry +{ + return [self publishToNode:node entry:entry withItemID:nil options:nil]; +} + +- (NSString *)publishToNode:(NSString *)node entry:(NSXMLElement *)entry withItemID:(NSString *)itemId +{ + return [self publishToNode:node entry:entry withItemID:itemId options:nil]; +} + +- (NSString *)publishToNode:(NSString *)node + entry:(NSXMLElement *)entry + withItemID:(NSString *)itemId + options:(NSDictionary *)options +{ + if (node == nil) return nil; + if (entry == nil) return nil; + + // + // + // + // + // Some content + // + // + // + // [... FORM ... ] + // + // + // + + NSString *uuid = [xmppStream generateUUID]; + + NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; + if (itemId) + [item addAttributeWithName:@"id" stringValue:itemId]; + [item addChild:entry]; + + NSXMLElement *publish = [NSXMLElement elementWithName:@"publish"]; + [publish addAttributeWithName:@"node" stringValue:node]; + [publish addChild:item]; + + NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; + [pubsub addChild:publish]; + + if (options) + { + // Example from XEP-0060 section 7.1.5: + // + // + // + // + // http://jabber.org/protocol/pubsub#publish-options + // + // + // presence + // + // + // + + NSXMLElement *x = [self formForOptions:options withFromType:XMLNS_PUBSUB_PUBLISH_OPTIONS]; + + NSXMLElement *publishOptions = [NSXMLElement elementWithName:@"publish-options"]; + [publishOptions addChild:x]; + + [pubsub addChild:publishOptions]; + } + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:serviceJID elementID:uuid]; + [iq addChild:pubsub]; + + [xmppStream sendElement:iq]; + + dispatch_async(moduleQueue, ^{ + publishDict[uuid] = node; + }); + return uuid; +} + +- (NSString *)retrieveItemsFromNode:(NSString *)node +{ + return [self retrieveItemsFromNode:node withItemIDs:nil]; +} + +- (NSString *)retrieveItemsFromNode:(NSString *)node withItemIDs:(NSArray *)itemIds +{ + if (node == nil) return nil; + + // + //   + //     + //       + //       + //     + //   + // + + NSString *uuid = [xmppStream generateUUID]; + + NSXMLElement *items = [NSXMLElement elementWithName:@"items"]; + [items addAttributeWithName:@"node" stringValue:node]; + + if (itemIds) { + for (id itemId in itemIds) + { + NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; + [item addAttributeWithName:@"id" stringValue:itemId]; + [items addChild:item]; + } + } + + NSXMLElement *pubsub = [NSXMLElement elementWithName:@"pubsub" xmlns:XMLNS_PUBSUB]; + [pubsub addChild:items]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:serviceJID elementID:uuid]; + [iq addChild:pubsub]; + + [xmppStream sendElement:iq]; + + dispatch_async(moduleQueue, ^{ + retrieveItemsDict[uuid] = node; + }); + return uuid; +} + +@end diff --git a/Extensions/XEP-0065/TURNSocket.h b/Extensions/XEP-0065/TURNSocket.h new file mode 100644 index 0000000..fc81f5f --- /dev/null +++ b/Extensions/XEP-0065/TURNSocket.h @@ -0,0 +1,80 @@ +#import + +@class XMPPIQ; +@class XMPPJID; +@class XMPPStream; +@class GCDAsyncSocket; + +/** + * TURNSocket is an implementation of XEP-0065: SOCKS5 Bytestreams. + * + * It is used for establishing an out-of-band bytestream between any two XMPP users, + * mainly for the purpose of file transfer. +**/ +@interface TURNSocket : NSObject +{ + int state; + BOOL isClient; + + dispatch_queue_t turnQueue; + void *turnQueueTag; + + XMPPStream *xmppStream; + XMPPJID *jid; + NSString *uuid; + + id delegate; + dispatch_queue_t delegateQueue; + + dispatch_source_t turnTimer; + + NSString *discoUUID; + dispatch_source_t discoTimer; + + NSArray *proxyCandidates; + NSUInteger proxyCandidateIndex; + + NSMutableArray *candidateJIDs; + NSUInteger candidateJIDIndex; + + NSMutableArray *streamhosts; + NSUInteger streamhostIndex; + + XMPPJID *proxyJID; + NSString *proxyHost; + UInt16 proxyPort; + + GCDAsyncSocket *asyncSocket; + + NSDate *startTime, *finishTime; +} + ++ (BOOL)isNewStartTURNRequest:(XMPPIQ *)iq; + ++ (NSArray *)proxyCandidates; ++ (void)setProxyCandidates:(NSArray *)candidates; + +- (id)initWithStream:(XMPPStream *)xmppStream toJID:(XMPPJID *)jid; +- (id)initWithStream:(XMPPStream *)xmppStream incomingTURNRequest:(XMPPIQ *)iq; + +- (void)startWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)aDelegateQueue; + +- (BOOL)isClient; + +- (void)abort; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol TURNSocketDelegate +@optional + +- (void)turnSocket:(TURNSocket *)sender didSucceed:(GCDAsyncSocket *)socket; + +- (void)turnSocketDidFail:(TURNSocket *)sender; + +@end + diff --git a/Extensions/XEP-0065/TURNSocket.m b/Extensions/XEP-0065/TURNSocket.m new file mode 100644 index 0000000..121f324 --- /dev/null +++ b/Extensions/XEP-0065/TURNSocket.m @@ -0,0 +1,1535 @@ +#import "TURNSocket.h" +#import "XMPP.h" +#import "XMPPLogging.h" +#import "GCDAsyncSocket.h" +#import "NSData+XMPP.h" +#import "NSNumber+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +// Define various states +#define STATE_INIT 0 + +#define STATE_PROXY_DISCO_ITEMS 10 +#define STATE_PROXY_DISCO_INFO 11 +#define STATE_PROXY_DISCO_ADDR 12 +#define STATE_REQUEST_SENT 13 +#define STATE_INITIATOR_CONNECT 14 +#define STATE_ACTIVATE_SENT 15 +#define STATE_TARGET_CONNECT 20 +#define STATE_DONE 30 +#define STATE_FAILURE 31 + +// Define various socket tags +#define SOCKS_OPEN 101 +#define SOCKS_CONNECT 102 +#define SOCKS_CONNECT_REPLY_1 103 +#define SOCKS_CONNECT_REPLY_2 104 + +// Define various timeouts (in seconds) +#define TIMEOUT_DISCO_ITEMS 8.00 +#define TIMEOUT_DISCO_INFO 8.00 +#define TIMEOUT_DISCO_ADDR 5.00 +#define TIMEOUT_CONNECT 8.00 +#define TIMEOUT_READ 5.00 +#define TIMEOUT_TOTAL 80.00 + +// Declare private methods +@interface TURNSocket (PrivateAPI) +- (void)processDiscoItemsResponse:(XMPPIQ *)iq; +- (void)processDiscoInfoResponse:(XMPPIQ *)iq; +- (void)processDiscoAddressResponse:(XMPPIQ *)iq; +- (void)processRequestResponse:(XMPPIQ *)iq; +- (void)processActivateResponse:(XMPPIQ *)iq; +- (void)performPostInitSetup; +- (void)queryProxyCandidates; +- (void)queryNextProxyCandidate; +- (void)queryCandidateJIDs; +- (void)queryNextCandidateJID; +- (void)queryProxyAddress; +- (void)targetConnect; +- (void)targetNextConnect; +- (void)initiatorConnect; +- (void)setupDiscoTimerForDiscoItems; +- (void)setupDiscoTimerForDiscoInfo; +- (void)setupDiscoTimerForDiscoAddress; +- (void)doDiscoItemsTimeout:(NSString *)uuid; +- (void)doDiscoInfoTimeout:(NSString *)uuid; +- (void)doDiscoAddressTimeout:(NSString *)uuid; +- (void)doTotalTimeout; +- (void)succeed; +- (void)fail; +- (void)cleanup; +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation TURNSocket + +static NSMutableDictionary *existingTurnSockets; +static NSMutableArray *proxyCandidates; + +/** + * Called automatically (courtesy of Cocoa) before the first method of this class is called. + * It may also be called directly, hence the safety mechanism. +**/ ++ (void)initialize +{ + static BOOL initialized = NO; + if (!initialized) + { + initialized = YES; + + existingTurnSockets = [[NSMutableDictionary alloc] init]; + proxyCandidates = [@[@"jabber.org"] mutableCopy]; + } +} + +/** + * Returns whether or not the given IQ is a new start TURN request. + * That is, the IQ must have a query with the proper namespace, + * and it must not correspond to an existing TURNSocket. +**/ ++ (BOOL)isNewStartTURNRequest:(XMPPIQ *)iq +{ + XMPPLogTrace(); + + // An incoming turn request looks like this: + // + // + // + // + // + // + // + // + // + // + // From XEP 65 (9.1): + // The 'mode' attribute specifies the mode to use, either "tcp" or "udp". + // If this attribute is not included, the default value of "tcp" MUST be assumed. + // This attribute is OPTIONAL. + + NSXMLElement *query = [iq elementForName:@"query" xmlns:@"http://jabber.org/protocol/bytestreams"]; + if (query == nil) { + return NO; + } + + NSString *queryMode = [[query attributeForName:@"mode"] stringValue]; + + BOOL isTcpBytestreamQuery = YES; + if (queryMode) + { + isTcpBytestreamQuery = [queryMode caseInsensitiveCompare:@"tcp"] == NSOrderedSame; + } + + if (isTcpBytestreamQuery) + { + NSString *uuid = [iq elementID]; + + @synchronized(existingTurnSockets) + { + if (existingTurnSockets[uuid]) + return NO; + else + return YES; + } + } + return NO; +} + +/** + * Returns a list of proxy candidates. + * + * You may want to configure this to include NSUserDefaults stuff, or implement your own static/dynamic list. +**/ ++ (NSArray *)proxyCandidates +{ + NSArray *result = nil; + + @synchronized(proxyCandidates) + { + XMPPLogTrace(); + + result = [proxyCandidates copy]; + } + + return result; +} + ++ (void)setProxyCandidates:(NSArray *)candidates +{ + @synchronized(proxyCandidates) + { + XMPPLogTrace(); + + [proxyCandidates removeAllObjects]; + [proxyCandidates addObjectsFromArray:candidates]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Init, Dealloc +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Initializes a new TURN socket to create a TCP connection by routing through a proxy. + * This constructor configures the object to be the client connecting to a server. +**/ +- (id)initWithStream:(XMPPStream *)stream toJID:(XMPPJID *)aJid +{ + if ((self = [super init])) + { + XMPPLogTrace(); + + // Store references + xmppStream = stream; + jid = aJid; + + // Create a uuid to be used as the id for all messages in the stun communication. + // This helps differentiate various turn messages between various turn sockets. + // Relying only on JID's is troublesome, because client A could be initiating a connection to server B, + // while at the same time client B could be initiating a connection to server A. + // So an incoming connection from JID clientB@deusty.com/home would be for which turn socket? + uuid = [xmppStream generateUUID]; + + // Setup initial state for a client connection + state = STATE_INIT; + isClient = YES; + + // Get list of proxy candidates + // Each host in this list will be queried to see if it can be used as a proxy + proxyCandidates = [[self class] proxyCandidates]; + + // Configure everything else + [self performPostInitSetup]; + } + return self; +} + +/** + * Initializes a new TURN socket to create a TCP connection by routing through a proxy. + * This constructor configures the object to be the server accepting a connection from a client. +**/ +- (id)initWithStream:(XMPPStream *)stream incomingTURNRequest:(XMPPIQ *)iq +{ + if ((self = [super init])) + { + XMPPLogTrace(); + + // Store references + xmppStream = stream; + jid = [iq from]; + + // Store a copy of the ID (which will be our uuid) + uuid = [[iq elementID] copy]; + + // Setup initial state for a server connection + state = STATE_INIT; + isClient = NO; + + // Extract streamhost information from turn request + NSXMLElement *query = [iq elementForName:@"query" xmlns:@"http://jabber.org/protocol/bytestreams"]; + streamhosts = [[query elementsForName:@"streamhost"] mutableCopy]; + + // Configure everything else + [self performPostInitSetup]; + } + return self; +} + +/** + * Common initialization tasks shared by all init methods. +**/ +- (void)performPostInitSetup +{ + // Create dispatch queue. + + turnQueue = dispatch_queue_create("TURNSocket", NULL); + + turnQueueTag = &turnQueueTag; + dispatch_queue_set_specific(turnQueue, turnQueueTag, turnQueueTag, NULL); + + // We want to add this new turn socket to the list of existing sockets. + // This gives us a central repository of turn socket objects that we can easily query. + + @synchronized(existingTurnSockets) + { + existingTurnSockets[uuid] = self; + } +} + +/** + * Standard deconstructor. + * Release any objects we may have retained. + * These objects should all be defined in the header. +**/ +- (void)dealloc +{ + XMPPLogTrace(); + + if ((state > STATE_INIT) && (state < STATE_DONE)) + { + XMPPLogWarn(@"%@: Deallocating prior to completion or cancellation. " + @"You should explicitly cancel before releasing.", THIS_FILE); + } + + if (turnTimer) + dispatch_source_cancel(turnTimer); + + if (discoTimer) + dispatch_source_cancel(discoTimer); + + #if !OS_OBJECT_USE_OBJC + if (turnQueue) + dispatch_release(turnQueue); + + if (delegateQueue) + dispatch_release(delegateQueue); + + if (turnTimer) + dispatch_release(turnTimer); + + if (discoTimer) + dispatch_release(discoTimer); + #endif + + if ([asyncSocket delegate] == self) + { + [asyncSocket setDelegate:nil delegateQueue:NULL]; + [asyncSocket disconnect]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Correspondence Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Starts the TURNSocket with the given delegate. + * If the TURNSocket has already been started, this method does nothing, and the existing delegate is not changed. +**/ +- (void)startWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)aDelegateQueue +{ + NSParameterAssert(aDelegate != nil); + NSParameterAssert(aDelegateQueue != NULL); + + dispatch_async(turnQueue, ^{ @autoreleasepool { + + if (state != STATE_INIT) + { + XMPPLogWarn(@"%@: Ignoring start request. Turn procedure already started.", THIS_FILE); + return; + } + + // Set reference to delegate and delegate's queue. + // Note that we do NOT retain the delegate. + + delegate = aDelegate; + delegateQueue = aDelegateQueue; + + #if !OS_OBJECT_USE_OBJC + dispatch_retain(delegateQueue); + #endif + + // Add self as xmpp delegate so we'll get message responses + [xmppStream addDelegate:self delegateQueue:turnQueue]; + + // Start the timer to calculate how long the procedure takes + startTime = [[NSDate alloc] init]; + + // Schedule timer to cancel the turn procedure. + // This ensures that, in the event of network error or crash, + // the TURNSocket object won't remain in memory forever, and will eventually fail. + + turnTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, turnQueue); + + dispatch_source_set_event_handler(turnTimer, ^{ @autoreleasepool { + + [self doTotalTimeout]; + + }}); + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (TIMEOUT_TOTAL * NSEC_PER_SEC)); + + dispatch_source_set_timer(turnTimer, tt, DISPATCH_TIME_FOREVER, 0.1); + dispatch_resume(turnTimer); + + // Start the TURN procedure + + if (isClient) + [self queryProxyCandidates]; + else + [self targetConnect]; + + }}); +} + +/** + * Returns the type of connection + * YES for a client connection to a server, NO for a server connection from a client. +**/ +- (BOOL)isClient +{ + // Note: The isClient variable is readonly (set in the init method). + + return isClient; +} + +/** + * Aborts the TURN connection attempt. + * The status will be changed to failure, and no delegate messages will be posted. +**/ +- (void)abort +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if ((state > STATE_INIT) && (state < STATE_DONE)) + { + // The only thing we really have to do here is move the state to failure. + // This simple act should prevent any further action from being taken in this TUNRSocket object, + // since every action is dictated based on the current state. + state = STATE_FAILURE; + + // And don't forget to cleanup after ourselves + [self cleanup]; + } + }}; + + if (dispatch_get_specific(turnQueueTag)) + block(); + else + dispatch_async(turnQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Communication +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Sends the request, from initiator to target, to start a connection to one of the streamhosts. + * This method automatically updates the state. +**/ +- (void)sendRequest +{ + NSAssert(isClient, @"Only the Initiator sends the request"); + + XMPPLogTrace(); + + // + // + // + // + // + // + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"http://jabber.org/protocol/bytestreams"]; + [query addAttributeWithName:@"sid" stringValue:uuid]; + [query addAttributeWithName:@"mode" stringValue:@"tcp"]; + + NSUInteger i; + for(i = 0; i < [streamhosts count]; i++) + { + [query addChild:streamhosts[i]]; + } + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:jid elementID:uuid child:query]; + + [xmppStream sendElement:iq]; + + // Update state + state = STATE_REQUEST_SENT; +} + +/** + * Sends the reply, from target to initiator, notifying the initiator of the streamhost we connected to. +**/ +- (void)sendReply +{ + NSAssert(!isClient, @"Only the Target sends the reply"); + + XMPPLogTrace(); + + // + // + // + // + // + + NSXMLElement *streamhostUsed = [NSXMLElement elementWithName:@"streamhost-used"]; + [streamhostUsed addAttributeWithName:@"jid" stringValue:[proxyJID full]]; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"http://jabber.org/protocol/bytestreams"]; + [query addAttributeWithName:@"sid" stringValue:uuid]; + [query addChild:streamhostUsed]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"result" to:jid elementID:uuid child:query]; + + [xmppStream sendElement:iq]; +} + +/** + * Sends the activate message to the proxy after the target and initiator are both connected to the proxy. + * This method automatically updates the state. +**/ +- (void)sendActivate +{ + NSAssert(isClient, @"Only the Initiator activates the proxy"); + + XMPPLogTrace(); + + NSXMLElement *activate = [NSXMLElement elementWithName:@"activate" stringValue:[jid full]]; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"http://jabber.org/protocol/bytestreams"]; + [query addAttributeWithName:@"sid" stringValue:uuid]; + [query addChild:activate]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:proxyJID elementID:uuid child:query]; + + [xmppStream sendElement:iq]; + + // Update state + state = STATE_ACTIVATE_SENT; +} + +/** + * Sends the error, from target to initiator, notifying the initiator we were unable to connect to any streamhost. +**/ +- (void)sendError +{ + NSAssert(!isClient, @"Only the Target sends the error"); + + XMPPLogTrace(); + + // + // + // + // + // + + NSXMLElement *inf = [NSXMLElement elementWithName:@"item-not-found" xmlns:@"urn:ietf:params:xml:ns:xmpp-stanzas"]; + + NSXMLElement *error = [NSXMLElement elementWithName:@"error"]; + [error addAttributeWithName:@"code" stringValue:@"404"]; + [error addAttributeWithName:@"type" stringValue:@"cancel"]; + [error addChild:inf]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"error" to:jid elementID:uuid child:error]; + + [xmppStream sendElement:iq]; +} + +/** + * Invoked by XMPPClient when an IQ is received. + * We can determine if the IQ applies to us by checking its element ID. +**/ +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + // Disco queries (sent to jabber server) use id=discoUUID + // P2P queries (sent to other Mojo app) use id=uuid + + if (state <= STATE_PROXY_DISCO_ADDR) + { + if (![discoUUID isEqualToString:[iq elementID]]) + { + // Doesn't apply to us, or is a delayed response that we've decided to ignore + return NO; + } + } + else + { + if (![uuid isEqualToString:[iq elementID]]) + { + // Doesn't apply to us + return NO; + } + } + + XMPPLogTrace2(@"%@: %@ - state(%i)", THIS_FILE, THIS_METHOD, state); + + if (state == STATE_PROXY_DISCO_ITEMS) + { + [self processDiscoItemsResponse:iq]; + } + else if (state == STATE_PROXY_DISCO_INFO) + { + [self processDiscoInfoResponse:iq]; + } + else if (state == STATE_PROXY_DISCO_ADDR) + { + [self processDiscoAddressResponse:iq]; + } + else if (state == STATE_REQUEST_SENT) + { + [self processRequestResponse:iq]; + } + else if (state == STATE_ACTIVATE_SENT) + { + [self processActivateResponse:iq]; + } + + return YES; +} + +- (void)processDiscoItemsResponse:(XMPPIQ *)iq +{ + XMPPLogTrace(); + + // We queried the current proxy candidate for all known JIDs in it's disco list. + // + // + // + // + // + // + // + + NSXMLElement *query = [iq elementForName:@"query" xmlns:@"http://jabber.org/protocol/disco#items"]; + NSArray *items = [query elementsForName:@"item"]; + + candidateJIDs = [[NSMutableArray alloc] initWithCapacity:[items count]]; + + NSUInteger i; + for(i = 0; i < [items count]; i++) + { + NSString *itemJidStr = [[items[i] attributeForName:@"jid"] stringValue]; + XMPPJID *itemJid = [XMPPJID jidWithString:itemJidStr]; + + if(itemJid) + { + [candidateJIDs addObject:itemJid]; + } + } + + [self queryCandidateJIDs]; +} + +- (void)processDiscoInfoResponse:(XMPPIQ *)iq +{ + XMPPLogTrace(); + + // We queried a potential proxy server to see if it was indeed a proxy. + // + // + // + // + // + // + // + + NSXMLElement *query = [iq elementForName:@"query" xmlns:@"http://jabber.org/protocol/disco#info"]; + NSArray *identities = [query elementsForName:@"identity"]; + + BOOL found = NO; + + NSUInteger i; + for(i = 0; i < [identities count] && !found; i++) + { + NSXMLElement *identity = identities[i]; + + NSString *category = [[identity attributeForName:@"category"] stringValue]; + NSString *type = [[identity attributeForName:@"type"] stringValue]; + + if([category isEqualToString:@"proxy"] && [type isEqualToString:@"bytestreams"]) + { + found = YES; + } + } + + if(found) + { + // We found a proxy service! + // Now we query the proxy for its public IP and port. + [self queryProxyAddress]; + } + else + { + // There are many jabber servers out there that advertise a proxy service via JID proxy.domain.tld. + // However, not all of these servers have an entry for proxy.domain.tld in the DNS servers. + // Thus, when we try to query the proxy JID, we end up getting a 404 error because our + // jabber server was unable to connect to the given JID. + // + // We could ignore the 404 error, and try to connect anyways, + // but this would be useless because we'd be unable to activate the stream later. + + XMPPJID *candidateJID = candidateJIDs[candidateJIDIndex]; + + // So the service was not a useable proxy service, or will not allow us to use its proxy. + // + // Now most servers have serveral services such as proxy, conference, pubsub, etc. + // If we queried a JID that started with "proxy", and it said no, + // chances are that none of the other services are proxies either, + // so we might as well not waste our time querying them. + + if([[candidateJID domain] hasPrefix:@"proxy"]) + { + // Move on to the next server + [self queryNextProxyCandidate]; + } + else + { + // Try the next JID in the list from the server + [self queryNextCandidateJID]; + } + } +} + +- (void)processDiscoAddressResponse:(XMPPIQ *)iq +{ + XMPPLogTrace(); + + // We queried a proxy for its public IP and port. + // + // + // + // + // + // + + NSXMLElement *query = [iq elementForName:@"query" xmlns:@"http://jabber.org/protocol/bytestreams"]; + NSXMLElement *streamhost = [query elementForName:@"streamhost"]; + + NSString *jidStr = [[streamhost attributeForName:@"jid"] stringValue]; + XMPPJID *streamhostJID = [XMPPJID jidWithString:jidStr]; + + NSString *host = [[streamhost attributeForName:@"host"] stringValue]; + UInt16 port = [[[streamhost attributeForName:@"port"] stringValue] intValue]; + + if(streamhostJID != nil || host != nil || port > 0) + { + [streamhost detach]; + [streamhosts addObject:streamhost]; + } + + // Finished with the current proxy candidate - move on to the next + [self queryNextProxyCandidate]; +} + +- (void)processRequestResponse:(XMPPIQ *)iq +{ + XMPPLogTrace(); + + // Target has replied - hopefully they've been able to connect to one of the streamhosts + + NSXMLElement *query = [iq elementForName:@"query" xmlns:@"http://jabber.org/protocol/bytestreams"]; + NSXMLElement *streamhostUsed = [query elementForName:@"streamhost-used"]; + + NSString *streamhostUsedJID = [[streamhostUsed attributeForName:@"jid"] stringValue]; + + BOOL found = NO; + NSUInteger i; + for(i = 0; i < [streamhosts count] && !found; i++) + { + NSXMLElement *streamhost = streamhosts[i]; + + NSString *streamhostJID = [[streamhost attributeForName:@"jid"] stringValue]; + + if([streamhostJID isEqualToString:streamhostUsedJID]) + { + NSAssert(proxyJID == nil && proxyHost == nil, @"proxy and proxyHost are expected to be nil"); + + proxyJID = [XMPPJID jidWithString:streamhostJID]; + + proxyHost = [[streamhost attributeForName:@"host"] stringValue]; + if([proxyHost isEqualToString:@"0.0.0.0"]) + { + proxyHost = [proxyJID full]; + } + + proxyPort = [[[streamhost attributeForName:@"port"] stringValue] intValue]; + + found = YES; + } + } + + if(found) + { + // The target is connected to the proxy + // Now it's our turn to connect + [self initiatorConnect]; + } + else + { + // Target was unable to connect to any of the streamhosts we sent it + [self fail]; + } +} + +- (void)processActivateResponse:(XMPPIQ *)iq +{ + XMPPLogTrace(); + + NSString *type = [[iq attributeForName:@"type"] stringValue]; + + BOOL activated = NO; + if (type) + { + activated = [type caseInsensitiveCompare:@"result"] == NSOrderedSame; + } + + if (activated) { + [self succeed]; + } + else { + [self fail]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Proxy Discovery +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Each query we send during the proxy discovery process has a different element id. + * This allows us to easily use timeouts, so we can recover from offline servers, and overly slow servers. + * In other words, changing the discoUUID allows us to easily ignore delayed responses from a server. +**/ +- (void)updateDiscoUUID +{ + discoUUID = [xmppStream generateUUID]; +} + +/** + * Initiates the process of querying each item in the proxyCandidates array to determine if it supports XEP-65. + * In order to do this we have to: + * - ask the server for a list of services, which returns a list of JIDs + * - query each service JID to determine if it's a proxy + * - if it is a proxy, we ask the proxy for it's public IP and port +**/ +- (void)queryProxyCandidates +{ + XMPPLogTrace(); + + // Prepare the streamhosts array, which will hold all of our results + streamhosts = [[NSMutableArray alloc] initWithCapacity:[proxyCandidates count]]; + + // Start querying each candidate in order + proxyCandidateIndex = -1; + [self queryNextProxyCandidate]; +} + +/** + * Queries the next proxy candidate in the list. + * If we've queried every candidate, then sends the request to the target, or fails if no proxies were found. +**/ +- (void)queryNextProxyCandidate +{ + XMPPLogTrace(); + + // Update state + state = STATE_PROXY_DISCO_ITEMS; + + // We start off with multiple proxy candidates (servers that have been known to be proxy servers in the past). + // We can stop when we've found at least 2 proxies. + + XMPPJID *proxyCandidateJID = nil; + + if ([streamhosts count] < 2) + { + while ((proxyCandidateJID == nil) && (++proxyCandidateIndex < [proxyCandidates count])) + { + NSString *proxyCandidate = proxyCandidates[proxyCandidateIndex]; + proxyCandidateJID = [XMPPJID jidWithString:proxyCandidate]; + + if (proxyCandidateJID == nil) + { + XMPPLogWarn(@"%@: Invalid proxy candidate '%@', not a valid JID", THIS_FILE, proxyCandidate); + } + } + } + + if (proxyCandidateJID) + { + [self updateDiscoUUID]; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"http://jabber.org/protocol/disco#items"]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:proxyCandidateJID elementID:discoUUID child:query]; + + [xmppStream sendElement:iq]; + + [self setupDiscoTimerForDiscoItems]; + } + else + { + if ([streamhosts count] > 0) + { + // We've got a list of potential proxy servers to send to the initiator + + XMPPLogVerbose(@"%@: Streamhosts: \n%@", THIS_FILE, streamhosts); + + [self sendRequest]; + } + else + { + // We were unable to find a single proxy server from our list + + XMPPLogVerbose(@"%@: No proxies found", THIS_FILE); + + [self fail]; + } + } +} + +/** + * Initiates the process of querying each candidate JID to determine if it represents a proxy service. + * This process will be stopped when a proxy service is found, or after each candidate JID has been queried. +**/ +- (void)queryCandidateJIDs +{ + XMPPLogTrace(); + + // Most of the time, the proxy will have a domain name that includes the word "proxy". + // We can speed up the process of discovering the proxy by searching for these domains, and querying them first. + + NSUInteger i; + for (i = 0; i < [candidateJIDs count]; i++) + { + XMPPJID *candidateJID = candidateJIDs[i]; + + NSRange proxyRange = [[candidateJID domain] rangeOfString:@"proxy" options:NSCaseInsensitiveSearch]; + + if (proxyRange.length > 0) + { + [candidateJIDs removeObjectAtIndex:i]; + [candidateJIDs insertObject:candidateJID atIndex:0]; + } + } + + XMPPLogVerbose(@"%@: CandidateJIDs: \n%@", THIS_FILE, candidateJIDs); + + // Start querying each candidate in order (we can stop when we find one) + candidateJIDIndex = -1; + [self queryNextCandidateJID]; +} + +/** + * Queries the next candidate JID in the list. + * If we've queried every item, we move on to the next proxy candidate. +**/ +- (void)queryNextCandidateJID +{ + XMPPLogTrace(); + + // Update state + state = STATE_PROXY_DISCO_INFO; + + candidateJIDIndex++; + if (candidateJIDIndex < [candidateJIDs count]) + { + [self updateDiscoUUID]; + + XMPPJID *candidateJID = candidateJIDs[candidateJIDIndex]; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"http://jabber.org/protocol/disco#info"]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:candidateJID elementID:discoUUID child:query]; + + [xmppStream sendElement:iq]; + + [self setupDiscoTimerForDiscoInfo]; + } + else + { + // Ran out of candidate JIDs for the current proxy candidate. + // Time to move on to the next proxy candidate. + [self queryNextProxyCandidate]; + } +} + +/** + * Once we've discovered a proxy service, we need to query it to obtain its public IP and port. +**/ +- (void)queryProxyAddress +{ + XMPPLogTrace(); + + // Update state + state = STATE_PROXY_DISCO_ADDR; + + [self updateDiscoUUID]; + + XMPPJID *candidateJID = candidateJIDs[candidateJIDIndex]; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"http://jabber.org/protocol/bytestreams"]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:candidateJID elementID:discoUUID child:query]; + + [xmppStream sendElement:iq]; + + [self setupDiscoTimerForDiscoAddress]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Proxy Connection +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)targetConnect +{ + XMPPLogTrace(); + + // Update state + state = STATE_TARGET_CONNECT; + + // Start trying to connect to each streamhost in order + streamhostIndex = -1; + [self targetNextConnect]; +} + +- (void)targetNextConnect +{ + XMPPLogTrace(); + + streamhostIndex++; + if(streamhostIndex < [streamhosts count]) + { + NSXMLElement *streamhost = streamhosts[streamhostIndex]; + + + proxyJID = [XMPPJID jidWithString:[[streamhost attributeForName:@"jid"] stringValue]]; + + proxyHost = [[streamhost attributeForName:@"host"] stringValue]; + if([proxyHost isEqualToString:@"0.0.0.0"]) + { + proxyHost = [proxyJID full]; + } + + proxyPort = [[[streamhost attributeForName:@"port"] stringValue] intValue]; + + if (asyncSocket == nil) + { + asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:turnQueue]; + } + else + { + NSAssert([asyncSocket isDisconnected], @"Expecting the socket to be disconnected at this point..."); + } + + XMPPLogVerbose(@"TURNSocket: targetNextConnect: %@(%@:%hu)", [proxyJID full], proxyHost, proxyPort); + + NSError *err = nil; + if (![asyncSocket connectToHost:proxyHost onPort:proxyPort withTimeout:TIMEOUT_CONNECT error:&err]) + { + XMPPLogError(@"TURNSocket: targetNextConnect: err: %@", err); + [self targetNextConnect]; + } + } + else + { + [self sendError]; + [self fail]; + } +} + +- (void)initiatorConnect +{ + NSAssert(asyncSocket == nil, @"Expecting asyncSocket to be nil"); + + asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:turnQueue]; + + XMPPLogVerbose(@"TURNSocket: initiatorConnect: %@(%@:%hu)", [proxyJID full], proxyHost, proxyPort); + + NSError *err = nil; + if (![asyncSocket connectToHost:proxyHost onPort:proxyPort withTimeout:TIMEOUT_CONNECT error:&err]) + { + XMPPLogError(@"TURNSocket: initiatorConnect: err: %@", err); + [self fail]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark SOCKS +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Sends the SOCKS5 open/handshake/authentication data, and starts reading the response. + * We attempt to gain anonymous access (no authentication). +**/ +- (void)socksOpen +{ + XMPPLogTrace(); + + // +-----+-----------+---------+ + // NAME | VER | NMETHODS | METHODS | + // +-----+-----------+---------+ + // SIZE | 1 | 1 | 1 - 255 | + // +-----+-----------+---------+ + // + // Note: Size is in bytes + // + // Version = 5 (for SOCKS5) + // NumMethods = 1 + // Method = 0 (No authentication, anonymous access) + + void *byteBuffer = malloc(3); + + UInt8 ver = 5; + memcpy(byteBuffer+0, &ver, sizeof(ver)); + + UInt8 nMethods = 1; + memcpy(byteBuffer+1, &nMethods, sizeof(nMethods)); + + UInt8 method = 0; + memcpy(byteBuffer+2, &method, sizeof(method)); + + NSData *data = [NSData dataWithBytesNoCopy:byteBuffer length:3 freeWhenDone:YES]; + XMPPLogVerbose(@"TURNSocket: SOCKS_OPEN: %@", data); + + [asyncSocket writeData:data withTimeout:-1 tag:SOCKS_OPEN]; + + // +-----+--------+ + // NAME | VER | METHOD | + // +-----+--------+ + // SIZE | 1 | 1 | + // +-----+--------+ + // + // Note: Size is in bytes + // + // Version = 5 (for SOCKS5) + // Method = 0 (No authentication, anonymous access) + + [asyncSocket readDataToLength:2 withTimeout:TIMEOUT_READ tag:SOCKS_OPEN]; +} + +/** + * Sends the SOCKS5 connect data (according to XEP-65), and starts reading the response. +**/ +- (void)socksConnect +{ + XMPPLogTrace(); + + XMPPJID *myJID = [xmppStream myJID]; + + // From XEP-0065: + // + // The [address] MUST be SHA1(SID + Initiator JID + Target JID) and + // the output is hexadecimal encoded (not binary). + + XMPPJID *initiatorJID = isClient ? myJID : jid; + XMPPJID *targetJID = isClient ? jid : myJID; + + NSString *hashMe = [NSString stringWithFormat:@"%@%@%@", uuid, [initiatorJID full], [targetJID full]]; + NSData *hashRaw = [[hashMe dataUsingEncoding:NSUTF8StringEncoding] xmpp_sha1Digest]; + NSData *hash = [[hashRaw xmpp_hexStringValue] dataUsingEncoding:NSUTF8StringEncoding]; + + XMPPLogVerbose(@"TURNSocket: hashMe : %@", hashMe); + XMPPLogVerbose(@"TURNSocket: hashRaw: %@", hashRaw); + XMPPLogVerbose(@"TURNSocket: hash : %@", hash); + + // +-----+-----+-----+------+------+------+ + // NAME | VER | CMD | RSV | ATYP | ADDR | PORT | + // +-----+-----+-----+------+------+------+ + // SIZE | 1 | 1 | 1 | 1 | var | 2 | + // +-----+-----+-----+------+------+------+ + // + // Note: Size is in bytes + // + // Version = 5 (for SOCKS5) + // Command = 1 (for Connect) + // Reserved = 0 + // Address Type = 3 (1=IPv4, 3=DomainName 4=IPv6) + // Address = P:D (P=LengthOfDomain D=DomainWithoutNullTermination) + // Port = 0 + + uint byteBufferLength = (uint)(4 + 1 + [hash length] + 2); + void *byteBuffer = malloc(byteBufferLength); + + UInt8 ver = 5; + memcpy(byteBuffer+0, &ver, sizeof(ver)); + + UInt8 cmd = 1; + memcpy(byteBuffer+1, &cmd, sizeof(cmd)); + + UInt8 rsv = 0; + memcpy(byteBuffer+2, &rsv, sizeof(rsv)); + + UInt8 atyp = 3; + memcpy(byteBuffer+3, &atyp, sizeof(atyp)); + + UInt8 hashLength = [hash length]; + memcpy(byteBuffer+4, &hashLength, sizeof(hashLength)); + + memcpy(byteBuffer+5, [hash bytes], [hash length]); + + UInt16 port = 0; + memcpy(byteBuffer+5+[hash length], &port, sizeof(port)); + + NSData *data = [NSData dataWithBytesNoCopy:byteBuffer length:byteBufferLength freeWhenDone:YES]; + XMPPLogVerbose(@"TURNSocket: SOCKS_CONNECT: %@", data); + + [asyncSocket writeData:data withTimeout:-1 tag:SOCKS_CONNECT]; + + // +-----+-----+-----+------+------+------+ + // NAME | VER | REP | RSV | ATYP | ADDR | PORT | + // +-----+-----+-----+------+------+------+ + // SIZE | 1 | 1 | 1 | 1 | var | 2 | + // +-----+-----+-----+------+------+------+ + // + // Note: Size is in bytes + // + // Version = 5 (for SOCKS5) + // Reply = 0 (0=Succeeded, X=ErrorCode) + // Reserved = 0 + // Address Type = 3 (1=IPv4, 3=DomainName 4=IPv6) + // Address = P:D (P=LengthOfDomain D=DomainWithoutNullTermination) + // Port = 0 + // + // It is expected that the SOCKS server will return the same address given in the connect request. + // But according to XEP-65 this is only marked as a SHOULD and not a MUST. + // So just in case, we'll read up to the address length now, and then read in the address+port next. + + [asyncSocket readDataToLength:5 withTimeout:TIMEOUT_READ tag:SOCKS_CONNECT_REPLY_1]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark AsyncSocket Delegate Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port +{ + XMPPLogTrace(); + + // Start the SOCKS protocol stuff + [self socksOpen]; +} + +- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag +{ + XMPPLogTrace(); + + if (tag == SOCKS_OPEN) + { + // See socksOpen method for socks reply format + + UInt8 ver = [NSNumber xmpp_extractUInt8FromData:data atOffset:0]; + UInt8 mtd = [NSNumber xmpp_extractUInt8FromData:data atOffset:1]; + + XMPPLogVerbose(@"TURNSocket: SOCKS_OPEN: ver(%o) mtd(%o)", ver, mtd); + + if(ver == 5 && mtd == 0) + { + [self socksConnect]; + } + else + { + // Some kind of error occurred. + // The proxy probably requires some kind of authentication. + [asyncSocket disconnect]; + } + } + else if (tag == SOCKS_CONNECT_REPLY_1) + { + // See socksConnect method for socks reply format + + XMPPLogVerbose(@"TURNSocket: SOCKS_CONNECT_REPLY_1: %@", data); + + UInt8 ver = [NSNumber xmpp_extractUInt8FromData:data atOffset:0]; + UInt8 rep = [NSNumber xmpp_extractUInt8FromData:data atOffset:1]; + + XMPPLogVerbose(@"TURNSocket: SOCKS_CONNECT_REPLY_1: ver(%o) rep(%o)", ver, rep); + + if(ver == 5 && rep == 0) + { + // We read in 5 bytes which we expect to be: + // 0: ver = 5 + // 1: rep = 0 + // 2: rsv = 0 + // 3: atyp = 3 + // 4: size = size of addr field + // + // However, some servers don't follow the protocol, and send a atyp value of 0. + + UInt8 atyp = [NSNumber xmpp_extractUInt8FromData:data atOffset:3]; + + if (atyp == 3) + { + UInt8 addrLength = [NSNumber xmpp_extractUInt8FromData:data atOffset:4]; + UInt8 portLength = 2; + + XMPPLogVerbose(@"TURNSocket: addrLength: %o", addrLength); + XMPPLogVerbose(@"TURNSocket: portLength: %o", portLength); + + [asyncSocket readDataToLength:(addrLength+portLength) + withTimeout:TIMEOUT_READ + tag:SOCKS_CONNECT_REPLY_2]; + } + else if (atyp == 0) + { + // The size field was actually the first byte of the port field + // We just have to read in that last byte + [asyncSocket readDataToLength:1 withTimeout:TIMEOUT_READ tag:SOCKS_CONNECT_REPLY_2]; + } + else + { + XMPPLogError(@"TURNSocket: Unknown atyp field in connect reply"); + [asyncSocket disconnect]; + } + } + else + { + // Some kind of error occurred. + [asyncSocket disconnect]; + } + } + else if (tag == SOCKS_CONNECT_REPLY_2) + { + // See socksConnect method for socks reply format + + XMPPLogVerbose(@"TURNSocket: SOCKS_CONNECT_REPLY_2: %@", data); + + if (isClient) + { + [self sendActivate]; + } + else + { + [self sendReply]; + [self succeed]; + } + } +} + +- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err +{ + XMPPLogTrace2(@"%@: %@ %@", THIS_FILE, THIS_METHOD, err); + + if (state == STATE_TARGET_CONNECT) + { + [self targetNextConnect]; + } + else if (state == STATE_INITIATOR_CONNECT) + { + [self fail]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Timeouts +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)setupDiscoTimer:(NSTimeInterval)timeout +{ + NSAssert(dispatch_get_specific(turnQueueTag), @"Invoked on incorrect queue"); + + if (discoTimer == NULL) + { + discoTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, turnQueue); + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(discoTimer, tt, DISPATCH_TIME_FOREVER, 0.1); + dispatch_resume(discoTimer); + } + else + { + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(discoTimer, tt, DISPATCH_TIME_FOREVER, 0.1); + } +} + +- (void)setupDiscoTimerForDiscoItems +{ + XMPPLogTrace(); + + [self setupDiscoTimer:TIMEOUT_DISCO_ITEMS]; + + NSString *theUUID = discoUUID; + + dispatch_source_set_event_handler(discoTimer, ^{ @autoreleasepool { + + [self doDiscoItemsTimeout:theUUID]; + }}); +} + +- (void)setupDiscoTimerForDiscoInfo +{ + XMPPLogTrace(); + + [self setupDiscoTimer:TIMEOUT_DISCO_INFO]; + + NSString *theUUID = discoUUID; + + dispatch_source_set_event_handler(discoTimer, ^{ @autoreleasepool { + + [self doDiscoInfoTimeout:theUUID]; + }}); +} + +- (void)setupDiscoTimerForDiscoAddress +{ + XMPPLogTrace(); + + [self setupDiscoTimer:TIMEOUT_DISCO_ADDR]; + + NSString *theUUID = discoUUID; + + dispatch_source_set_event_handler(discoTimer, ^{ @autoreleasepool { + + [self doDiscoAddressTimeout:theUUID]; + }}); +} + +- (void)doDiscoItemsTimeout:(NSString *)theUUID +{ + NSAssert(dispatch_get_specific(turnQueueTag), @"Invoked on incorrect queue"); + + if (state == STATE_PROXY_DISCO_ITEMS) + { + if ([theUUID isEqualToString:discoUUID]) + { + XMPPLogTrace(); + + // Server isn't responding - server may be offline + [self queryNextProxyCandidate]; + } + } +} + +- (void)doDiscoInfoTimeout:(NSString *)theUUID +{ + NSAssert(dispatch_get_specific(turnQueueTag), @"Invoked on incorrect queue"); + + if (state == STATE_PROXY_DISCO_INFO) + { + if ([theUUID isEqualToString:discoUUID]) + { + XMPPLogTrace(); + + // Move on to the next proxy candidate + [self queryNextProxyCandidate]; + } + } +} + +- (void)doDiscoAddressTimeout:(NSString *)theUUID +{ + NSAssert(dispatch_get_specific(turnQueueTag), @"Invoked on incorrect queue"); + + if (state == STATE_PROXY_DISCO_ADDR) + { + if ([theUUID isEqualToString:discoUUID]) + { + XMPPLogTrace(); + + // Server is taking a long time to respond to a simple query. + // We could jump to the next candidate JID, but we'll take this as a sign of an overloaded server. + [self queryNextProxyCandidate]; + } + } +} + +- (void)doTotalTimeout +{ + NSAssert(dispatch_get_specific(turnQueueTag), @"Invoked on incorrect queue"); + + if ((state != STATE_DONE) && (state != STATE_FAILURE)) + { + XMPPLogTrace(); + + // A timeout occured to cancel the entire TURN procedure. + // This probably means the other endpoint crashed, or a network error occurred. + // In either case, we can consider this a failure, and recycle the memory associated with this object. + + [self fail]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Finish and Cleanup +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)succeed +{ + NSAssert(dispatch_get_specific(turnQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + // Record finish time + finishTime = [[NSDate alloc] init]; + + // Update state + state = STATE_DONE; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + if ([delegate respondsToSelector:@selector(turnSocket:didSucceed:)]) + { + [delegate turnSocket:self didSucceed:asyncSocket]; + } + }}); + + [self cleanup]; +} + +- (void)fail +{ + NSAssert(dispatch_get_specific(turnQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + // Record finish time + finishTime = [[NSDate alloc] init]; + + // Update state + state = STATE_FAILURE; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + if ([delegate respondsToSelector:@selector(turnSocketDidFail:)]) + { + [delegate turnSocketDidFail:self]; + } + + }}); + + [self cleanup]; +} + +- (void)cleanup +{ + // This method must be run on the turnQueue + NSAssert(dispatch_get_specific(turnQueueTag), @"Invoked on incorrect queue."); + + XMPPLogTrace(); + + if (turnTimer) + { + dispatch_source_cancel(turnTimer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(turnTimer); + #endif + turnTimer = NULL; + } + + if (discoTimer) + { + dispatch_source_cancel(discoTimer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(discoTimer); + #endif + discoTimer = NULL; + } + + // Remove self as xmpp delegate + [xmppStream removeDelegate:self delegateQueue:turnQueue]; + + // Remove self from existingStuntSockets dictionary so we can be deallocated + @synchronized(existingTurnSockets) + { + [existingTurnSockets removeObjectForKey:uuid]; + } +} + +@end diff --git a/Extensions/XEP-0066/XMPPIQ+XEP_0066.h b/Extensions/XEP-0066/XMPPIQ+XEP_0066.h new file mode 100644 index 0000000..4c110ba --- /dev/null +++ b/Extensions/XEP-0066/XMPPIQ+XEP_0066.h @@ -0,0 +1,45 @@ +#import "XMPPIQ.h" + +@interface XMPPIQ (XEP_0066) + ++ (XMPPIQ *)outOfBandDataRequestTo:(XMPPJID *)jid + elementID:(NSString *)eid + URL:(NSURL *)URL + desc:(NSString *)dec; + ++ (XMPPIQ *)outOfBandDataRequestTo:(XMPPJID *)jid + elementID:(NSString *)eid + URI:(NSString *)URI + desc:(NSString *)dec; + + +- (id)initOutOfBandDataRequestTo:(XMPPJID *)jid + elementID:(NSString *)eid + URL:(NSURL *)URL + desc:(NSString *)dec; + +- (id)initOutOfBandDataRequestTo:(XMPPJID *)jid + elementID:(NSString *)eid + URI:(NSString *)URI + desc:(NSString *)dec; + +- (void)addOutOfBandURL:(NSURL *)URL desc:(NSString *)desc; +- (void)addOutOfBandURI:(NSString *)URI desc:(NSString *)desc; + +- (XMPPIQ *)generateOutOfBandDataSuccessResponse; + +- (XMPPIQ *)generateOutOfBandDataFailureResponse; + +- (XMPPIQ *)generateOutOfBandDataRejectResponse; + +- (BOOL)isOutOfBandDataRequest; +- (BOOL)isOutOfBandDataFailureResponse; +- (BOOL)isOutOfBandDataRejectResponse; + +- (BOOL)hasOutOfBandData; + +- (NSURL *)outOfBandURL; +- (NSString *)outOfBandURI; +- (NSString *)outOfBandDesc; + +@end diff --git a/Extensions/XEP-0066/XMPPIQ+XEP_0066.m b/Extensions/XEP-0066/XMPPIQ+XEP_0066.m new file mode 100644 index 0000000..ce4e4da --- /dev/null +++ b/Extensions/XEP-0066/XMPPIQ+XEP_0066.m @@ -0,0 +1,229 @@ +#import "XMPPIQ+XEP_0066.h" +#import "NSXMLElement+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#define NAME_OUT_OF_BAND @"query" +#define XMLNS_OUT_OF_BAND @"jabber:iq:oob" + +@implementation XMPPIQ (XEP_0066) + + ++ (XMPPIQ *)outOfBandDataRequestTo:(XMPPJID *)jid + elementID:(NSString *)eid + URL:(NSURL *)URL + desc:(NSString *)desc +{ + return [[XMPPIQ alloc] initOutOfBandDataRequestTo:jid + elementID:eid + URL:URL + desc:desc]; +} + ++ (XMPPIQ *)outOfBandDataRequestTo:(XMPPJID *)jid + elementID:(NSString *)eid + URI:(NSString *)URI + desc:(NSString *)desc +{ + return [[XMPPIQ alloc] initOutOfBandDataRequestTo:jid + elementID:eid + URI:URI + desc:desc]; +} + + +- (id)initOutOfBandDataRequestTo:(XMPPJID *)jid + elementID:(NSString *)eid + URL:(NSURL *)URL + desc:(NSString *)desc +{ + if((self = [self initWithType:@"set" to:jid elementID:eid])) + { + [self addOutOfBandURL:URL desc:desc]; + } + + return self; +} + +- (id)initOutOfBandDataRequestTo:(XMPPJID *)jid + elementID:(NSString *)eid + URI:(NSString *)URI + desc:(NSString *)desc +{ + if((self = [self initWithType:@"set" to:jid elementID:eid])) + { + [self addOutOfBandURI:URI desc:desc]; + } + + return self; +} + +- (void)addOutOfBandURL:(NSURL *)URL desc:(NSString *)desc +{ + NSXMLElement *outOfBand = [NSXMLElement elementWithName:NAME_OUT_OF_BAND xmlns:XMLNS_OUT_OF_BAND]; + + if([[URL path] length]) + { + NSXMLElement *URLElement = [NSXMLElement elementWithName:@"url" stringValue:[URL path]]; + [outOfBand addChild:URLElement]; + } + + if([desc length]) + { + NSXMLElement *descElement = [NSXMLElement elementWithName:@"desc" stringValue:desc]; + [outOfBand addChild:descElement]; + } + + [self addChild:outOfBand]; +} + +- (void)addOutOfBandURI:(NSString *)URI desc:(NSString *)desc +{ + NSXMLElement *outOfBand = [NSXMLElement elementWithName:NAME_OUT_OF_BAND xmlns:XMLNS_OUT_OF_BAND]; + + if([URI length]) + { + NSXMLElement *URLElement = [NSXMLElement elementWithName:@"url" stringValue:URI]; + [outOfBand addChild:URLElement]; + } + + if([desc length]) + { + NSXMLElement *descElement = [NSXMLElement elementWithName:@"desc" stringValue:desc]; + [outOfBand addChild:descElement]; + } + + [self addChild:outOfBand]; +} + +- (XMPPIQ *)generateOutOfBandDataSuccessResponse +{ + return [XMPPIQ iqWithType:@"result" to:[self from] elementID:[self elementID]]; +} + +- (XMPPIQ *)generateOutOfBandDataFailureResponse +{ + XMPPIQ *outOfBandDataFailureResponse = [XMPPIQ iqWithType:@"error" to:[self from] elementID:[self elementID]]; + + [outOfBandDataFailureResponse addOutOfBandURI:[self outOfBandURI] desc:[self outOfBandDesc]]; + + NSXMLElement *errorElement = [NSXMLElement elementWithName:@"error"]; + [errorElement addAttributeWithName:@"code" stringValue:@"404"]; + [errorElement addAttributeWithName:@"type" stringValue:@"cancel"]; + + NSXMLElement *itemNotFoundElement = [NSXMLElement elementWithName:@"item-not-found" xmlns:@"rn:ietf:params:xml:ns:xmpp-stanzas"]; + [errorElement addChild:itemNotFoundElement]; + + [outOfBandDataFailureResponse addChild:errorElement]; + + + return outOfBandDataFailureResponse; +} + +- (XMPPIQ *)generateOutOfBandDataRejectResponse +{ + XMPPIQ *outOfBandDataRejectResponse = [XMPPIQ iqWithType:@"error" to:[self from] elementID:[self elementID]]; + + [outOfBandDataRejectResponse addOutOfBandURI:[self outOfBandURI] desc:[self outOfBandDesc]]; + + NSXMLElement *errorElement = [NSXMLElement elementWithName:@"error"]; + [errorElement addAttributeWithName:@"code" stringValue:@"406"]; + [errorElement addAttributeWithName:@"type" stringValue:@"modify"]; + + NSXMLElement *notAcceptableElement = [NSXMLElement elementWithName:@"not-acceptable" xmlns:@"rn:ietf:params:xml:ns:xmpp-stanzas"]; + [errorElement addChild:notAcceptableElement]; + + [outOfBandDataRejectResponse addChild:errorElement]; + + return outOfBandDataRejectResponse; +} + +- (BOOL)isOutOfBandDataRequest +{ + if([self hasOutOfBandData] && [self isSetIQ]) + { + return YES; + }else{ + return NO; + } +} + +- (BOOL)isOutOfBandDataFailureResponse +{ + NSXMLElement *errorElement = [self elementForName:@"error"]; + + NSUInteger errorCode = [errorElement attributeIntegerValueForName:@"code"]; + NSString *errorType = [errorElement attributeStringValueForName:@"type"]; + + if([self hasOutOfBandData] && [self isErrorIQ] && errorCode == 404 && [errorType isEqualToString:@"cancel"]) + { + return YES; + }else{ + return NO; + } +} + +- (BOOL)isOutOfBandDataRejectResponse +{ + NSXMLElement *errorElement = [self elementForName:@"error"]; + + NSUInteger errorCode = [errorElement attributeIntegerValueForName:@"code"]; + NSString *errorType = [errorElement attributeStringValueForName:@"type"]; + + if([self hasOutOfBandData] && [self isErrorIQ] && errorCode == 406 && [errorType isEqualToString:@"modify"]) + { + return YES; + }else{ + return NO; + } +} + +- (BOOL)hasOutOfBandData +{ + return ([self elementForName:NAME_OUT_OF_BAND xmlns:XMLNS_OUT_OF_BAND] ? YES : NO); +} + +- (NSURL *)outOfBandURL +{ + NSURL *URL = nil; + + NSXMLElement *outOfBand = [self elementForName:NAME_OUT_OF_BAND xmlns:XMLNS_OUT_OF_BAND]; + + NSXMLElement *URLElement = [outOfBand elementForName:@"url"]; + + NSString *URLString = [URLElement stringValue]; + + if([URLString length]) + { + URL = [NSURL URLWithString:URLString]; + } + + return URL; + +} + +- (NSString *)outOfBandURI +{ + NSXMLElement *outOfBand = [self elementForName:NAME_OUT_OF_BAND xmlns:XMLNS_OUT_OF_BAND]; + + NSXMLElement *URLElement = [outOfBand elementForName:@"url"]; + + NSString *URI= [URLElement stringValue]; + + return URI; +} + +- (NSString *)outOfBandDesc +{ + NSXMLElement *outOfBand = [self elementForName:NAME_OUT_OF_BAND xmlns:XMLNS_OUT_OF_BAND]; + + NSXMLElement *descElement = [outOfBand elementForName:@"desc"]; + + NSString *desc = [descElement stringValue]; + + return desc; +} + +@end diff --git a/Extensions/XEP-0066/XMPPMessage+XEP_0066.h b/Extensions/XEP-0066/XMPPMessage+XEP_0066.h new file mode 100644 index 0000000..7cc0ab1 --- /dev/null +++ b/Extensions/XEP-0066/XMPPMessage+XEP_0066.h @@ -0,0 +1,14 @@ +#import "XMPPMessage.h" + +@interface XMPPMessage (XEP_0066) + +- (void)addOutOfBandURL:(NSURL *)URL desc:(NSString *)desc; +- (void)addOutOfBandURI:(NSString *)URI desc:(NSString *)desc; + +- (BOOL)hasOutOfBandData; + +- (NSURL *)outOfBandURL; +- (NSString *)outOfBandURI; +- (NSString *)outOfBandDesc; + +@end diff --git a/Extensions/XEP-0066/XMPPMessage+XEP_0066.m b/Extensions/XEP-0066/XMPPMessage+XEP_0066.m new file mode 100644 index 0000000..13e78db --- /dev/null +++ b/Extensions/XEP-0066/XMPPMessage+XEP_0066.m @@ -0,0 +1,97 @@ +#import "XMPPMessage+XEP_0066.h" +#import "NSXMLElement+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#define NAME_OUT_OF_BAND @"x" +#define XMLNS_OUT_OF_BAND @"jabber:x:oob" + +@implementation XMPPMessage (XEP_0066) + +- (void)addOutOfBandURL:(NSURL *)URL desc:(NSString *)desc +{ + NSXMLElement *outOfBand = [NSXMLElement elementWithName:NAME_OUT_OF_BAND xmlns:XMLNS_OUT_OF_BAND]; + + if([[URL path] length]) + { + NSXMLElement *URLElement = [NSXMLElement elementWithName:@"url" stringValue:[URL path]]; + [outOfBand addChild:URLElement]; + } + + if([desc length]) + { + NSXMLElement *descElement = [NSXMLElement elementWithName:@"desc" stringValue:desc]; + [outOfBand addChild:descElement]; + } + + [self addChild:outOfBand]; +} + +- (void)addOutOfBandURI:(NSString *)URI desc:(NSString *)desc +{ + NSXMLElement *outOfBand = [NSXMLElement elementWithName:NAME_OUT_OF_BAND xmlns:XMLNS_OUT_OF_BAND]; + + if([URI length]) + { + NSXMLElement *URLElement = [NSXMLElement elementWithName:@"url" stringValue:URI]; + [outOfBand addChild:URLElement]; + } + + if([desc length]) + { + NSXMLElement *descElement = [NSXMLElement elementWithName:@"desc" stringValue:desc]; + [outOfBand addChild:descElement]; + } + + [self addChild:outOfBand]; +} + +- (BOOL)hasOutOfBandData +{ + return ([self elementForName:NAME_OUT_OF_BAND xmlns:XMLNS_OUT_OF_BAND] ? YES : NO); +} + +- (NSURL *)outOfBandURL +{ + NSURL *URL = nil; + + NSXMLElement *outOfBand = [self elementForName:NAME_OUT_OF_BAND xmlns:XMLNS_OUT_OF_BAND]; + + NSXMLElement *URLElement = [outOfBand elementForName:@"url"]; + + NSString *URLString = [URLElement stringValue]; + + if([URLString length]) + { + URL = [NSURL URLWithString:URLString]; + } + + return URL; + +} + +- (NSString *)outOfBandURI +{ + NSXMLElement *outOfBand = [self elementForName:NAME_OUT_OF_BAND xmlns:XMLNS_OUT_OF_BAND]; + + NSXMLElement *URLElement = [outOfBand elementForName:@"url"]; + + NSString *URI= [URLElement stringValue]; + + return URI; +} + +- (NSString *)outOfBandDesc +{ + NSXMLElement *outOfBand = [self elementForName:NAME_OUT_OF_BAND xmlns:XMLNS_OUT_OF_BAND]; + + NSXMLElement *descElement = [outOfBand elementForName:@"desc"]; + + NSString *desc = [descElement stringValue]; + + return desc; +} + +@end diff --git a/Extensions/XEP-0077/XMPPRegistration.h b/Extensions/XEP-0077/XMPPRegistration.h new file mode 100644 index 0000000..94cc5ad --- /dev/null +++ b/Extensions/XEP-0077/XMPPRegistration.h @@ -0,0 +1,94 @@ +// +// Created by Jonathon Staff on 10/11/14. +// Copyright (c) 2014 Jonathon Staff. All rights reserved. +// + +#import +#import "XMPPModule.h" + +@class XMPPIDTracker; + +#define _XMPP_REGISTRATION_H + +@interface XMPPRegistration : XMPPModule { + XMPPIDTracker *xmppIDTracker; +} + +/** +* This method will attempt to change the current user's password to the new one provided. The +* user *MUST* be authenticated for this to work successfully. +* +* @see passwordChangeSuccessful: +* @see passwordChangeFailed:withError: +* +* @param newPassword The new password for the user +*/ +- (BOOL)changePassword:(NSString *)newPassword; + +/** +* This method will attempt to cancel the current user's registration. Later implementations +* will provide support for handling authentication challenges by the server. For now, +* simply pass a value of 'nil' in for password, or preferably, use the other cancellation +* method. +* +* @see cancelRegistration +*/ +- (BOOL)cancelRegistrationUsingPassword:(NSString *)password; + +/** +* This method will attempt to cancel the current user's registration. The user *MUST* be +* already authenticated for this to work successfully. +* +* @see cancelRegistrationSuccessful: +* @see cancelRegistrationFailed:withError: +*/ +- (BOOL)cancelRegistration; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - XMPPRegistrationDelegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPRegistrationDelegate +@optional + +/** +* Implement this method when calling [regInstance changePassword:]. It will be invoked +* if the request for changing the user's password is successfully executed and receives a +* successful response. +* +* @param sender XMPPRegistration object invoking this delegate method. +*/ +- (void)passwordChangeSuccessful:(XMPPRegistration *)sender; + +/** +* Implement this method when calling [regInstance changePassword:]. It will be invoked +* if the request for changing the user's password is unsuccessfully executed or receives +* an unsuccessful response. +* +* @param sender XMPPRegistration object invoking this delegate method. +* @param error NSError containing more details of the failure. +*/ +- (void)passwordChangeFailed:(XMPPRegistration *)sender withError:(NSError *)error; + +/** +* Implement this method when calling [regInstance cancelRegistration] or a variation. It +* is invoked if the request for canceling the user's registration is successfully +* executed and receives a successful response. +* +* @param sender XMPPRegistration object invoking this delegate method. +*/ +- (void)cancelRegistrationSuccessful:(XMPPRegistration *)sender; + +/** +* Implement this method when calling [regInstance cancelRegistration] or a variation. It +* is invoked if the request for canceling the user's registration is unsuccessfully +* executed or receives an unsuccessful response. +* +* @param sender XMPPRegistration object invoking this delegate method. +* @param error NSError containing more details of the failure. +*/ +- (void)cancelRegistrationFailed:(XMPPRegistration *)sender withError:(NSError *)error; + +@end diff --git a/Extensions/XEP-0077/XMPPRegistration.m b/Extensions/XEP-0077/XMPPRegistration.m new file mode 100644 index 0000000..b1d3bce --- /dev/null +++ b/Extensions/XEP-0077/XMPPRegistration.m @@ -0,0 +1,237 @@ +// +// Created by Jonathon Staff on 10/11/14. +// Copyright (c) 2014 Jonathon Staff. All rights reserved. +// + +#import "XMPPRegistration.h" +#import "XMPPStream.h" +#import "XMPPIDTracker.h" +#import "XMPPIQ.h" +#import "NSXMLElement+XMPP.h" + +NSString *const XMPPRegistrationErrorDomain = @"XMPPRegistrationErrorDomain"; + +@implementation XMPPRegistration + +- (void)didActivate +{ + xmppIDTracker = [[XMPPIDTracker alloc] initWithStream:xmppStream dispatchQueue:moduleQueue]; +} + +- (void)willDeactivate +{ + [xmppIDTracker removeAllIDs]; + xmppIDTracker = nil; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Public API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** +* This method provides functionality of XEP-0077 3.3 User Changes Password. +* +* @link {http://xmpp.org/extensions/xep-0077.html#usecases-changepw} +* +* Example 18. Password Change +* +* +* +* bill +* newpass +* +* +* +*/ +- (BOOL)changePassword:(NSString *)newPassword +{ + if (![xmppStream isAuthenticated]) + return NO; // You must be authenticated in order to change your password + + dispatch_block_t block = ^{ + @autoreleasepool { + NSString *toStr = xmppStream.myJID.domain; + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:register"]; + + NSXMLElement *username = [NSXMLElement elementWithName:@"username" + stringValue:xmppStream.myJID.user]; + NSXMLElement *password = [NSXMLElement elementWithName:@"password" + stringValue:newPassword]; + [query addChild:username]; + [query addChild:password]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" + to:[XMPPJID jidWithString:toStr] + elementID:[xmppStream generateUUID] + child:query]; + + [xmppIDTracker addID:[iq elementID] + target:self + selector:@selector(handlePasswordChangeQueryIQ:withInfo:) + timeout:60]; + + [xmppStream sendElement:iq]; + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); + + return YES; +} + +/** +* This method provides functionality of XEP-0077 3.2 Entity Cancels an Existing Registration. +* +* @link {http://xmpp.org/extensions/xep-0077.html#usecases-cancel} +* +* +* +* +* +* +* +*/ +- (BOOL)cancelRegistration +{ + return [self cancelRegistrationUsingPassword:nil]; +} + +/** +* Same as cancelRegistration. Handling authentication challenges is not yet implemented. +*/ +- (BOOL)cancelRegistrationUsingPassword:(NSString *)password +{ + // TODO: Handle the scenario of using password + + dispatch_block_t block = ^{ + @autoreleasepool { + + NSXMLElement *remove = [NSXMLElement elementWithName:@"remove"]; + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:register"]; + [query addChild:remove]; + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" + elementID:[xmppStream generateUUID] + child:query]; + + [xmppIDTracker addElement:iq + target:self + selector:@selector(handleRegistrationCancelQueryIQ:withInfo:) + timeout:60]; + + [xmppStream sendElement:iq]; + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); + + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - XMPPIDTracker +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** +* This method handles the response received (or not received) after calling changePassword. +*/ +- (void)handlePasswordChangeQueryIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)info +{ + dispatch_block_t block = ^{ + @autoreleasepool { + NSXMLElement *errorElem = [iq elementForName:@"error"]; + + if (errorElem) { + NSString *errMsg = [[errorElem children] componentsJoinedByString:@", "]; + NSInteger errCode = [errorElem attributeIntegerValueForName:@"code" + withDefaultValue:-1]; + NSDictionary *errInfo = @{NSLocalizedDescriptionKey : errMsg}; + NSError *err = [NSError errorWithDomain:XMPPRegistrationErrorDomain + code:errCode + userInfo:errInfo]; + + [multicastDelegate passwordChangeFailed:self + withError:err]; + return; + } + + NSString *type = [iq type]; + + if ([type isEqualToString:@"result"]) { + [multicastDelegate passwordChangeSuccessful:self]; + } else { + // this should be impossible to reach, but just for safety's sake... + [multicastDelegate passwordChangeFailed:self + withError:nil]; + } + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +/** +* This method handles the response received (or not received) after calling cancelRegistration. +*/ +- (void)handleRegistrationCancelQueryIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)info +{ + dispatch_block_t block = ^{ + @autoreleasepool { + NSXMLElement *errorElem = [iq elementForName:@"error"]; + + if (errorElem) { + NSString *errMsg = [[errorElem children] componentsJoinedByString:@", "]; + NSInteger errCode = [errorElem attributeIntegerValueForName:@"code" + withDefaultValue:-1]; + NSDictionary *errInfo = @{NSLocalizedDescriptionKey : errMsg}; + NSError *err = [NSError errorWithDomain:XMPPRegistrationErrorDomain + code:errCode + userInfo:errInfo]; + + [multicastDelegate cancelRegistrationFailed:self + withError:err]; + return; + } + + NSString *type = [iq type]; + + if ([type isEqualToString:@"result"]) { + [multicastDelegate cancelRegistrationSuccessful:self]; + } else { + // this should be impossible to reach, but just for safety's sake... + [multicastDelegate cancelRegistrationFailed:self + withError:nil]; + } + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - XMPPStreamDelegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)xmppStream:(XMPPStream *)stream didReceiveIQ:(XMPPIQ *)iq +{ + NSString *type = [iq type]; + + if ([type isEqualToString:@"result"] || [type isEqualToString:@"error"]) { + return [xmppIDTracker invokeForElement:iq withObject:iq]; + } + + return NO; +} + +@end diff --git a/Extensions/XEP-0082/NSDate+XMPPDateTimeProfiles.h b/Extensions/XEP-0082/NSDate+XMPPDateTimeProfiles.h new file mode 100644 index 0000000..bc142d5 --- /dev/null +++ b/Extensions/XEP-0082/NSDate+XMPPDateTimeProfiles.h @@ -0,0 +1,26 @@ +// +// NSDate+XMPPDateTimeProfiles.h +// +// NSDate category to implement XEP-0082. +// +// Created by Eric Chamberlain on 3/9/11. +// Copyright 2011 RF.com. All rights reserved. +// Copyright 2010 Martin Morrison. All rights reserved. +// + +#import + +@interface NSDate(XMPPDateTimeProfiles) + + ++ (NSDate *)dateWithXmppDateString:(NSString *)str; ++ (NSDate *)dateWithXmppTimeString:(NSString *)str; ++ (NSDate *)dateWithXmppDateTimeString:(NSString *)str; + + +- (NSString *)xmppDateString; +- (NSString *)xmppTimeString; +- (NSString *)xmppDateTimeString; + + +@end diff --git a/Extensions/XEP-0082/NSDate+XMPPDateTimeProfiles.m b/Extensions/XEP-0082/NSDate+XMPPDateTimeProfiles.m new file mode 100644 index 0000000..2e9e67d --- /dev/null +++ b/Extensions/XEP-0082/NSDate+XMPPDateTimeProfiles.m @@ -0,0 +1,80 @@ +// +// NSDate+XMPPDateTimeProfiles.m +// +// NSDate category to implement XEP-0082. +// +// Created by Eric Chamberlain on 3/9/11. +// Copyright 2011 RF.com. All rights reserved. +// Copyright 2010 Martin Morrison. All rights reserved. +// + +#import "NSDate+XMPPDateTimeProfiles.h" +#import "XMPPDateTimeProfiles.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +@interface NSDate(XMPPDateTimeProfilesPrivate) +- (NSString *)xmppStringWithDateFormat:(NSString *)dateFormat; +@end + +#pragma mark - + +@implementation NSDate(XMPPDateTimeProfiles) + + +#pragma mark Convert from XMPP string to NSDate + + ++ (NSDate *)dateWithXmppDateString:(NSString *)str { + return [XMPPDateTimeProfiles parseDate:str]; +} + + ++ (NSDate *)dateWithXmppTimeString:(NSString *)str { + return [XMPPDateTimeProfiles parseTime:str]; +} + + ++ (NSDate *)dateWithXmppDateTimeString:(NSString *)str { + return [XMPPDateTimeProfiles parseDateTime:str]; +} + + +#pragma mark Convert from NSDate to XMPP string + + +- (NSString *)xmppDateString { + return [self xmppStringWithDateFormat:@"yyyy-MM-dd"]; +} + + +- (NSString *)xmppTimeString { + return [self xmppStringWithDateFormat:@"HH:mm:ss'Z'"]; +} + + +- (NSString *)xmppDateTimeString { + return [self xmppStringWithDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"]; +} + + +#pragma mark XMPPDateTimeProfilesPrivate methods + + +- (NSString *)xmppStringWithDateFormat:(NSString *)dateFormat +{ + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; + [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]; + [dateFormatter setDateFormat:dateFormat]; + [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + + NSString *str = [dateFormatter stringFromDate:self]; + + return str; +} + + +@end diff --git a/Extensions/XEP-0082/XMPPDateTimeProfiles.h b/Extensions/XEP-0082/XMPPDateTimeProfiles.h new file mode 100644 index 0000000..5a717e0 --- /dev/null +++ b/Extensions/XEP-0082/XMPPDateTimeProfiles.h @@ -0,0 +1,17 @@ +#import +#import "NSDate+XMPPDateTimeProfiles.h" + +@interface XMPPDateTimeProfiles : NSObject + +/** + * The following methods attempt to parse the given string following XEP-0082. + * They return nil if the given string doesn't follow the spec. +**/ + ++ (NSDate *)parseDate:(NSString *)dateStr; ++ (NSDate *)parseTime:(NSString *)timeStr; ++ (NSDate *)parseDateTime:(NSString *)dateTimeStr; + ++ (NSTimeZone *)parseTimeZoneOffset:(NSString *)tzo; + +@end diff --git a/Extensions/XEP-0082/XMPPDateTimeProfiles.m b/Extensions/XEP-0082/XMPPDateTimeProfiles.m new file mode 100644 index 0000000..e0e444a --- /dev/null +++ b/Extensions/XEP-0082/XMPPDateTimeProfiles.m @@ -0,0 +1,295 @@ +#import "XMPPDateTimeProfiles.h" +#import "NSNumber+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +static NSString * const kXEP0082SharedDateFormatterKey = @"xep0082_shared_date_formatter_key"; + +@interface XMPPDateTimeProfiles (PrivateAPI) ++ (NSDate *)parseDateTime:(NSString *)dateTimeStr withMandatoryTimeZone:(BOOL)mandatoryTZ; +@end + + +@implementation XMPPDateTimeProfiles + +/** + * The following acronyms and characters are used from XEP-0082 to represent time-related concepts: + * + * CCYY four-digit year portion of Date + * MM two-digit month portion of Date + * DD two-digit day portion of Date + * - ISO 8601 separator among Date portions + * T ISO 8601 separator between Date and Time + * hh two-digit hour portion of Time (00 through 23) + * mm two-digit minutes portion of Time (00 through 59) + * ss two-digit seconds portion of Time (00 through 59) + * : ISO 8601 separator among Time portions + * . ISO 8601 separator between seconds and milliseconds + * sss fractional second addendum to Time (MAY contain any number of digits) + * TZD Time Zone Definition (either "Z" for UTC or "(+|-)hh:mm" for a specific time zone) + * +**/ + ++ (NSDate *)parseDate:(NSString *)dateStr +{ + if ([dateStr length] < 10) return nil; + + // The Date profile defines a date without including the time of day. + // The lexical representation is as follows: + // + // CCYY-MM-DD + // + // Example: + // + // 1776-07-04 + + NSDateFormatter *df = [self threadDateFormatter]; + [df setFormatterBehavior:NSDateFormatterBehavior10_4]; // Use unicode patterns (as opposed to 10_3) + [df setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]; + [df setDateFormat:@"yyyy-MM-dd"]; + + NSDate *result = [df dateFromString:dateStr]; + + return result; +} + ++ (NSDate *)parseTime:(NSString *)timeStr +{ + // The Time profile is used to specify an instant of time that recurs (e.g., every day). + // The lexical representation is as follows: + // + // hh:mm:ss[.sss][TZD] + // + // The Time Zone Definition is optional; if included, it MUST be either UTC (denoted by addition + // of the character 'Z' to the end of the string) or some offset from UTC (denoted by addition + // of '[+|-]' and 'hh:mm' to the end of the string). + // + // Examples: + // + // 16:00:00 + // 16:00:00Z + // 16:00:00+07:00 + // 16:00:00.123 + // 16:00:00.123Z + // 16:00:00.123+07:00 + + + // Extract the current day so the result can be on the current day. + // Why do we bother doing this? + // + // First, it is rather intuitive. + // Second, if we don't we risk being on a date with a conflicting DST (daylight saving time). + // + // For example, -0800 instead of the current -0700. + // This can be rather confusing when printing the result. + + NSDateFormatter *df = [self threadDateFormatter]; + [df setFormatterBehavior:NSDateFormatterBehavior10_4]; // Use unicode patterns (as opposed to 10_3) + [df setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]; + [df setDateFormat:@"yyyy-MM-dd"]; + + NSString *today = [df stringFromDate:[NSDate date]]; + + NSString *dateTimeStr = [NSString stringWithFormat:@"%@T%@", today, timeStr]; + + return [self parseDateTime:dateTimeStr withMandatoryTimeZone:NO]; +} + ++ (NSDate *)parseDateTime:(NSString *)dateTimeStr +{ + // The DateTime profile is used to specify a non-recurring moment in time to an accuracy of seconds (or, + // optionally, fractions of a second). The format is as follows: + // + // CCYY-MM-DDThh:mm:ss[.sss]TZD + // + // The Time Zone Definition is mandatory and MUST be either UTC (denoted by addition of the character 'Z' + // to the end of the string) or some offset from UTC (denoted by addition of '[+|-]' and 'hh:mm' to the + // end of the string). + // + // Examples: + // + // 1969-07-21T02:56:15Z + // 1969-07-20T21:56:15-05:00 + // 1969-07-21T02:56:15.123Z + // 1969-07-20T21:56:15.123-05:00 + + return [self parseDateTime:dateTimeStr withMandatoryTimeZone:YES]; +} + ++ (NSDate *)parseDateTime:(NSString *)dateTimeStr withMandatoryTimeZone:(BOOL)mandatoryTZ +{ + if ([dateTimeStr length] < 19) return nil; + + // The DateTime profile is used to specify a non-recurring moment in time to an accuracy of seconds (or, + // optionally, fractions of a second). The format is as follows: + // + // CCYY-MM-DDThh:mm:ss[.sss]{TZD} + // + // Examples: + // + // 1969-07-21T02:56:15 + // 1969-07-21T02:56:15Z + // 1969-07-20T21:56:15-05:00 + // 1969-07-21T02:56:15.123 + // 1969-07-21T02:56:15.123Z + // 1969-07-20T21:56:15.123-05:00 + + BOOL hasMilliseconds = NO; + BOOL hasTimeZoneInfo = NO; + BOOL hasTimeZoneOffset = NO; + NSInteger fractionalDigits = 0; + + if ([dateTimeStr length] > 19) + { + unichar c = [dateTimeStr characterAtIndex:19]; + + // Check for optional milliseconds + if (c == '.') + { + hasMilliseconds = YES; + + // At least one fractional digit? + if ([dateTimeStr length] < 21) return nil; + } + + // Check for optional time zone info, which is at the last char (Z), or the + // char 6 chars from the end + if ([dateTimeStr characterAtIndex:[dateTimeStr length] - 1] == 'Z') + { + hasTimeZoneInfo = YES; + hasTimeZoneOffset = NO; + if (hasMilliseconds) + { + // 1969-07-21T02:56:15.1234Z -> 25 - 1 - 20 = 4 + fractionalDigits = [dateTimeStr length] - 1 - 20; + } + } + else + { + c = [dateTimeStr characterAtIndex:[dateTimeStr length] - 6]; + if (c == '+' || c == '-') + { + hasTimeZoneInfo = YES; + hasTimeZoneOffset = YES; + if (hasMilliseconds) + { + // 1969-07-21T02:56:15.1234+00:00 -> 30 - 6 - 20 = 4 + fractionalDigits = [dateTimeStr length] - 6 - 20; + } + } + else if (hasMilliseconds) + { + // 1969-07-21T02:56:15.1234 -> 24 - 20 = 4 + fractionalDigits = [dateTimeStr length] - 20; + } + } + } + + if (mandatoryTZ && !hasTimeZoneInfo) return nil; + + NSDateFormatter *df = [self threadDateFormatter]; + [df setFormatterBehavior:NSDateFormatterBehavior10_4]; // Use unicode patterns (as opposed to 10_3) + [df setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]; + [df setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss"]; + + NSDate *result = nil; + NSString *dateAndTime = [dateTimeStr substringToIndex:19]; + NSString *fraction = fractionalDigits != 0 ? [NSString stringWithFormat:@"0%@", [dateTimeStr substringWithRange:NSMakeRange(19, fractionalDigits + 1)]] : nil; + + if (hasTimeZoneInfo && !hasTimeZoneOffset) + { + [df setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + result = [df dateFromString:dateAndTime]; + } + else if (hasTimeZoneInfo && hasTimeZoneOffset) + { + NSString *timeZone = [dateTimeStr substringFromIndex:[dateTimeStr length] - 6]; + NSTimeZone *tz = [self parseTimeZoneOffset:timeZone]; + if (tz == nil) + { + result = nil; + } + else + { + [df setTimeZone:tz]; + result = [df dateFromString:dateAndTime]; + } + } + else + { + result = [df dateFromString:dateAndTime]; + } + + if (result && fraction) + { + static NSNumberFormatter *numberFormatter = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + numberFormatter = [[NSNumberFormatter alloc] init]; + [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; + [numberFormatter setDecimalSeparator:@"."]; + }); + + NSTimeInterval fractionInterval = [[numberFormatter numberFromString:fraction] doubleValue]; + NSTimeInterval current = [result timeIntervalSinceReferenceDate]; + result = [NSDate dateWithTimeIntervalSinceReferenceDate:floor(current) + fractionInterval]; + } + + return result; +} + ++ (NSTimeZone *)parseTimeZoneOffset:(NSString *)tzo +{ + // The tzo value is supposed to start with '+' or '-'. + // Spec says: (+-)hh:mm + // + // hh : two-digit hour portion (00 through 23) + // mm : two-digit minutes portion (00 through 59) + + if ([tzo length] != 6) + { + return nil; + } + + NSString *hoursStr = [tzo substringWithRange:NSMakeRange(1, 2)]; + NSString *minutesStr = [tzo substringWithRange:NSMakeRange(4, 2)]; + + NSUInteger hours; + if (![NSNumber xmpp_parseString:hoursStr intoNSUInteger:&hours]) + return nil; + + NSUInteger minutes; + if (![NSNumber xmpp_parseString:minutesStr intoNSUInteger:&minutes]) + return nil; + + if (hours > 23) return nil; + if (minutes > 59) return nil; + + NSInteger secondsOffset = (NSInteger)((hours * 60 * 60) + (minutes * 60)); + + if ([tzo hasPrefix:@"-"]) + { + secondsOffset = -1 * secondsOffset; + } + else if (![tzo hasPrefix:@"+"]) + { + return nil; + } + + return [NSTimeZone timeZoneForSecondsFromGMT:secondsOffset]; +} + ++ (NSDateFormatter *)threadDateFormatter { + NSMutableDictionary *currentThreadStorage = [[NSThread currentThread] threadDictionary]; + NSDateFormatter *sharedDateFormatter = currentThreadStorage[kXEP0082SharedDateFormatterKey]; + if (!sharedDateFormatter) { + sharedDateFormatter = [NSDateFormatter new]; + currentThreadStorage[kXEP0082SharedDateFormatterKey] = sharedDateFormatter; + } + + return sharedDateFormatter; +} + +@end diff --git a/Extensions/XEP-0085/XMPPMessage+XEP_0085.h b/Extensions/XEP-0085/XMPPMessage+XEP_0085.h new file mode 100644 index 0000000..481c395 --- /dev/null +++ b/Extensions/XEP-0085/XMPPMessage+XEP_0085.h @@ -0,0 +1,23 @@ +#import +#import "XMPPMessage.h" + + +@interface XMPPMessage (XEP_0085) + +- (NSString *)chatState; + +- (BOOL)hasChatState; + +- (BOOL)hasActiveChatState; +- (BOOL)hasComposingChatState; +- (BOOL)hasPausedChatState; +- (BOOL)hasInactiveChatState; +- (BOOL)hasGoneChatState; + +- (void)addActiveChatState; +- (void)addComposingChatState; +- (void)addPausedChatState; +- (void)addInactiveChatState; +- (void)addGoneChatState; + +@end diff --git a/Extensions/XEP-0085/XMPPMessage+XEP_0085.m b/Extensions/XEP-0085/XMPPMessage+XEP_0085.m new file mode 100644 index 0000000..d44dbc4 --- /dev/null +++ b/Extensions/XEP-0085/XMPPMessage+XEP_0085.m @@ -0,0 +1,69 @@ +#import "XMPPMessage+XEP_0085.h" +#import "NSXMLElement+XMPP.h" + + +static NSString *const xmlns_chatstates = @"http://jabber.org/protocol/chatstates"; + +@implementation XMPPMessage (XEP_0085) + +- (NSString *)chatState{ + return [[[self elementsForXmlns:xmlns_chatstates] lastObject] name]; +} + +- (BOOL)hasChatState +{ + return ([[self elementsForXmlns:xmlns_chatstates] count] > 0); +} + +- (BOOL)hasActiveChatState +{ + return ([self elementForName:@"active" xmlns:xmlns_chatstates] != nil); +} + +- (BOOL)hasComposingChatState +{ + return ([self elementForName:@"composing" xmlns:xmlns_chatstates] != nil); +} + +- (BOOL)hasPausedChatState +{ + return ([self elementForName:@"paused" xmlns:xmlns_chatstates] != nil); +} + +- (BOOL)hasInactiveChatState +{ + return ([self elementForName:@"inactive" xmlns:xmlns_chatstates] != nil); +} + +- (BOOL)hasGoneChatState +{ + return ([self elementForName:@"gone" xmlns:xmlns_chatstates] != nil); +} + + +- (void)addActiveChatState +{ + [self addChild:[NSXMLElement elementWithName:@"active" xmlns:xmlns_chatstates]]; +} + +- (void)addComposingChatState +{ + [self addChild:[NSXMLElement elementWithName:@"composing" xmlns:xmlns_chatstates]]; +} + +- (void)addPausedChatState +{ + [self addChild:[NSXMLElement elementWithName:@"paused" xmlns:xmlns_chatstates]]; +} + +- (void)addInactiveChatState +{ + [self addChild:[NSXMLElement elementWithName:@"inactive" xmlns:xmlns_chatstates]]; +} + +- (void)addGoneChatState +{ + [self addChild:[NSXMLElement elementWithName:@"gone" xmlns:xmlns_chatstates]]; +} + +@end diff --git a/Extensions/XEP-0092/XMPPSoftwareVersion.h b/Extensions/XEP-0092/XMPPSoftwareVersion.h new file mode 100644 index 0000000..f58a52e --- /dev/null +++ b/Extensions/XEP-0092/XMPPSoftwareVersion.h @@ -0,0 +1,24 @@ +#import + +#if TARGET_OS_IPHONE + #import +#else + #import +#endif + +#import "XMPPModule.h" + +@interface XMPPSoftwareVersion : XMPPModule + +@property (copy,readonly) NSString *name; +@property (copy,readonly) NSString *version; +@property (copy,readonly) NSString *os; + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue; + +- (id)initWithName:(NSString *)name + version:(NSString *)version + os:(NSString *)os + dispatchQueue:(dispatch_queue_t)queue; + +@end diff --git a/Extensions/XEP-0092/XMPPSoftwareVersion.m b/Extensions/XEP-0092/XMPPSoftwareVersion.m new file mode 100644 index 0000000..0c41e0b --- /dev/null +++ b/Extensions/XEP-0092/XMPPSoftwareVersion.m @@ -0,0 +1,122 @@ +#import "XMPPSoftwareVersion.h" +#import "XMPP.h" +#import "XMPPFramework.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#define XMLNS_URN_XMPP_VERSION @"jabber:iq:version" + +@implementation XMPPSoftwareVersion + +@synthesize name = _name; +@synthesize version = _version; +@synthesize os = _os; + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + NSString *name = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]; + NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey]; +#if TARGET_OS_IPHONE + NSString *os = [NSString stringWithFormat:@"%@ %@",[[UIDevice currentDevice] systemName],[[UIDevice currentDevice] systemVersion]]; +#else + NSString *os = [NSString stringWithFormat:@"OS X %@",[[NSProcessInfo processInfo] operatingSystemVersionString]]; +#endif + + return [self initWithName:name version:version os:os dispatchQueue:queue]; +} + +- (id)initWithName:(NSString *)name + version:(NSString *)version + os:(NSString *)os + dispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super initWithDispatchQueue:queue])) + { + NSAssert([name length], @"name MUST NOT be nil"); + NSAssert([version length], @"version MUST NOT be nil"); + + _name = [name copy]; + _version = [version copy]; + _os = [os copy]; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + if ([super activate:aXmppStream]) + { +#ifdef _XMPP_CAPABILITIES_H + [xmppStream autoAddDelegate:self delegateQueue:moduleQueue toModulesOfClass:[XMPPCapabilities class]]; +#endif + return YES; + } + + return NO; +} + +- (void)deactivate +{ +#ifdef _XMPP_CAPABILITIES_H + [xmppStream removeAutoDelegate:self delegateQueue:moduleQueue fromModulesOfClass:[XMPPCapabilities class]]; +#endif + + [super deactivate]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStreamDelegate methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + NSXMLElement *query = [iq elementForName:@"query" xmlns:XMLNS_URN_XMPP_VERSION]; + + if (query) + { + XMPPIQ *resultIQ = [XMPPIQ iqWithType:@"result" to:[iq from] elementID:[iq elementID]]; + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMLNS_URN_XMPP_VERSION]; + [resultIQ addChild:query]; + + NSXMLElement *nameElement = [NSXMLElement elementWithName:@"name" stringValue:self.name]; + [query addChild:nameElement]; + + NSXMLElement *versionElement = [NSXMLElement elementWithName:@"version" stringValue:self.version]; + [query addChild:versionElement]; + + if([self.os length]) + { + NSXMLElement *osElement = [NSXMLElement elementWithName:@"os" stringValue:self.os]; + [query addChild:osElement]; + } + + [xmppStream sendElement:resultIQ]; + + return YES; + } + + return NO; +} + +#ifdef _XMPP_CAPABILITIES_H +/** + * If an XMPPCapabilites instance is used we want to advertise our support for XEP-0092. + **/ +- (NSArray *)myFeaturesForXMPPCapabilities:(XMPPCapabilities *)sender +{ + // This method is invoked on the moduleQueue. + + // + // ... + // + // ... + // + + return @[XMLNS_URN_XMPP_VERSION]; +} +#endif + +@end diff --git a/Extensions/XEP-0100/XMPPTransports.h b/Extensions/XEP-0100/XMPPTransports.h new file mode 100644 index 0000000..ffeee1c --- /dev/null +++ b/Extensions/XEP-0100/XMPPTransports.h @@ -0,0 +1,23 @@ +#import + +#define _XMPP_TRANSPORTS_H + +@class XMPPStream; + + +@interface XMPPTransports : NSObject +{ + XMPPStream *xmppStream; +} + +- (id)initWithStream:(XMPPStream *)xmppStream; + +@property (nonatomic, strong, readonly) XMPPStream *xmppStream; + +- (void)queryGatewayDiscoveryIdentityForLegacyService:(NSString *)service; +- (void)queryGatewayAgentInfo; +- (void)queryRegistrationRequirementsForLegacyService:(NSString *)service; +- (void)registerLegacyService:(NSString *)service username:(NSString *)username password:(NSString *)password; +- (void)unregisterLegacyService:(NSString *)service; + +@end diff --git a/Extensions/XEP-0100/XMPPTransports.m b/Extensions/XEP-0100/XMPPTransports.m new file mode 100644 index 0000000..41a5168 --- /dev/null +++ b/Extensions/XEP-0100/XMPPTransports.m @@ -0,0 +1,148 @@ +#import "XMPPTransports.h" +#import "XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation XMPPTransports + +@synthesize xmppStream; + +- (id)initWithStream:(XMPPStream *)stream +{ + if ((self = [super init])) + { + xmppStream = stream; + } + return self; +} + + +/** + * Registration process + * @see: http://www.xmpp.org/extensions/xep-0100.html#usecases-jabber-register-pri +**/ + +- (void)queryGatewayDiscoveryIdentityForLegacyService:(NSString *)service +{ + XMPPJID *myJID = xmppStream.myJID; + + NSString *toValue = [NSString stringWithFormat:@"%@.%@", service, [myJID domain]]; + + // + // + // + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"http://jabber.org/protocol/disco#info"]; + + NSXMLElement *iq = [NSXMLElement elementWithName:@"iq"]; + [iq addAttributeWithName:@"type" stringValue:@"get"]; + [iq addAttributeWithName:@"from" stringValue:[myJID full]]; + [iq addAttributeWithName:@"to" stringValue:toValue]; + [iq addAttributeWithName:@"id" stringValue:@"disco1"]; + [iq addChild:query]; + + [xmppStream sendElement:iq]; +} + +- (void)queryGatewayAgentInfo +{ + XMPPJID *myJID = xmppStream.myJID; + + // + // + // + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:agents"]; + + NSXMLElement *iq = [NSXMLElement elementWithName:@"iq"]; + [iq addAttributeWithName:@"type" stringValue:@"get"]; + [iq addAttributeWithName:@"from" stringValue:[myJID full]]; + [iq addAttributeWithName:@"to" stringValue:[myJID domain]]; + [iq addAttributeWithName:@"id" stringValue:@"agent1"]; + [iq addChild:query]; + + [xmppStream sendElement:iq]; +} + +- (void)queryRegistrationRequirementsForLegacyService:(NSString *)service +{ + XMPPJID *myJID = xmppStream.myJID; + + NSString *toValue = [NSString stringWithFormat:@"%@.%@", service, [myJID domain]]; + + // + // + // + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:register"]; + + NSXMLElement *iq = [NSXMLElement elementWithName:@"iq"]; + [iq addAttributeWithName:@"type" stringValue:@"get"]; + [iq addAttributeWithName:@"from" stringValue:[myJID full]]; + [iq addAttributeWithName:@"to" stringValue:toValue]; + [iq addAttributeWithName:@"id" stringValue:@"reg1"]; + [iq addChild:query]; + + [xmppStream sendElement:iq]; +} + +- (void)registerLegacyService:(NSString *)service username:(NSString *)username password:(NSString *)password +{ + XMPPJID *myJID = xmppStream.myJID; + + NSString *toValue = [NSString stringWithFormat:@"%@.%@", service, [myJID domain]]; + + // + // + // username + // password + // + // + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:register"]; + [query addChild:[NSXMLElement elementWithName:@"username" stringValue:username]]; + [query addChild:[NSXMLElement elementWithName:@"password" stringValue:password]]; + + NSXMLElement *iq = [NSXMLElement elementWithName:@"iq"]; + [iq addAttributeWithName:@"type" stringValue:@"set"]; + [iq addAttributeWithName:@"from" stringValue:[myJID full]]; + [iq addAttributeWithName:@"to" stringValue:toValue]; + [iq addAttributeWithName:@"id" stringValue:@"reg2"]; + [iq addChild:query]; + + [xmppStream sendElement:iq]; +} + +/** + * Unregistration process + * @see: http://www.xmpp.org/extensions/xep-0100.html#usecases-jabber-unregister-pri +**/ +- (void)unregisterLegacyService:(NSString *)service +{ + XMPPJID *myJID = xmppStream.myJID; + + NSString *toValue = [NSString stringWithFormat:@"%@.%@", service, [myJID domain]]; + + // + // + // + // + // + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"jabber:iq:register"]; + [query addChild:[NSXMLElement elementWithName:@"remove"]]; + + NSXMLElement *iq = [NSXMLElement elementWithName:@"iq"]; + [iq addAttributeWithName:@"type" stringValue:@"set"]; + [iq addAttributeWithName:@"from" stringValue:[myJID full]]; + [iq addAttributeWithName:@"to" stringValue:toValue]; + [iq addAttributeWithName:@"id" stringValue:@"unreg1"]; + [iq addChild:query]; + + [xmppStream sendElement:iq]; +} + +@end diff --git a/Extensions/XEP-0106/NSString+XEP_0106.h b/Extensions/XEP-0106/NSString+XEP_0106.h new file mode 100644 index 0000000..c9a01af --- /dev/null +++ b/Extensions/XEP-0106/NSString+XEP_0106.h @@ -0,0 +1,9 @@ +#import + +@interface NSString (XEP_0106) + +- (NSString *)jidEscapedString; + +- (NSString *)jidUnescapedString; + +@end diff --git a/Extensions/XEP-0106/NSString+XEP_0106.m b/Extensions/XEP-0106/NSString+XEP_0106.m new file mode 100644 index 0000000..2446dd0 --- /dev/null +++ b/Extensions/XEP-0106/NSString+XEP_0106.m @@ -0,0 +1,57 @@ +#import "NSString+XEP_0106.h" + +@implementation NSString (XEP_0106) + +- (NSString *)jidEscapedString +{ + NSString *jidEscapedString = self; + + // XEP-0106: The character sequence \20 MUST NOT be the first or last character of an escaped node identifier. + jidEscapedString = [jidEscapedString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + + // \ should only be escaped to \5c if it could be misinterpreted as an escape sequence, so we do this first. + jidEscapedString = [jidEscapedString stringByReplacingOccurrencesOfString:@"\\5c" withString:@"\\5c5c"]; + jidEscapedString = [jidEscapedString stringByReplacingOccurrencesOfString:@"\\20" withString:@"\\5c\\20"]; + jidEscapedString = [jidEscapedString stringByReplacingOccurrencesOfString:@"\\40" withString:@"\\5c\\40"]; + jidEscapedString = [jidEscapedString stringByReplacingOccurrencesOfString:@"\\3e" withString:@"\\5c\\3e"]; + jidEscapedString = [jidEscapedString stringByReplacingOccurrencesOfString:@"\\3c" withString:@"\\5c\\3c"]; + jidEscapedString = [jidEscapedString stringByReplacingOccurrencesOfString:@"\\3a" withString:@"\\5c\\3a"]; + jidEscapedString = [jidEscapedString stringByReplacingOccurrencesOfString:@"\\2f" withString:@"\\5c\\2f"]; + jidEscapedString = [jidEscapedString stringByReplacingOccurrencesOfString:@"\\27" withString:@"\\5c\\27"]; + jidEscapedString = [jidEscapedString stringByReplacingOccurrencesOfString:@"\\26" withString:@"\\5c\\26"]; + jidEscapedString = [jidEscapedString stringByReplacingOccurrencesOfString:@"\\22" withString:@"\\5c\\22"]; + + // Escape the charachters + jidEscapedString = [jidEscapedString stringByReplacingOccurrencesOfString:@" " withString:@"\\20"]; + jidEscapedString = [jidEscapedString stringByReplacingOccurrencesOfString:@"\"" withString:@"\\22"]; + jidEscapedString = [jidEscapedString stringByReplacingOccurrencesOfString:@"&" withString:@"\\26"]; + jidEscapedString = [jidEscapedString stringByReplacingOccurrencesOfString:@"\'" withString:@"\\27"]; + jidEscapedString = [jidEscapedString stringByReplacingOccurrencesOfString:@"/" withString:@"\\2f"]; + jidEscapedString = [jidEscapedString stringByReplacingOccurrencesOfString:@":" withString:@"\\3a"]; + jidEscapedString = [jidEscapedString stringByReplacingOccurrencesOfString:@"<" withString:@"\\3c"]; + jidEscapedString = [jidEscapedString stringByReplacingOccurrencesOfString:@">" withString:@"\\3e"]; + jidEscapedString = [jidEscapedString stringByReplacingOccurrencesOfString:@"@" withString:@"\\40"]; + + return jidEscapedString; +} + +- (NSString *)jidUnescapedString +{ + NSString *jidUnescapedString = self; + + //Unescape the charachters + jidUnescapedString = [jidUnescapedString stringByReplacingOccurrencesOfString:@"\\20" withString:@" "]; + jidUnescapedString = [jidUnescapedString stringByReplacingOccurrencesOfString:@"\\40" withString:@"@"]; + jidUnescapedString = [jidUnescapedString stringByReplacingOccurrencesOfString:@"\\3e" withString:@">"]; + jidUnescapedString = [jidUnescapedString stringByReplacingOccurrencesOfString:@"\\3c" withString:@"<" ]; + jidUnescapedString = [jidUnescapedString stringByReplacingOccurrencesOfString:@"\\3a" withString:@":"]; + jidUnescapedString = [jidUnescapedString stringByReplacingOccurrencesOfString:@"\\2f" withString:@"/"]; + jidUnescapedString = [jidUnescapedString stringByReplacingOccurrencesOfString:@"\\27" withString:@"\'"]; + jidUnescapedString = [jidUnescapedString stringByReplacingOccurrencesOfString:@"\\26" withString:@"&"]; + jidUnescapedString = [jidUnescapedString stringByReplacingOccurrencesOfString:@"\\22" withString:@"\""]; + jidUnescapedString = [jidUnescapedString stringByReplacingOccurrencesOfString:@"\\5c" withString:@"\\"]; + + return jidUnescapedString; +} + +@end diff --git a/Extensions/XEP-0115/CoreDataStorage/XMPPCapabilities.xcdatamodel/elements b/Extensions/XEP-0115/CoreDataStorage/XMPPCapabilities.xcdatamodel/elements new file mode 100644 index 0000000000000000000000000000000000000000..ad0de649ee6082856cbb200e91a9fb0d6a92a645 GIT binary patch literal 45046 zcmd441zcO#|Nnn)NJ3oE;_ei8m*Vak0t5&I0!eVY7wYS}t*fo=>TaudZP%`{x_hb1 z)^2szt^M5_2!xw;pU{2(>wi2}!o4@I9C^Rb^L^g;mCa&cG;OMzOu)+O+CO`so0X@J2 z@C3Yo5Fiu?0}6mbfCmVGGC%@!0o}kzU=%PK7z2z2<^fB9rNDY%3vd893>*ip0uO*! zz-!!@)o>2n+^8z)&y@%mTB)B9IRjgEe3m*bR;ZM}eck zG2mEm95^2Q4qOUu26utG!9(C-@CbMmJO*9?uYwQ2KXFvtFq}2c3+IjV!TI9+aQ?Ue zTpTVJC&V@3#^9#nrs3w`HsChmw&M=suHf$D-f2)Yv@{Gg95nniVl=Wf$~B}K-5QfL z=4q_a_*vr^;H+^)Xs))Lkc z))O`mHWD@wHWQ8zjuOriZV+A&@kB1soajjOAchlDiF{%$v6J`(aVBv&aSQPP@gVU6 z@ec7N@s%b`ldh?)X{PC^8KIe`S*$72lxcQrPSTvGxkmG6&BL0PH1BG@A(2SBBx{l< z$%_<6iYKL$ibyh2H)#@S9%%(>J!v=TB(9e3E>fe3$%`0#aBM6N)n>kP<`5p;S;BDWfS2CBWdNe(mo=>l(chV=)zo##u|3KeC zKT5w$zej(|ATo3qRtyhDI3tz8XVfz47^4|u7+*1FGJas}U>s#!X53~xWcmcg_>kjJ?>ph#!He@@n{n$zDWOg>Yl-m_^{;{4LE=0twA zAfMM%ikzsfMlO_QT9%0TQYnwcLXXOY68JUza)G3v{{Z?X9*c?oEfI@l$^4Qkdq00;+Z@K}b~3CYRf{91moP$ZNI1ybjxl2X2mFPFUv=mWz51Hce40*nC@ zq&B928DI`r0G5CiU=7#+wtyY-n*}%mPJlBo9B`5UWsT}agw>Z+31lP0l5n)R0%;>( z8d+0<)Vq|&8m4r*uW(YSOe{ePIrzOXH8O#uj9(%cI4!T0xUoi9Q=WjfB9CSCH);c) zmdQgdM%&gv>9Po>IIP^0orXb5j>W$(wq7VFz_CT0E4?>a3=&p1hlJm!QdVi01+WP`Zq*KPRof* zJB{>$H{b*K0)9YXR$`iSm7rOgi~R1R1Y|QG@CO1QO^7rf2m*p3GBg=&q26wmhKnTv z^rO>y8#_sk79Pt=aXwWb71v8j1b=(oH5Lm92O@w-U<6_hQ9v|e4>3S2QX+F84v0sr zA^}JQk`NoP26TWlARVy@Yvf24Qldm47svzhc`P%fMo2N(BC76#=zD(JCl$82ENm(XJfS3|rBy8z*&(eB? z-z9vc?1MiIbD0{S_P2G%$f$EoU#atWLTPBRR3_n<$RH}@gjf&((tv0%C`MjTVXzLd z5aid-5QW5Hm%Mb(>JuNZFFsHL*~|lqA^JR^6k-%+NAQvdnx_XS2Pyy|Vh8A`VBXjF zRY-pa0X0a!7bBy>|6y+{_rXWI5~(N?sT~<|gy;}J8mNj4XoYMR1NA@y&|#DDv-p~l!td>F7Xzz+)xZzH8elE3Zty7{i!*Skyep9FE0D;V5w(TDCSWu0Be3po z&dKYm&L^qU>)TX%y+P6INCO)xbq8DC9j6eh@c|A3hajW5y%jhD94$;tOQ@Iei$wyt zB`*Mu0LOrJ2w~)Zs#6S>N--Lhih(#+zg2ML#ZX7{x{79#lBTJWCReUG;Pa1wXOPVT z;4$z7cnX<8=8(k#;5qODcmY{L)({e+*dYOxGvd-BfwQ!^rUY@x1|bq>IQNEQvgW>? zL}DTy%k?AIMmCj*>PrP9B;sn6s^Jyfm|WCMW7Gp3;KcnU;yL}6+OIm=Q4i%M)B|=$6HIKzqz6^QWH5R8A17J7`0x$wWfhjN% zj8uUE>W?Un^TBweH&7fTBCk_GH?(iS3>4351X2GhI{L`@zHkERdh`c@G70iT`kqRH z_>eR5!h^)YjSz?i+A$X_Kw?)g56p+WAn*BLA;^P#pj3J9@{&t&k&LLB9(VXA`&mzs{rJxL~2OGdf zunF>m{Gk9S5DJ2Vp%5r^DcAzGf^A?s*a3Ee5C}tIP&gC;#Y2fu5|j+3$R{;O>mb1h zN^m5&FO8{5m(HFMTu}mZ^uaNb>Lmt(jCj|qOA9*O%`-XH0zeXxV!W(tQ7G%Hz3sH+je&ity zSSNqo8$F-{mQP=-;U^zk)X+o>0jnud(nJkbq3N%#Cb(QhQ>vsHsiYYo*Br3?E#P*9 zRNz+NDewdu0a-xi3&0(6QbqNW%H<;@39XljtI?3$7~5Bl zew51@D%b468Q=_YXtG9WQie0>9|GkQfq$G{_YK?sc+KBVjQ?p;mew}}dMgisI9uFs zWKxE+!`b5;aE>@9oHLXTWk8uw7L*O;EXKLuTybtVcbo^#6Uv41pnRwRDuj4)Xz`Mf zu|JL{5tP@9_>#z`S_GeHvK--~e1@ir4q5U`s)aR~d{MnXaR$Lv-T}k7#DAtBD(j_~PI`LTM!TD5QUil=>(~ zuF1yzBoAY8({bM+(P(l4Vj8`9uEn?+xS6O}EiMY9F zl!%+x8ztiAe>_UWEkkNE5GA^!wZpCZ&!farr1j)c;&9|e35ojw`R{~RY-~T%aGP;k zl_N#mHr!8;0IKW3?ZEwv+ld+cb|ZtI@8^vF%8bD#X9S!}zi-ta}6_sNf6vc|t z^~N^pXd>|vrWLWFjY4xsp$$e88`>x|)eddYxDKPK9EPAFm_oBR3{j)*D4sBy*l!ELVrp}<0=YkjcdKI*0}z0SZmxFfVIYb&;zvyjmQ5v ztlQ+UhQV==3-YQJxs_Dmxo=>+eWCGMPHK%;Ncy9FzQ!Akw@?Q(ULM%+l2etFHyVFw zyw~_J*uC)};@*Cr6W*POd$(ZWji=zbSa|Pk*T(DMb@6(5ef%)I0R%xX)CG-# zMnhwuvCud*+#Q9t#v5gKSIK^KdQ=;*8tSYqpS8d7TyX?wc(9| z7E?|vXcd}6%1dB0l>-7a7*c5Vb5$HU+kFgO7Q zzl6bweRM%y=y#9u(kgzxDk6aL~y$P^6Z<8L5}ebY#d`*@U7`S`p4Etw{hU{KnR zrWGXQPYL3HEc^rfL;NHBW55{y1pgE;#XrM8NAIBHU*KQ%u3*5wMsArPt5@)UDy?1# z01^or1pNP{iK=nqMXeme`*Oag20XhYy2-5UHkm!?cd>N6RMCFshd4Z(n5 z{!dF&2o?lOf)&AZLZB%-`@mul#L>}J8q@lmy?2#{ z8kz(@6-{MA%~aB4%LkCYo7#jJ_-9AAXI_<_VJn|o)~K${;YoKjAC5>6A& zKx?7xJ%n?F^8^&lO80vR7XfR+qR%;NT8E5utFf~t!Y#smb!JV32ZV=&M})_OCxoYj zXN2d_dT0Z*5&99@0&Ru1K|iVC1aikZtvvFn%$nAI%B+bnSQ9&IQfR6@Ya(hR&N^6A zdDgTEaaM&UX4XWsB-&zUO+-8S$h3LTtcmDMbp6y>6FNK%E}inM361g_q^hYr zYf{!6(yR$nPGu%3YYu7Fgwe!uNKyA8&6+Tp${bQIXWy)e2oYgo*I@4^j_mjD&y@1q z(dXTRQl7*wiIe{+SME>3fUoLJuyYaW<3>} zYG*x(f2dSnIqQjj<3yp^cY{>Jv>(^R*C?)uZ+dY}eEV@+Yl4GtJqxu9P0D|c>w^kh zFGgM*AaN|i>HL0NYce%C%DC3#YHC4;pmRN%I-0tgdV?KWa~R^#i$5o>48v?i(?!!&(@oP|(*rsJ9fgiTC!tf&Y3K}eRt*bBajnT!c5oG3ANmwr zV>Ge2R%oh?Yt0Omaw_BcIAY-nO$@FzOEoL7xYiWPaeZPCt~F~k>pm6NNRks%V`W^U zNlr!S0uh^4J0XeD#Nt|^IV4m$6`DiBHAWMQYlUVXt~GaR?$X?i z@$O&xz56ra`h1^v55l$PQB7q1!N(V0X`a zYfUX>Z&tzexlh40MiYx`g{IoL{+n_t;~HIbrO@of^{3Sx%W8$@kXVhWJC@Z7O|@A~ zvQ_CLWmaERs(b%prhZnFyirz@e0o_;^8GlgNg;z+{XJ?Fq?rGl)i)HZUV^;1PU5uC zySV+VCMA+mlvzzmC8a?(p@%)B3{oa3Yp^$yau9D`@;O<33-RV_SXPsYNmc5wnj|7s zlWIs}QZ1>DBq2$mU!mWi+t44-UFaTkA9|pMg`=z{r7C;33af8^3ac@iSXL`E)n+vb zNq%B9m05iUv2cYZhSj76q$OBZla|U^{rezRlU9+|{0mlNN{3~&qIBxB8sR@iQ<>E$ z{}q};Vl_q+%W8$@kXVh;#IjnUIV4tNG_kB!X!fz1^nnbJ!NJ~5*68={&&29SecnBY z)npnOS#I!gR+F{J+GHKFE?JMP4?TvSKu@7(&~xa88s3aDkd&tE%_^*Z_$jQ$XkuBd z&>Rx0F`8IbD>R41YK$h9)e6lau^OX^Wwk<6ZB~=ZRr*Mo)h`jhR%oiSn%sc0n%vmS zYI4)ZSxxR7#Om3oQIN;}=d6CCVD(bu#cLABObHq6}UT2VD)XM#)W{r0msLawEgyr=S}nib3~)=#bcr5yh~3Ky*m( z#)x9zJs>(HdSgT}^d1mZ8{d@0DjlVSZy02tJN+nnN)_OgjVQn=n|c9G+5B;UQ+5mj z_#D(WDEt3wfWshLLGWehvMW~a7kJiAGm7rVIOPcCxH85mCnzUjkOPCpJ(SawGnBI! zA3u-y__EK)aTw&HTQf1SB&S@b{GkrXDR(LNDEBE3C=V%*D32*mU{DJNwP8>P2K8ZZ z7z`S~pdkzzsp0l0%qb_7-5v{bq#-|r<`_{7%?Cu)#yORvQc@+H!=Nr|_llxofKD}| zT48}sweAJF-XNe;9jMNq3Ut+`R0cY7J9eOaLn1n+DKSJJ5FHZIF`^ix4~Pzl=@?NA z(+5O{1a*uk2I>Q%eN?BmQ#+`ggFT)K_j~;3qPmH~;|F1#I-dH?KVh9ZiTW*dGIa`d zDs>tRn!=zN44T8B1q@okpp_c_jbfd0QrW*%v2Oe+SjUK}U|k_PB&=gZRj{rQ9TL_t zqAFNdhz<$s7*Q3hD@4`CI`ub|jv9n@YjpTfsH$R}`W(eN^+hk%sV_f{b?RS(us#>H z4H~jT>wmd-3%Z=zPJ#91DAsLBoY*5b68e!&qtY14NT)GrEEu$hL9ZSfhsLF84R&yv z4&va;KP%E5PzSfcBAsSJL)KOO?|oTlb~JmM1I>}MY{c`ARQyBf^>zb+DNCRsFZXN(uboquTaGxomNCE#Uh<1 z=ta8AAf(eOY1RJ%>6lupAYDEJ4HK9J3~85I|qY4 zFz5?|elX|{g8?uYsD^)|NT(s+uaW!GkdTfM zRYAH!bVx|Yh^io6A*wdg>6R)TH3;cJ=7A)R^y z91O<8V1gQMk7AvUbg=vf?d+MVSP%OYtYbt~u&xkQ8|(D1RZ2Pt>rtrPD^xL9r_Z4; zz+#=guovsmgRo9tMql}Jrhinht`Hp( z)-j?gSXYP+3F{b96|5^n`>;-bNB@)l7slg1^n3j0!g`Xz;|F1#L1Hlf3F{0NgU#SD zxC|`@l8H-(!4w!wg~2o!Oozb?HT)aJIz36*zg4lG_$gS&h^k;+Avz?iV?ole&+f(g;X4JB`mxR!$C#jub;g&Bi7=Q8gJnI8 zuNmJkCJlCS#$?3FSAABj=b=uXg~dAKd&XjQu+CV*Sjt$&Sk73%Sjkw$SPg^uFjxSC zg)qp6!D1LJfx%K3M7}-vKX+J1vCjAs?ONpLBUsP<6s%)JRj{rQRU7M!<0>T`gmoTj z_X<@E))`kBH?de}-0H=8(IBiderMeKRIIBur83s#1Xh$!eXJv~0;VaIu`Z7l6rw}I zI!06l>k82!VI3o?f^~)Hkg$#sRl&MKv=8e{Kc+u3V6ewCgZe%Gb78$);qim8&WvIv z{u9=jNz7zs3Nw|N#zZ(?0fRyqtc1ZT7!<)^wHp47Vx2Kj*}qk>UiK+i$B3$6T_HLo ztYbt~u&xjt64o)IDp*&D4hicRQ5CE!MAgPRbG}MPp~G=M)@#t=O`+P4_1u9iFZ#Z$ z&s>juSzo?UCvzinQ!cvyW~BVN8%vm*nLjeOFt;+dF@FNqDV;%f^F*H9pm^?td{-C6 z-~T5Mmq0hs`n@04%$>|Va#;6mqQ(4$xfcdyFbMT9_cISL4`N*RFkp>r^YZyNJcq%0 zWJS!kSXML7Ft4b?YUWkuHRg5Z4dzYeE#|Mx-(au-1{-0p2?kqXunh*=VXy-RJJs-I zl-0~%lzmxs3NHH;R%1j}SgjCMo7F6$N=XN?8rd=q>GHm!Vpz@6W+9te4&FYTHLRD_ zErVFiGGQTmji_!0u1-@TSjE&@nbmTvD#|w`R%1j}SgjBp600$yDy&wB4vEzmQ59Az zM2Ey`jHn8$6{3BtX4SDIEa_l}XVohl{xfZ(1tX6o=ymu(`)IK`SfdbBe{>%$)@ark z)>zg!)_B$zFxUly-7q*321mo-7#JL@j!hza(J=QayZ4~YtXfc?f%<$#v76QZjPHRU z712!tSV0~wr6l^Lx_5h6D^)}{4~W9xC}q`&a#a?nMhpCn?vEeU{pw$ocFRXqzgAaO zJ{S+~t*ry9kUZK~^wu|WQ8|=Zzq9_3Zx@-IpuCmZV%A;OJ=T5J1J=X7$14p!|9`lT z_Ww4SV3kePluc$bvT%8PpBbucG3XYen2p+$ECxci5LIaQ+KZajg3-iUOTVbv#_~4> zRWg=A1y$cvR4cD+D(E+v6qWC_l>V=`U`#}l zHJ~SIitL2{Hd#YP(lclU6`eD*Auc*za3(+V&MT_+yu`#p(oq>{@mmTf&yIW$b!(0~-nZCd1$q z7(_OK`wj+Yz~D?6oCSm5t6}Zvc*D+=k1!ui)J*!6;146J68tGd)eipH(^X13DEOO( z(m|n$3I5ni*ekHXAA4nQ@Hc%>@W)=u-taGiKTNGvf>=}UX>zr1TPf8URF z&Nm!nv#fz<-EpRH5W+2o!A(7!>74I4GcZm*3vu#*&x-XGsFN?jVx6;yvsxXjbAI5g z;jHDXBPa2*V;hrtanxKRzaN3qVCqU`pnIi2O7f_03j z3f2{(YGa*qUZtdiu>J#T_X<@E);YI1cd=OK-0Q{qnn76SJmw&eIsLSpj%rgXV_iNY zQf@6u+B~5X8se_xmnz7ZVoq>o5w|NvHl2yTVQZ23~qzLpI~si8vczE zm@`$`zg4lm=~J+d5mmvuLUc%2$B3$6T_HLotYbt~u&xjt64o)IDp*&Ds*QCn^1Mro z=pd}`K!-PlYCqQdZn|){A~#+7amd}y-2vImRXVVXyL<3~z1)510eMP@dw_coc^EzN zRCR$UnO{=HFBdH49^xM69^oG4B9E-boa6O9yu0txfBWQbMGa04s=*oV*@5z%=U&J` z?<2`exPaTky$GxuIFQv>bfjE~X>o#PL20O@qyl-gy5uDH3SgHm5sPII2#^4Mz#MP@ zJOMu-SmUI|d5!0I2HqO)j`zbS;Pdd+crjjrug5pxTkvi84*d7{bNCko4#AZWPAEY( zt{zL6OjtwMM7Th>N2C!ML^e^2s6*5v4kH>7O^N2n2C~*fTcSPDk?2HpB?b{k5L1Y0 z#B5?NF`vjMmJkKRQN&fm$C_H2KAL%&U7GVX4{AOpu}B`IY*Hzyj8s9YM79&ELADZ- zlIlr~q!v;ese=TQx=ABR<4E6-W|QV3JLxPUEhQ}{Z6RGH&eHF@9e*)a3~IxN=hxI znKGL4HDv~6F=Z{~C(1#}N#qNoXDR0?J(P=-OO(r$E0k-L8N#)twqZjiqK$1=JSmMCu~yX6jDrLF#4dZ`9Yw7go%W?!3k2m^PmF9c?acDeVsJ586H21KK0nW7-qiQ`$4y3)(B% zYuX#yJKCRge|jK2m>x^J)WLGPogK&3+Oz08M4;25?N;|rPtFN z>CN;-^d9;<2A)A=kQr2DRhkY%mobcC!SH2-FcKIO7!#3|Ig=QZ8B-Wj8Pgck88aBO zkmWD48FLx)81or77`GU|G43$_VBBTgW87yvU_4?xVLW9#W4vIzWa=$ezxfi*4cnY~GcaC|1d69XUd58H2^B(g7^D*-o^Bs%C;;>9vZmf3JT-Hw3 zb=FPRudLfhE*8zYA~{#qBi0kvQ`U3VOV%saYt~!VJ2t?kvzcrywhmj5ZNxTVo3Xvv zaY#OAGW$pNHTDhmE%tBhJM2H$_t+2E583b7e{mSd9FfD(;^-iA!C}Ze(3mrva$j-ZaNlZaYLT^AS{yAcEgdZ*EfXy>Eeowotr9JPR;89mt42$zRj<{k^`+Jv ztp!@kw0_sRt94)Nq1I!qCt6Rno@qVTda3nV>y6f1tv|K?(t58Qs2!{wsvWK!sXanF zN;_IRMmtVBK|4`9NjpV5Rr?$5Z?&grPt*QRdxrK*?OEF2YtPZ1r#)YLf%YQp#oE7V z-_ia<`=0g#?T6Zrv>$6f(SD}=Li?rmEA2PhZ*_ci{B;6#f^|Z5!gRuQB6K2kqI6<( zVs+wl5_A%Eq&m}dw&~o|`Bmq(&hI*Rb?)mt)OoD)MCZLOpi9?f>aumYy4t$By861- zy0*IZx{kWex-PnIx*57+-LG_)=^oU*sb{8Vp=YIMqi3gQujioWsOO|NT+da{P0wA= zQ_oAUUawKFS+7;EU9Us0QxDRE^}6*&>5bMKqc=`(yxu{*!+J;cj_aM&JEeD8?~LAA zz4LlKdKdLB>0Qygs&AxkqHm^ep>L&ct#6}mt8b_8pzoybtUp}eRo_j&PG72DuivQO ztly&Fs^6yHuHUH->v!pQ>yOeOt-n`)zy3k}!}>?{kLe%RKcRn8|Fr&D{d4-~^?UR$ z>R-{nt^a%&IE*>WaMUFtxCUAV+6KA?dIrM` z3=ND7j15c;%na-eybXK}f($|o!VIDeVhrL8Y7HhE{Ah5^;D*61gWnA982n*y&)|W< zLxXn)e;G0iS%w@#EkhkcJ;Py!hK9z5!wp>x-3>hry$yX0{S5;RgAF?jA;T`ik%prU z#~6+^9A`M*aDw4P!>G!dGlu63FBo1lykvOU@QUG8!|R4O4R0C#YIxi5 zj*+#It&zQvqmi@Ga3dEZS0gtg48+RB( z#<1~DQ?e=5lx`YtnrNDAnrfPEnqiu0nq``8nroVGT3}jeT4c&MoohPZ zbfM{D)1{`%OqZLkFkNZ7+H{TSTGMr=8%#HvJ~w@7`r7oZ>7S;5nZ7suUvW(&<0nJqS3 zVz$(5x!FpyRc5Qr)|jm|du;a9?77)Xv)5*C%-)*4GyBu*y*XeGn&Zsz<^=O(bE)~a z=IhNjnr}AWV!q9MyZH|Do#wmDcborWzSsPK`62Ve=10trnIAVlV}8T@mih1Icg^pc zKQVu1{=$M};bf6wQExHJVz$Lxi}@A{Ef!lWwODSk!s17ZtrmwZj#?bIIB9X(;;hAa ziyn(h7WXY4T0FLRYVq9SrNwKDw-$d|`dbEC23v+&hFeBhMp}-rjIxZejI)flOt4I{ zOt$>W@*B%#M&as?pInQ!|l zA6Y)Jd}jIF@`dFq%hy%}D@`kg70ZfarDbJcWn^VyWoG4U>lH-&#$vnrd~{>bzBt)g`MdR#&aASzWifVRg&u zH>=xLcdY)fx@&D`?O^R>J>1&W+RfVC+QZt@+S}UK+RxhGI?y`Ey4||d8n*7X9%Vh+ zdW`j0>v7g!Sbu3f(fTXvZ>%R-pR_)0eb)NCb&vH$>r2*`t*=;Lv%X<{)B2Y6Z`QYM zs5ZlG(rj96+H5*(Ae%0mkv5}k#@dXtnQAlLW|7Sjn`JgDY*yL)V6)a{hs{o#JvRGn z4%!^CIc{^`7PRHs+SvNq#@UXyonSlB_G{Zow%^)Lww+=-)pok=4BMHuvutPE&au5~ zN4ImbOR!6_OR-C{%dpF`%eKq2%eO1AymJau^P@Y3OxBi@nd$aU0q)OFN%G;lO>G;wrvbar%c zbaV7@^m6oZY;~w@2VaG1VZpTrMV;sjij&uCNaf0Jv$D@wN9Zx!*c0A*F z*72O%}!gK zwmEHg+UIn@>9EsLr*lphoO+z~oQF9ZIvYEiI-5D0J6kwgI$Jy2I@>whJ3Bf%Ig6ZY zoNJvW&NAnE=LY9S=O*VC=Qihd=MHDc8FpUgyvg}T=floNosT=8bUy8T*7>}1kMkwx zE6$IdpE^Hxe(C($`JMCo;lOZ>;l$ze;abBDhuaT#9_}{WdwA^dq~SA$Zy$bT_+P_6 zxPUGiE(8}%7m^Fbh3Z0cVYo0|*e+ZbEf;MUT^BtUBNuxYM;BKYcNb3=KbHWPAeR!C zF)k}yPPv?MIp=c0<)X`Fm#Z$g?*`>gHPKDs`=QZFFsRZE9J+2qs^xX{HjNDAz%-qb~EZi*JtlVte?A+|#9Ne7ToZbB0 z0^K6qMz}@0#k!@qrMYFeWx18QmAO^9RlAAZWNr;^O>UdrwzzF`+wS(W+fKJ#ZoA#~ zxb1b@?{>iLpxa@$Bkojpx;xXI?ap=Aa@Tg(ao2U%cQWO0HIEw}w>*CHxa0BMxtRJd-?=JySf>JkveD^_=24&GS3YnVz#ezxSN&ImdIJ=K{}#o{Kz}crNw4#^4pucuzmyee5d|Q0meA|6Hd^>$1-!9*gzN36c z`;PS;=X=2Skna)SW4@hkJI@T>F_`PKM|{c8Q{{3L!dzXrcXzb3yHzgEAk zen0u`@Z0IP+i#EGFMfOd_W2#~JLGrR?}*A%Z=xBoBx`~3I&AMiiqf7t(VfL4G{055rC=U<@R0UKA)C9B!v|@ z7pM^!78nsYA}~5IHZU$QJ}@CLF)%qWH83qOJuovcD{yMy^uQT`vjS%a&Iz0wI4^L1 z;KIPgflC6H1}+a=5qLN7e&EBv$AM1+p9MY-d=dCE@O9wZz;}Uv2EGsc5EK*?5)>8` z5i}wwDkwTACMY&2J}5CLDJVH8H7G4;Qqbg}sX^0&W(3U)niceY(CnbOLGyza1T73& z9JC~8dC-esyU6kHQ54z3NB1WSXPf=309 z37!xmj0zbOGA`tckS{}ygd7Vw5ppWzOvu@gb0OzLE`(eRxg2sO z6(0!o?LJx%=4?P)r zI*b#h6{ZuW7d9-+Aj~k#D9kv_G|W8ABFr+(I?N`lEUY4|GE5X!6DAI;4XX>2gvr7h z!WzSx!dk*w!?uR~6t*L5XV~trJz>9u?G4)(b|CCf*x|4vVaLLbhcm)i;hb=-aGh}7 zaJ_K-@L}PG;l|-6;ilo{;TGY=;icha;T7SP;Z@kr9y?lqYH%dE7CrU4BSd>AOVU%%{Nt9)jYm|GGPn2I&KvZZ{c+`lfs;IA`)<<28 zx*T;i>Uz}8s9&RQNBti4N7T!x*U{u?YBW8X8O@I7Mr%jwM(ao0M>|G4N4rG3M|(y4 zMh8R(M|VU+(OuCaqen-Ni5?q0E_!_Qgy@OUUqyc%Jt_Lz=u^>WqR&NNh`tzoDf)8s zmFTO{*Q0Mn--`Y<`gZi480#3@82cE<80VPbF)lH#F>Wy)FlEu8TOBKot&5e$*2gx) zHpVu^HpjNcw#RnFcE-Z7U9r1j_r&gv-5+}}_E7BM*dwt=V~@w4j6D^5I`(Yrxj5}O z-8lU?gE*r&<2aKz(>Sv@i#V$|>o}V@yEyx}q&R8Zw74B{JLC4m?Ty+WO@s;t*;#b74ivJ;gUHtm^4e=Y}H^u)Lzcqea{7>;a;(v~Rp8zD_67UI{ z38Vya0wsZ(Ku=&MuoBn_+yt$Jw1kX=tc09|{Dgvp!USGIQ9^M-X@Vf3ETJMnn6NZq zdBVzs)d_18)+MY@*pRR>VROQkgslnN61FGoNcbz^Ln1B_pQxEgN+c&z5~+#wL}nr@ zk)6m*)JoJ%EKVGixF~UN;{L>giH8%9CLT{bk$5`sOyb$Z^NAM{FD712ypnh|@kZj! z#CwUa6W=C&NCJ~Il1NF^Bu0{ZQdUxH()^@_NlTKJC9OYUWMsq<3jr!GugoVp}+Y3lOS6{&Yq@25UYeVqC%^?B-x)R(ERQs1P$OZ_wT zuhb7|K$=xrOj>o?C(ub$JrhBA&rhBD(r~9P)r3a)3rU#{mq=%+M>0Rj~(?_R| zOCO*9Mf!yFFVnwD|0aD>`nTy*(x;}MO+TOBlYS}vYWlVG>*+VrZ>Im6emnh6`tRv? z)9+>2XEWMaG1T zi5XvKe3LOLV{*onj58VMGI}yDWn9g;mT^7fM#jyIUo&oJ+{ySo<8H>ijE9-NnfaM* znX@uyXU@%>pSdt|apscD<(VroS7xrx{2_B~=K9PHnHw{I%-oW>BlB?P(acksXEM)a zUdp_hc_WLMWuKLpRiD+E)soeg)tLonb!Uyr8lClR)|9NpSxd8)XRXXyowX)wUDk%I zOs(e(*5#~g*~ZzX+2+}n**4j>*>>6X*$&xG*~7D4vR$*?vpuq< z+4b3t+0EH)+3ndK*`3)?c31Yu>`~dHv&UwS%RZ2ODEnyk@$6ICr?bywpUpm(eIff| z_NDC0*;li#W#7+!nEfLARrZ_gcR9EmLXKt*IY%c)FUKgyB*!erI>$E0K1Y~Sl~bJ~ z&XMFub7VR7ISn~YIW0M@Ic+%|Ih{E>a(3qI&iN&0f6jrNgE@zC4(A-rIi7PO=VZ?5 zoHM!HTV_bT%%m$T$5bWT=QJZT&rB`T-#i`+=|@FTv2XKZe6Yhl`&n({{FjmevwH#Kj1-i*9id9(B8=B>(Glea!^Q{I-mpYndrdylL( z!R6!gHSC@3u874Qp63Q7wE1?2@51xpH+6|5{+U9h%b zUBUW-4FwwuHWzFu*jliyV0*!if?b8Q!r_HUh2p}xLTO=rVPj!)VM}3qVMk$SAzav1 zII?hb;h4g)gi;ff>D>_khs;H;va?#bI>qQTW z9u++=dRg?k=&zy=e2^c)593GhNAP3#vHUoGJU@Y-#82U;^3(Vk{7n87{xtq~{F(gO z{5kx&{CWKO{Du6*{3ZOQ{N?-={JZ@7{D=I<{Ac{<{1^O}{8#)p{CE66`G4_06a&S< z#i7OF#gWC)#WBUP#c{>)#finq#VN(9#p%Tv#gmJt7EdppQT%=J?BY4abBpH{FDPDA zytsHt@v`FO#m9@EmFSiPm4uXpmqeCCm&BIDmn4=XmE@NcmQymFvrk0#7IbYIKa;fBM$+eQ}B{xcLmi$_ByW~#E?h?I+i+@ zx|F(?dX##WdX;*Y`j+~a29ySt2A77Ec9z1W-KC>S$Ci#O9bfuI>4egWrC*nRQ#z@1 za_N-PGo|NBFO*&^y;6F$^jhil(i^3>N`EW8U3#bVkJ7sWJAs41NibaCCU6&c2s{N| z0v~~&z+Vs`2oeMfIs}lQTQEv6RxnO5Uhsurf?%THYr!{yNrK6ODT3*OTV=#D>#`AL z(Pgn^@nwl+$z>^J>17#ZnPu5!Ic0fe1!aY0yt3l5k}^?Qb6IN{RMu5CvTR)0gtD*7 zHk4f``?K7l+^XER+`inYe0aHQxqG=sd1!ffd3t$fd3Jejd473ec~N1KC*mF`S|iL%a4>FD?d?ws{CyEx$^Vn7s`9eFO^>@zgm8+{6_iB3bP7}3abj6 z3i}F&3dahk3g-%!3bzXP3Xck}3h#=Bil&OziuMYq057ywal5s?w&?w$iTBq0+Ik zs&adWG7gh7Ci>phj1=Z!%71hG(s%lZS zxVojfyLwdh=<2VkXH+k%-cY@}`h4}h8eEN5jed=Fjctun&F~u68m}7Pnt&R9&8V7X zH79CL)ts$4U(-`_spd+}wVLZS4{9EX0WnUD7Zb%KF-1%hGsG;hnb=ZnBeoYiiCx6* zVlT0;xJle1ZWDKiVR4tZTRc)cN<2n9PCQ=xh4@SHMDbDaaq&s} z8MXPfyjp&3No{Sdq_(lPxwf?ys_m*BS-YopZ|(ltgSAI$kJcWmJzjgF_Ehbe+OxIi zYA@9G)alg?t23-Kt~0AMud}GLth1`Ksk5uIuXCt#s&lTZt`pbQ)k*6b>Kf~s>YD3X z>e}i$>N@M7x~{tJy4`iZ)a|P~P$p;POQfaJGHHcWD6N!=q}9@u z($&&6(sj~}(oNFM(jTQ;q}!z1r8}fQOLs|k%Rrfij3^_?s4|+2E@Q};GPaB>(~@b+ zbY*(7VX_=qo~%$-BrB1X$^^19S-DIotCERi)iSZHR`!Ezt!#sAlWdD@t8AO>C)sw{ z&$3;z-LgHhy|R7vgnG?-ay_-4QO~Sr)wAn4^;-2h^}6+X^~357>MiTN>m%y3>Pzbz z>nGOFt-n@(qyAR?Z}oTTf3N?e{%-xf`Umxo>L1rXsee}gyurP}v%$N;x52+5pdqj! zs3Eu^v?079q9L*&sv){zOvAW_2@Mk)zHa!YVN%1l4U-$DHcW5$u3<*QtcLF!E;n3l zxZZHH;n#-W8g4h-Y52Y2Zo~bC2MrG!9ydH`bZK;J^l0>I^l9{M^lS8Q3}_5$3~3B) z3~P*NjBNb6aed?I#%GN$8ecWOX?)lCSL6F8unE_s(L`t>Hj$brP1Gh@6SIlcq}^oR zWZ7ib15OCrgKdfnl3h7Zo1NRx9NV#o*4t$SM!v>t3d z)Oxt}Nb9lI6Rjs(Pqm(DJ=><$rqia^HmuF4&A82^&9u#|&7#e!&AQE|&92S9EvZe~ zHmz+(+s?Kj?Y(sr!vc-!T+t8Mq%9<)7bd(!r-?M2(Gc6_^LJEfi8&T8kj z>$GRIXSL_F=d~BM^V*Br`R&E+rR`%7@{tMh*6o6dL0_rP!v9{Elt4PrtbP&U*Cp%0;7iadUP z74!r0e0225;plUQ&p_vphZtT&9$(c1LcM-ctUF0rG7p;rlHLT09%c9Gw%cje&%caY`%d^Y7E23*eS6o*@ zS5jAcS7ujs*R-zhx@LBL-!->uUf2At1ziif7I!V}TGq9^Yh~A}uKQgNyB>Ev?RwGm zvg=jX>#jFl@4Ei#df)Y-8|=n)hjxc|M|MYb$9Bhc$9E@mCw3=yr*@}xr*~&|XLV2Q zp58s9dsg?H?z!Fby61N<=w8&lqS8~6ZLJ^K?os{$Gd^z8iX6aUHeKu%*TeKxxwiR2oHCwk0+p=xjw*x!0BRjSeJF_dh zwus$X+#c)+6{tcD>d=5jc+iAqw4xmy=tMVqFaigFFpddKVhYokg%>`|V*!h}z$JnR zA&eVDaEmBnh$De}JR-?*R0uLF*v58tu#4Rs<|vKi7{@umNltNwbM(?j zKNon;0D}xM%o|2{%P3=vGr@a4GRbGYGsO>ODofeQQK5=dtP+)}LY1nLN4@ITfQIC1 nMVs2uuJ&}S6P@Zz0Y#M1i&Acmo9pJM{g#pOXI}Wv|GNc01tO%C literal 0 HcmV?d00001 diff --git a/Extensions/XEP-0115/CoreDataStorage/XMPPCapabilities.xcdatamodel/layout b/Extensions/XEP-0115/CoreDataStorage/XMPPCapabilities.xcdatamodel/layout new file mode 100644 index 0000000000000000000000000000000000000000..e2f28f0543d029cceb33811e5e54fe610680ad06 GIT binary patch literal 7630 zcmb7J34Bw<);}}ZlP0;jS#A-~+7_rlAx+w*5fIvjLRkvZLR+?ywxJEANl8+m)XJcU zY$BqFq9U~5zI^V2em+FST@;EN;EsZP&+Q?Pr=Rkjdy_UTukiV9e?M|(?#!7v|8vg& zoEfSc1O7|O*B%O?hV(}`?0$$JsN7{ zfvnp8IJX8=pav`0K!9X?PlYRB4CKRDm<**b6RMyZ=EDLALKs%Tjj$Tlz*^V{x4|9I z4m)8N+z0o=!|-45H+ThJhePl&9EWe=d-wrqE3pxQB$Iw5g`|@K zWH8AgBgj=GpNu0@Nhzryv&dXhOPa`Hat+x;ZY6h-yUD%e0rDVuh&)c7CeM)P$=}E; zwj;H&dSeQX6&;-88s529Qhq(M5X+X(|; zAPj;Gre+yzAWJ(6gJB2^g-jR*!@-Utv%rDtJ0Tl#AQxPa2X43$M!-lIg*)Qsb}d#E z@YY4DIIhA|=Jf|~Z$Wf1(c3CK1)-*3t+<)#LI0vAU$Li(Gvdy|-X*AKX|y@ui_EF; zjQ4qKCm|eTgSFTag~VHuQ1NR*D9RE)mNhpbl(=LZk5s=*9E-|zKYnZFH+GtyA3L! zva5wJQ=~ADO`Zj_5!G(nZihKA7b<&pqB*;pB{p>)c+t)C!(nf88_a`B5uqBW1>X#` z==5R_Gc%LeR|oaruf#xzM8jxU##eFa&aAO+cTToD)9LoOGPASuotb%_JXdB>c79P& zw%e8O$j*wbvJe937A2*Vo1)(8fUi`N(GCltA*Phnjtvb#2i#=_y2r9dG9DxW# zp$Qhl65Q5OXoh8Iz81I!TH#tW<8olI0&46npC-Scgiihnrvn+&rtK zRGcBLi8-Uj!=b_BaEhDrZaaZBv-PE0Oimm(%9 zRh$W-i-x`ay837wn2OH!>=tLPy)#^AwDf@H0fgqE9vJXlI!hRctwZn#;y4~M;4v|V zk$}p);W}S5&Xp{Ur8D&ri0$8olADWREJYkNBmlBff^@LumPKUm_AM-S`{w)k3z21sG!s5=<45I9m}j z!d`e8{V&0RZLklG)xD=v#X+KqNaq((=nZ%i-h#K`9aQK&I1p1Pv6m5SEE~z{yYFRO z_q}w$VI&UGMsd~J;Y0XHVv}y2=p^ya7tSl6z^8Boj>0kc3_i!a@+EvF<`te3Df~)y z1L7W3U?=f`D+w@LZtAMY)DTE{~s8L#5|N`B#8MZ__=r5jS<~xWVm6S4EIlo z*YfOH4tG{TUS@8VyC^f;nO~Ucc6&UTg|6J9f2f=O;?oK-4{)|1S~11vW>tL`RaKnCO8KBjJb%H)oD*E<5#Q zhGCm0Zccp3WjVMxR~2W)tPyHL(w0~(5)a)uvA&d8=f${<^g$i^_RwLx7_!oH5fa0q zv~vuL6J3uUk?6X|#6T$~(C!H)z?z4pvka*okO(F#*CAE|j;y?)DOnDCS3N)mF=HFa zAXl(atl&d3gbWqggk+LoWH_;tEEq(bBwOMY^!Zvp(sF*VE`a3;GEHX>l0_&i6)Z*m zKp;^)VDb_>q|d2^HzKxUSUww=@b|0GS4l2$kv!riS0Y18LrWXIHNNR3rPIb0u+bcu z!u7KcbGcLO!*ZQj_B=;kmiX#&yYuYMoV*z2laXW;CUa?e8yN|e-35#VEc4mpP@?0*v!mB!1?mQ`ZA!e(Pwp{Rlrz~MEcGu#hsP{l;H_C$7Z%I@PTLgM zgo4Elc=TZt7y{3Vc??cqGg&1zp^6(`;h8$M+cI%45(}zsy@?u4wA>Q1lr*DSEog*p zgTzx0o`wp2fdH#O=@XBVR&p)Evz#!p0_R;vt|u$W4P+J0U5y~EMZQ}{){~pa2JG2L zL`bB)V!0(siPe7H-W^rkB|VlD_3s*qg&Ah)s92bxfHL3Gs00F5#-&Y-jiGQSeL81QoP=NLADSL6AdO=9J&j7>)UrZSk?fok1G zHj~@Q7IFu`6^E{#q}B9jx_9Wd^y2 zY{N-Ks6ZRJ2PzMf?I?N&l203HNBY@C?j!flK@Ab%K&Vb!Lqtg=Hu1faTiDfXG@HgL zS?P?@<_MyL2iGVfah{5?_o3{%1=phr?VNAqA+RI z?joLJqYLcu*xXI_NSUddV{}?+7kPoaNdB9=BytiauU>{oM-kCv)mv=8N#4TP zeuun^v3)>{Z8n$9!N8i`8CdgTftAyHVC6^gKvmro^+md^lL)VwiSLO>t0o^|K+R%R zM-l;bUO0V9-XupcoVo>5r{B&sq1Sfv*-jXGQ|>tV7L_|ePNH(BB;{&Z4JudNsoeaS za(TTg*W*H^SN-Pn7?M!2f&4?_4By2#;|w`Y&YsU1ood#KYW4`uPDLeqQ%YrXyH2@S z8+n`CDV2`ZRDsl9OqJ+q)4aht+o_RqqLf}UK%$Jt6Hg1C!$j|OOI1=Xp2M>85>*nlV3kCz)W#ZE zs7IAVlW8BUk}_yNk{v5?f>`3D^ej>=~f)jRGLO6(*cmd8viZZr^Tp3Dx&BW z|5>(|4gz#2rtEM$Wz*qO%68=2?Rj>WC)4h9Vaj&oWM$^%jdf(&3*1Hd?xI3_(O6GB zWm5;HZ0e-hG>1jmQg%%*o=;tvvNK4Cjv$NZD9Q7i(ACot`ErTq`CSf+;tJ7Wqipd7 zW0Dq87scDF-eR&TmP`IxRMM-Fax%y}bUN;>T-sYRQchZea$0)qjWp0XQZOyMNHEcP z^lDmtpx1lgPfpP?og@RVXep-X$jU?-eZ3>hc3naUBj-uFm^38KwEpq zuJrsxp}eirXC$Kqjl z(N20Ry^U_Bx3lZnN_GQV#hdDL_VcEiGfwgCg!r~97Vvk|dob;G5rfAs6}gH+#bm{7gD`JnP)<)g|cl}{_5RX(rWqkK>Kk@6emDdkxeQOQ)vssSpO z%B>oq8l_sGTB+KudR+CqYLDtg)k~_qs(q@rRqv_}s6J2~QXN&DRQ;g(U9DDY)k$iD zdW71eE>e$IPf$-%m#E9tmFijQIqE8Py?U{_RlQEVS-oHVw)$Q50rdy!L+ZookJO*2 zkEp*-*^Y>QnTo`XTyE{gwKW`uX~hzE!_cze&GC|B(I>{bTy)^t<(6=uhfT>3`9m)}PUz zHS{-R8Qg}c2A_c$ZZm8*v>A39?lU}Kc+~K?;R(Z2hGz`l8BQCiQExOFO-8fPW1M8H zF#3$w8E-P)X>2z>YkbT2iSdZ>3*%SD?~Esnr;L9${%HK!_z&Z+#xtCXGjLY!QZ9u{ z<%V#X+?8A*H-VeN)pB9(T5c7$np?}Q=QeN~xh>qI+#YTp_bRuadz1T&`-1z5`-c0L z`-%I5J7=OMohi*U%v5BmF!@XYQJ!>0a8)O@58)kFba%?V}+cv^B$~M|I&K9r*ZH>0D zEoy7Fwb)v1%WW%c8*CeGn{2n)UbDS!d&l;k?E~8(+hN;BwohzFY{zV$+rAT$1cSf{ zykHUf2z`bA!ezqc!Z5)uID~A$E7S<}!UCZ|2nmaXh|nY~5t@Y+!dhXyuvvIOcvRRc zyek|KJ{CR|jtZX%UkE3KQ^MbcABCTVGs5r5kgP~HB&R12NzO~oPcBNXNS>2i9Xp6& Q0Vq9mSM0a*-{ji=0|gp+egFUf literal 0 HcmV?d00001 diff --git a/Extensions/XEP-0115/CoreDataStorage/XMPPCapabilitiesCoreDataStorage.h b/Extensions/XEP-0115/CoreDataStorage/XMPPCapabilitiesCoreDataStorage.h new file mode 100644 index 0000000..23bcbef --- /dev/null +++ b/Extensions/XEP-0115/CoreDataStorage/XMPPCapabilitiesCoreDataStorage.h @@ -0,0 +1,54 @@ +#import +#import + +#import "XMPPCapabilities.h" +#import "XMPPCoreDataStorage.h" + +/** + * This class is an example implementation of XMPPCapabilitiesStorage using core data. + * You are free to substitute your own storage class. +**/ + +@interface XMPPCapabilitiesCoreDataStorage : XMPPCoreDataStorage +{ + /* Inherited protected variables from XMPPCoreDataStorage + + NSString *databaseFileName; + NSUInteger saveThreshold; + + dispatch_queue_t storageQueue; + + */ +} + +/** + * XEP-0115 provides a mechanism for hashing a list of capabilities. + * Clients then broadcast this hash instead of the entire list to save bandwidth. + * Because the hashing is standardized, it is safe to persistently store the linked hash & capabilities. + * + * For this reason, it is recommended you use this sharedInstance across all your xmppStreams. + * This way all streams can shared a knowledgebase concerning known hashes. + * + * All other aspects of capabilities handling (such as JID's, lookup failures, etc) are kept separate between streams. +**/ ++ (instancetype)sharedInstance; + + +/* Inherited from XMPPCoreDataStorage + * Please see the XMPPCoreDataStorage header file for extensive documentation. + +- (id)initWithDatabaseFilename:(NSString *)databaseFileName storeOptions:(NSDictionary *)storeOptions; +- (id)initWithInMemoryStore; + +@property (readonly) NSString *databaseFileName; + +@property (readwrite) NSUInteger saveThreshold; + +@property (readonly) NSManagedObjectModel *managedObjectModel; +@property (readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator; + +@property (readonly) NSManagedObjectContext *mainThreadManagedObjectContext; + +*/ + +@end diff --git a/Extensions/XEP-0115/CoreDataStorage/XMPPCapabilitiesCoreDataStorage.m b/Extensions/XEP-0115/CoreDataStorage/XMPPCapabilitiesCoreDataStorage.m new file mode 100644 index 0000000..3ebdd2e --- /dev/null +++ b/Extensions/XEP-0115/CoreDataStorage/XMPPCapabilitiesCoreDataStorage.m @@ -0,0 +1,587 @@ +#import "XMPPCapabilitiesCoreDataStorage.h" +#import "XMPPCapsCoreDataStorageObject.h" +#import "XMPPCapsResourceCoreDataStorageObject.h" +#import "XMPP.h" +#import "XMPPCoreDataStorageProtected.h" +#import "XMPPLogging.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +@implementation XMPPCapabilitiesCoreDataStorage + +static XMPPCapabilitiesCoreDataStorage *sharedInstance; + ++ (instancetype)sharedInstance +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + sharedInstance = [[XMPPCapabilitiesCoreDataStorage alloc] initWithDatabaseFilename:nil storeOptions:nil]; + }); + + return sharedInstance; +} + +- (void)commonInit +{ + XMPPLogTrace(); + [super commonInit]; + + autoRecreateDatabaseFile = YES; +} +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Setup +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)configureWithParent:(XMPPCapabilities *)aParent queue:(dispatch_queue_t)queue +{ + return [super configureWithParent:aParent queue:queue]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (XMPPCapsResourceCoreDataStorageObject *)resourceForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream +{ + NSAssert(dispatch_get_specific(storageQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace2(@"%@: %@ %@", THIS_FILE, THIS_METHOD, jid); + + if (jid == nil) return nil; + + NSEntityDescription *entity = [NSEntityDescription entityForName:@"XMPPCapsResourceCoreDataStorageObject" + inManagedObjectContext:[self managedObjectContext]]; + + NSPredicate *predicate; + if (stream == nil) + predicate = [NSPredicate predicateWithFormat:@"jidStr == %@", [jid full]]; + else + predicate = [NSPredicate predicateWithFormat:@"jidStr == %@ AND streamBareJidStr == %@", + [jid full], [[self myJIDForXMPPStream:stream] bare]]; + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:entity]; + [fetchRequest setPredicate:predicate]; + [fetchRequest setFetchLimit:1]; + + NSArray *results = [[self managedObjectContext] executeFetchRequest:fetchRequest error:nil]; + + XMPPCapsResourceCoreDataStorageObject *resource = [results lastObject]; + + XMPPLogVerbose(@"%@: %@ - %@", THIS_FILE, THIS_METHOD, resource); + return resource; +} + +- (XMPPCapsCoreDataStorageObject *)capsForHash:(NSString *)hash algorithm:(NSString *)hashAlg +{ + NSAssert(dispatch_get_specific(storageQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace2(@"%@: capsForHash:%@ algorithm:%@", THIS_FILE, hash, hashAlg); + + if (hash == nil) return nil; + if (hashAlg == nil) return nil; + + NSEntityDescription *entity = [NSEntityDescription entityForName:@"XMPPCapsCoreDataStorageObject" + inManagedObjectContext:[self managedObjectContext]]; + + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"hashStr == %@ AND hashAlgorithm == %@", + hash, hashAlg]; + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:entity]; + [fetchRequest setPredicate:predicate]; + [fetchRequest setFetchLimit:1]; + + NSArray *results = [[self managedObjectContext] executeFetchRequest:fetchRequest error:nil]; + + XMPPCapsCoreDataStorageObject *caps = [results lastObject]; + + XMPPLogVerbose(@"%@: %@ - %@", THIS_FILE, THIS_METHOD, caps); + return caps; +} + +- (void)_clearAllNonPersistentCapabilitiesForXMPPStream:(XMPPStream *)stream +{ + NSAssert(dispatch_get_specific(storageQueueTag), @"Invoked on incorrect queue"); + + NSEntityDescription *entity = [NSEntityDescription entityForName:@"XMPPCapsResourceCoreDataStorageObject" + inManagedObjectContext:[self managedObjectContext]]; + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:entity]; + [fetchRequest setFetchBatchSize:saveThreshold]; + + if (stream) + { + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"streamBareJidStr == %@", + [[self myJIDForXMPPStream:stream] bare]]; + + [fetchRequest setPredicate:predicate]; + } + + NSArray *results = [[self managedObjectContext] executeFetchRequest:fetchRequest error:nil]; + + NSUInteger unsavedCount = [self numberOfUnsavedChanges]; + + for (XMPPCapsResourceCoreDataStorageObject *resource in results) + { + NSString *hash = resource.hashStr; + NSString *hashAlg = resource.hashAlgorithm; + + BOOL nonPersistentCapabilities = ((hash == nil) || (hashAlg == nil)); + + if (nonPersistentCapabilities) + { + XMPPCapsCoreDataStorageObject *caps = resource.caps; + if (caps) + { + [[self managedObjectContext] deleteObject:caps]; + } + } + + [[self managedObjectContext] deleteObject:resource]; + + if (++unsavedCount >= saveThreshold) + { + [self save]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Overrides +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)didCreateManagedObjectContext +{ + // This method is overriden from the XMPPCoreDataStore superclass. + + [self _clearAllNonPersistentCapabilitiesForXMPPStream:nil]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Protocol Public API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)areCapabilitiesKnownForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + + __block BOOL result; + + [self executeBlock:^{ + + XMPPCapsResourceCoreDataStorageObject *resource = [self resourceForJID:jid xmppStream:stream]; + result = (resource.caps != nil); + + }]; + + return result; +} + +- (NSXMLElement *)capabilitiesForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + + return [self capabilitiesForJID:jid ext:nil xmppStream:stream]; +} + +- (NSXMLElement *)capabilitiesForJID:(XMPPJID *)jid ext:(NSString **)extPtr xmppStream:(XMPPStream *)stream +{ + // By design this method should not be invoked from the storageQueue. + NSAssert(!dispatch_get_specific(storageQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + __block NSXMLElement *result = nil; + __block NSString *ext = nil; + + [self executeBlock:^{ + + XMPPCapsResourceCoreDataStorageObject *resource = [self resourceForJID:jid xmppStream:stream]; + + if (resource) + { + result = [[resource caps] capabilities]; + ext = [resource ext]; + } + + }]; + + if (extPtr) + *extPtr = ext; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Protocol Private API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)setCapabilitiesNode:(NSString *)node + ver:(NSString *)ver + ext:(NSString *)ext + hash:(NSString *)hash + algorithm:(NSString *)hashAlg + forJID:(XMPPJID *)jid + xmppStream:(XMPPStream *)stream + andGetNewCapabilities:(NSXMLElement **)newCapabilitiesPtr +{ + + XMPPLogTrace(); + + __block BOOL result = NO; + __block NSXMLElement *newCapabilities = nil; + + [self executeBlock:^{ + + BOOL hashChange = NO; + + XMPPCapsResourceCoreDataStorageObject *resource = [self resourceForJID:jid xmppStream:stream]; + if (resource) + { + resource.node = node; + resource.ver = ver; + resource.ext = ext; + + if (![hash isEqual:[resource hashStr]]) + { + hashChange = YES; + resource.hashStr = hash; + } + + if (![hashAlg isEqual:[resource hashAlgorithm]]) + { + hashChange = YES; + resource.hashAlgorithm = hashAlg; + } + } + else + { + resource = [NSEntityDescription insertNewObjectForEntityForName:@"XMPPCapsResourceCoreDataStorageObject" + inManagedObjectContext:[self managedObjectContext]]; + + resource.jidStr = [jid full]; + resource.streamBareJidStr = [[self myJIDForXMPPStream:stream] bare]; + + resource.node = node; + resource.ver = ver; + resource.ext = ext; + + resource.hashStr = hash; + resource.hashAlgorithm = hashAlg; + + hashChange = ((hash != nil) || (hashAlg != nil)); + } + + if (hashChange) + { + resource.caps = [self capsForHash:hash algorithm:hashAlg]; + + newCapabilities = resource.caps.capabilities; + } + + // Return whether or not the capabilities are known for the given jid + + result = (resource.caps != nil); + + }]; + + + if (newCapabilitiesPtr) + *newCapabilitiesPtr = newCapabilities; + + return result; +} + +- (BOOL)getCapabilitiesHash:(NSString **)hashPtr + algorithm:(NSString **)hashAlgPtr + forJID:(XMPPJID *)jid + xmppStream:(XMPPStream *)stream +{ + // By design this method should not be invoked from the storageQueue. + NSAssert(!dispatch_get_specific(storageQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + __block BOOL result = NO; + __block NSString *hash = nil; + __block NSString *hashAlg = nil; + + [self executeBlock:^{ + + XMPPCapsResourceCoreDataStorageObject *resource = [self resourceForJID:jid xmppStream:stream]; + if (resource) + { + hash = resource.hashStr; + hashAlg = resource.hashAlgorithm; + + result = (hash && hashAlg); + } + else + { + hash = nil; + hashAlg = nil; + + result = NO; + } + + }]; + + + if (hashPtr) + *hashPtr = hash; + + if (hashAlgPtr) + *hashAlgPtr = hashAlg; + + return result; +} + +- (void)clearCapabilitiesHashAndAlgorithmForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + + [self scheduleBlock:^{ + + XMPPCapsResourceCoreDataStorageObject *resource = [self resourceForJID:jid xmppStream:stream]; + if (resource) + { + BOOL clearCaps = NO; + + NSString *hash = resource.hashStr; + NSString *hashAlg = resource.hashAlgorithm; + + if (hash && hashAlg) + { + clearCaps = YES; + } + + resource.hashStr = nil; + resource.hashAlgorithm = nil; + + if (clearCaps) + { + resource.caps = nil; + } + } + + }]; +} + +- (void)getCapabilitiesKnown:(BOOL *)areCapabilitiesKnownPtr + failed:(BOOL *)haveFailedFetchingBeforePtr + node:(NSString **)nodePtr + ver:(NSString **)verPtr + ext:(NSString **)extPtr + hash:(NSString **)hashPtr + algorithm:(NSString **)hashAlgPtr + forJID:(XMPPJID *)jid + xmppStream:(XMPPStream *)stream +{ + // By design this method should not be invoked from the storageQueue. + NSAssert(!dispatch_get_specific(storageQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + __block BOOL areCapabilitiesKnown = NO; + __block BOOL haveFailedFetchingBefore = NO; + __block NSString *node = nil; + __block NSString *ver = nil; + __block NSString *ext = nil; + __block NSString *hash = nil; + __block NSString *hashAlg = nil; + + [self executeBlock:^{ + + XMPPCapsResourceCoreDataStorageObject *resource = [self resourceForJID:jid xmppStream:stream]; + + if (resource == nil) + { + // We don't know anything about the given jid + + areCapabilitiesKnown = NO; + haveFailedFetchingBefore = NO; + + node = nil; + ver = nil; + ext = nil; + hash = nil; + hashAlg = nil; + } + else + { + areCapabilitiesKnown = (resource.caps != nil); + haveFailedFetchingBefore = resource.haveFailed; + + node = resource.node; + ver = resource.ver; + ext = resource.ext; + hash = resource.hashStr; + hashAlg = resource.hashAlgorithm; + } + + }]; + + if (areCapabilitiesKnownPtr) *areCapabilitiesKnownPtr = areCapabilitiesKnown; + if (haveFailedFetchingBeforePtr) *haveFailedFetchingBeforePtr = haveFailedFetchingBefore; + + if (nodePtr) *nodePtr = node; + if (verPtr) *verPtr = ver; + if (extPtr) *extPtr = ext; + if (hashPtr) *hashPtr = hash; + if (hashAlgPtr) *hashAlgPtr = hashAlg; +} + +- (void)setCapabilities:(NSXMLElement *)capabilities forHash:(NSString *)hash algorithm:(NSString *)hashAlg +{ + XMPPLogTrace(); + + if (hash == nil) return; + if (hashAlg == nil) return; + + [self scheduleBlock:^{ + + XMPPCapsCoreDataStorageObject *caps = [self capsForHash:hash algorithm:hashAlg]; + if (caps) + { + caps.capabilities = capabilities; + } + else + { + caps = [NSEntityDescription insertNewObjectForEntityForName:@"XMPPCapsCoreDataStorageObject" + inManagedObjectContext:[self managedObjectContext]]; + caps.hashStr = hash; + caps.hashAlgorithm = hashAlg; + + caps.capabilities = capabilities; + } + + NSEntityDescription *entity = [NSEntityDescription entityForName:@"XMPPCapsResourceCoreDataStorageObject" + inManagedObjectContext:[self managedObjectContext]]; + + NSPredicate *predicate; + predicate = [NSPredicate predicateWithFormat:@"hashStr == %@ AND hashAlgorithm == %@", hash, hashAlg]; + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:entity]; + [fetchRequest setPredicate:predicate]; + [fetchRequest setFetchBatchSize:saveThreshold]; + + NSArray *results = [[self managedObjectContext] executeFetchRequest:fetchRequest error:nil]; + + NSUInteger unsavedCount = [self numberOfUnsavedChanges]; + + for (XMPPCapsResourceCoreDataStorageObject *resource in results) + { + resource.caps = caps; + + if (++unsavedCount >= saveThreshold) + { + [self save]; + } + } + + }]; +} + +- (void)setCapabilities:(NSXMLElement *)capabilities forJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream +{ + // By design this method should not be invoked from the storageQueue. + NSAssert(!dispatch_get_specific(storageQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + if (jid == nil) return; + + [self scheduleBlock:^{ + + XMPPCapsCoreDataStorageObject *caps; + caps = [NSEntityDescription insertNewObjectForEntityForName:@"XMPPCapsCoreDataStorageObject" + inManagedObjectContext:[self managedObjectContext]]; + caps.capabilities = capabilities; + + XMPPCapsResourceCoreDataStorageObject *resource = [self resourceForJID:jid xmppStream:stream]; + + if (resource == nil) + { + resource = [NSEntityDescription insertNewObjectForEntityForName:@"XMPPCapsResourceCoreDataStorageObject" + inManagedObjectContext:[self managedObjectContext]]; + resource.jidStr = [jid full]; + resource.streamBareJidStr = [[self myJIDForXMPPStream:stream] bare]; + } + + resource.caps = caps; + + }]; +} + +- (void)setCapabilitiesFetchFailedForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + + [self scheduleBlock:^{ + + XMPPCapsResourceCoreDataStorageObject *resource = [self resourceForJID:jid xmppStream:stream]; + resource.haveFailed = YES; + + }]; +} + +- (void)clearAllNonPersistentCapabilitiesForXMPPStream:(XMPPStream *)stream +{ + // This method is called for the protocol, + // but is also called when we first load the database file from disk. + + XMPPLogTrace(); + + [self scheduleBlock:^{ + + [self _clearAllNonPersistentCapabilitiesForXMPPStream:stream]; + + }]; +} + +- (void)clearNonPersistentCapabilitiesForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream +{ + XMPPLogTrace(); + + [self scheduleBlock:^{ + + XMPPCapsResourceCoreDataStorageObject *resource = [self resourceForJID:jid xmppStream:stream]; + + if (resource != nil) + { + NSString *hash = resource.hashStr; + NSString *hashAlg = resource.hashAlgorithm; + + if (hash && hashAlg) + { + // The associated capabilities are persistent + } + else + { + XMPPCapsCoreDataStorageObject *caps = resource.caps; + if (caps) + { + [[self managedObjectContext] deleteObject:caps]; + } + } + + [[self managedObjectContext] deleteObject:resource]; + } + + }]; +} + +@end diff --git a/Extensions/XEP-0115/CoreDataStorage/XMPPCapsCoreDataStorageObject.h b/Extensions/XEP-0115/CoreDataStorage/XMPPCapsCoreDataStorageObject.h new file mode 100644 index 0000000..4e72575 --- /dev/null +++ b/Extensions/XEP-0115/CoreDataStorage/XMPPCapsCoreDataStorageObject.h @@ -0,0 +1,30 @@ +#import + +#if TARGET_OS_IPHONE + #import "DDXML.h" +#endif + +@class XMPPCapsResourceCoreDataStorageObject; + + +@interface XMPPCapsCoreDataStorageObject : NSManagedObject + +@property (nonatomic, strong) NSXMLElement *capabilities; + +@property (nonatomic, strong) NSString * hashStr; +@property (nonatomic, strong) NSString * hashAlgorithm; +@property (nonatomic, strong) NSString * capabilitiesStr; + +@property (nonatomic, strong) NSSet * resources; + +@end + + +@interface XMPPCapsCoreDataStorageObject (CoreDataGeneratedAccessors) + +- (void)addResourcesObject:(XMPPCapsResourceCoreDataStorageObject *)value; +- (void)removeResourcesObject:(XMPPCapsResourceCoreDataStorageObject *)value; +- (void)addResources:(NSSet *)value; +- (void)removeResources:(NSSet *)value; + +@end diff --git a/Extensions/XEP-0115/CoreDataStorage/XMPPCapsCoreDataStorageObject.m b/Extensions/XEP-0115/CoreDataStorage/XMPPCapsCoreDataStorageObject.m new file mode 100644 index 0000000..27b97ee --- /dev/null +++ b/Extensions/XEP-0115/CoreDataStorage/XMPPCapsCoreDataStorageObject.m @@ -0,0 +1,30 @@ +#import "XMPPCapsCoreDataStorageObject.h" +#import "XMPPCapsResourceCoreDataStorageObject.h" +#import "XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation XMPPCapsCoreDataStorageObject + +@dynamic capabilities; + +@dynamic hashStr; +@dynamic hashAlgorithm; +@dynamic capabilitiesStr; + +@dynamic resources; + +- (NSXMLElement *)capabilities +{ + return [[NSXMLElement alloc] initWithXMLString:[self capabilitiesStr] error:nil]; +} + +- (void)setCapabilities:(NSXMLElement *)caps +{ + self.capabilitiesStr = [caps compactXMLString]; +} + +@end diff --git a/Extensions/XEP-0115/CoreDataStorage/XMPPCapsResourceCoreDataStorageObject.h b/Extensions/XEP-0115/CoreDataStorage/XMPPCapsResourceCoreDataStorageObject.h new file mode 100644 index 0000000..edd2541 --- /dev/null +++ b/Extensions/XEP-0115/CoreDataStorage/XMPPCapsResourceCoreDataStorageObject.h @@ -0,0 +1,23 @@ +#import + +@class XMPPCapsCoreDataStorageObject; + + +@interface XMPPCapsResourceCoreDataStorageObject : NSManagedObject + +@property (nonatomic, strong) NSString * jidStr; +@property (nonatomic, strong) NSString * streamBareJidStr; + +@property (nonatomic, assign) BOOL haveFailed; +@property (nonatomic, strong) NSNumber * failed; + +@property (nonatomic, strong) NSString * node; +@property (nonatomic, strong) NSString * ver; +@property (nonatomic, strong) NSString * ext; + +@property (nonatomic, strong) NSString * hashStr; +@property (nonatomic, strong) NSString * hashAlgorithm; + +@property (nonatomic, strong) XMPPCapsCoreDataStorageObject * caps; + +@end diff --git a/Extensions/XEP-0115/CoreDataStorage/XMPPCapsResourceCoreDataStorageObject.m b/Extensions/XEP-0115/CoreDataStorage/XMPPCapsResourceCoreDataStorageObject.m new file mode 100644 index 0000000..183345d --- /dev/null +++ b/Extensions/XEP-0115/CoreDataStorage/XMPPCapsResourceCoreDataStorageObject.m @@ -0,0 +1,36 @@ +#import "XMPPCapsResourceCoreDataStorageObject.h" +#import "XMPPCapsCoreDataStorageObject.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation XMPPCapsResourceCoreDataStorageObject + +@dynamic jidStr; +@dynamic streamBareJidStr; + +@dynamic haveFailed; +@dynamic failed; + +@dynamic node; +@dynamic ver; +@dynamic ext; + +@dynamic hashStr; +@dynamic hashAlgorithm; + +@dynamic caps; + +- (BOOL)haveFailed +{ + return [[self failed] boolValue]; +} + +- (void)setHaveFailed:(BOOL)flag +{ + self.failed = @(flag); +} + +@end diff --git a/Extensions/XEP-0115/XMPPCapabilities.h b/Extensions/XEP-0115/XMPPCapabilities.h new file mode 100644 index 0000000..1be070a --- /dev/null +++ b/Extensions/XEP-0115/XMPPCapabilities.h @@ -0,0 +1,380 @@ +#import +#import "XMPP.h" + +#define _XMPP_CAPABILITIES_H + +@protocol XMPPCapabilitiesStorage; + +/** + * This class provides support for capabilities discovery. + * + * It collects our capabilities and publishes them according to the XEP by: + * - Injecting the element into outgoing presence stanzas + * - Responding to incoming disco#info queries + * + * It also collects the capabilities of available resources, + * provides a mechanism to persistently store XEP-0115 hased caps, + * and makes available a simple API to query (disco#info) a resource or server. +**/ +@interface XMPPCapabilities : XMPPModule +{ + __strong id xmppCapabilitiesStorage; + + NSString *myCapabilitiesNode; + + NSXMLElement *myCapabilitiesQuery; // Full list of capabilites + NSXMLElement *myCapabilitiesC; // Hashed element + BOOL collectingMyCapabilities; + + NSMutableSet *discoRequestJidSet; + NSMutableDictionary *discoRequestHashDict; + NSMutableDictionary *discoTimerJidDict; + + BOOL autoFetchHashedCapabilities; + BOOL autoFetchNonHashedCapabilities; + BOOL autoFetchMyServerCapabilities; + + NSTimeInterval capabilitiesRequestTimeout; + + NSMutableSet *timers; +} + +- (id)initWithCapabilitiesStorage:(id )storage; +- (id)initWithCapabilitiesStorage:(id )storage dispatchQueue:(dispatch_queue_t)queue; + +@property (nonatomic, strong, readonly) id xmppCapabilitiesStorage; + +/** + * Defines the node attribute in a element qualified by the 'http://jabber.org/protocol/caps' namespace. + * + * It is RECOMMENDED for the value of the 'node' attribute to be an HTTP URL + * at which a user could find further information about the software product, + * such as "http://github.com/robbiehanson/XMPPFramework" + * + * This MUST NOT be nil + * + * The default value is http://github.com/robbiehanson/XMPPFramework +**/ + +@property (nonatomic, copy) NSString *myCapabilitiesNode; + +/** + * Defines fetching behavior for entities using the XEP-0115 standard. + * + * XEP-0115 defines a technique for hashing capabilities (disco info responses), + * and broadcasting them within a presence element. + * Due to the standardized hashing technique, capabilities associated with a hash may be persisted indefinitely. + * + * The end result is that capabilities need to be fetched less often + * since they are already known due to the caching of responses. + * + * The default value is YES. +**/ +@property (assign) BOOL autoFetchHashedCapabilities; + +/** + * Defines fetching behavior for entities NOT using the XEP-0115 standard. + * + * Because the capabilities are not associated with a standardized hash, + * it is not possible to cache the capabilities between sessions. + * + * The default value is NO. + * + * It is recommended you leave this value set to NO unless you + * know that you'll need the capabilities of every resource, + * and that fetching of the capabilities cannot be delayed. + * + * You may always fetch the capabilities (if/when needed) via the fetchCapabilitiesForJID method. +**/ +@property (assign) BOOL autoFetchNonHashedCapabilities; + +/** + * Auto fetch the capabilities of the server upon authentication. + * This uses the non hashed approach outlined in XEP-0030: Service Discovery. + * + * The default value is NO. +**/ + +@property (assign) BOOL autoFetchMyServerCapabilities; + +/** + * Manually fetch the capabilities for the given jid. + * + * The jid must be a full jid (user@domain/resource) or a domain JID (domain without user or resource). + * You would pass a full jid if you wanted to know the capabilities of a particular user's resource. + * You would pass a domain jid if you wanted to know the capabilities of a particular server. + * + * If there is an existing disco request associated with the given jid, this method does nothing. + * + * When the capabilities are received, + * the xmppCapabilities:didDiscoverCapabilities:forJID: delegate method is invoked. +**/ +- (void)fetchCapabilitiesForJID:(XMPPJID *)jid; + +/** + * This module automatically collects my capabilities. + * See the xmppCapabilities:collectingMyCapabilities: delegate method. + * + * The design of XEP-115 is such that capabilites are expected to remain rather static. + * However, if the capabilities change, this method may be used to perform a manual update. +**/ +- (void)recollectMyCapabilities; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPCapabilitiesStorage +@required + +// +// +// -- PUBLIC METHODS -- +// +// + +/** + * Returns whether or not we know the capabilities for a given jid. + * + * The stream parameter is optional. + * If given, the jid must have been registered via the given stream. + * Otherwise it will match the given jid from any stream this storage instance is managing. +**/ +- (BOOL)areCapabilitiesKnownForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream; + +/** + * Returns the capabilities for the given jid. + * The returned element is the element response to a disco#info request. + * + * The stream parameter is optional. + * If given, the jid must have been registered via the given stream. + * Otherwise it will match the given jid from any stream this storage instance is managing. +**/ +- (NSXMLElement *)capabilitiesForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream; + +/** + * Returns the capabilities for the given jid. + * The returned element is the element response to a disco#info request. + * + * The given jid should be a full jid (user@domain/resource) or a domin JID (domain without user or resource). + * + * If the jid has broadcast capabilities via the legacy format of XEP-0115, + * the extension list may optionally be retrieved via the ext parameter. + * + * For example, the jid may send a presence element like: + * + * + * + * + * + * In the above example, the ext string would be set to "rdserver rdclient avcap". + * + * You may pass nil for extPtr if you don't care about the legacy attributes, + * or you could simply use the capabilitiesForJID: method above. + * + * The stream parameter is optional. + * If given, the jid must have been registered via the given stream. + * Otherwise it will match the given jid from any stream this storage instance is managing. +**/ +- (NSXMLElement *)capabilitiesForJID:(XMPPJID *)jid ext:(NSString **)extPtr xmppStream:(XMPPStream *)stream; + +// +// +// -- PRIVATE METHODS -- +// +// These methods are designed to be used ONLY by the XMPPCapabilities class. +// +// + +/** + * Configures the capabilities storage class, passing it's parent and parent's dispatch queue. + * + * This method is called by the init methods of the XMPPCapabilities class. + * This method is designed to inform the storage class of it's parent + * and of the dispatch queue the parent will be operating on. + * + * A storage class may choose to operate on the same queue as it's parent, + * as the majority of the time it will be getting called by the parent. + * If both are operating on the same queue, the combination may run faster. + * + * Some storage classes support multiple xmppStreams, + * and may choose to operate on their own internal queue. + * + * This method should return YES if it was configured properly. + * It should return NO only if configuration failed. + * For example, a storage class designed to be used only with a single xmppStream is being added to a second stream. + * The XMPPCapabilites class is configured to ignore the passed + * storage class in it's init method if this method returns NO. +**/ +- (BOOL)configureWithParent:(XMPPCapabilities *)aParent queue:(dispatch_queue_t)queue; + +/** + * Sets metadata for the given jid. + * + * This method should return: + * - YES if the capabilities for the given jid are known. + * - NO if the capabilities for the given jid are NOT known. + * + * If the hash and algorithm are given, and an associated set of capabilities matches the hash/algorithm, + * this method should link the jid to the capabilities and return YES. + * + * If the linked set of capabilities was not previously linked to the jid, + * the newCapabilities parameter shoud be filled out. + * + * This method may be called multiple times for a given jid with the same information. + * If this method sets the newCapabilitiesPtr parameter, + * the XMPPCapabilities module will invoke the xmppCapabilities:didDiscoverCapabilities:forJID: delegate method. + * This delegate method is designed to be invoked only when the capabilities for the given JID have changed. + * That is, the capabilities for the jid have been discovered for the first time (jid just signed in) + * or the capabilities for the given jid have changed (jid broadcast new capabilities). +**/ +- (BOOL)setCapabilitiesNode:(NSString *)node + ver:(NSString *)ver + ext:(NSString *)ext + hash:(NSString *)hash + algorithm:(NSString *)hashAlg + forJID:(XMPPJID *)jid + xmppStream:(XMPPStream *)stream + andGetNewCapabilities:(NSXMLElement **)newCapabilitiesPtr; + +/** + * Fetches the associated capabilities hash for a given jid. + * + * If the jid is not associated with a capabilities hash, this method should return NO. + * Otherwise it should return YES, and set the corresponding variables. +**/ +- (BOOL)getCapabilitiesHash:(NSString **)hashPtr + algorithm:(NSString **)hashAlgPtr + forJID:(XMPPJID *)jid + xmppStream:(XMPPStream *)stream; + +/** + * Clears any associated hash from a jid. + * If the jid is linked to a set of capabilities, it should be unlinked. + * + * This method should not clear the actual capabilities information itself. + * It should simply unlink the connection between the jid and the capabilities. +**/ +- (void)clearCapabilitiesHashAndAlgorithmForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream; + +/** + * Gets the metadata for the given jid. + * + * If the capabilities are known, the areCapabilitiesKnown boolean should be set to YES. +**/ +- (void)getCapabilitiesKnown:(BOOL *)areCapabilitiesKnownPtr + failed:(BOOL *)haveFailedFetchingBeforePtr + node:(NSString **)nodePtr + ver:(NSString **)verPtr + ext:(NSString **)extPtr + hash:(NSString **)hashPtr + algorithm:(NSString **)hashAlgPtr + forJID:(XMPPJID *)jid + xmppStream:(XMPPStream *)stream; + +/** + * Sets the capabilities associated with a given hash. + * + * Since the capabilities are linked to a hash, these capabilities (and associated hash) + * should be persisted to disk and persisted between multiple sessions/streams. + * + * It is the responsibility of the storage implementation to link the + * associated jids (those with the given hash) to the given set of capabilities. + * + * Implementation Note: + * + * If we receive multiple simultaneous presence elements from + * multiple jids all broadcasting the same capabilities hash: + * + * - A single disco request will be sent to one of the jids. + * - When the response comes back, the setCapabilities:forHash:algorithm: method will be invoked. + * + * The setCapabilities:forJID: method will NOT be invoked for each corresponding jid. + * This is by design to allow the storage implementation to optimize itself. +**/ +- (void)setCapabilities:(NSXMLElement *)caps forHash:(NSString *)hash algorithm:(NSString *)hashAlg; + +/** + * Sets the capabilities for a given jid. + * + * The jid is guaranteed NOT to be associated with a capabilities hash. + * + * Since the capabilities are NOT linked to a hash, + * these capabilities should not be persisted between multiple sessions/streams. + * See the various clear methods below. +**/ +- (void)setCapabilities:(NSXMLElement *)caps forJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream; + +/** + * Marks the disco fetch request as failed so we know not to bother trying again. + * + * This is temporary metadata associated with the jid. + * It should be cleared when we go unavailable or offline, or if the given jid goes unavailable. + * See the various clear methods below. +**/ +- (void)setCapabilitiesFetchFailedForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream; + +/** + * This method is called when we go unavailable or offline. + * + * This method should clear all metadata (node, ver, ext, hash, algorithm, failed) from all jids in the roster. + * All jids should be unlinked from associated capabilities. + * + * If the associated capabilities are persistent, they should not be cleared. + * That is, if the associated capabilities are associated with a hash, they should be persisted. + * + * Non persistent capabilities (those not associated with a hash) + * should be cleared at this point as they will no longer be linked to any users. +**/ +- (void)clearAllNonPersistentCapabilitiesForXMPPStream:(XMPPStream *)stream; + +/** + * This method is called when the given jid goes unavailable. + * + * This method should clear all metadata (node, ver, ext, hash ,algorithm, failed) from the given jid. + * The jid should be unlinked from associated capabilities. + * + * If the associated capabilities are persistent, they should not be cleared. + * That is, if the associated capabilities are associated with a hash, they should be persisted. + * + * Non persistent capabilities (those not associated with a hash) should be cleared. +**/ +- (void)clearNonPersistentCapabilitiesForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPCapabilitiesDelegate +@optional + +/** + * Use this delegate method to add specific capabilities. + * This method in invoked automatically when the stream is connected for the first time, + * or if the module detects an outgoing presence element and my capabilities haven't been collected yet + * + * The design of XEP-115 is such that capabilites are expected to remain rather static. + * However, if the capabilities change, the recollectMyCapabilities method may be used to perform a manual update. +**/ +- (void)xmppCapabilities:(XMPPCapabilities *)sender collectingMyCapabilities:(NSXMLElement *)query; + + +/** + * Use this delegate method to return the feature you want to have in your capabilities e.g. @[@"urn:xmpp:archive"] + * Duplicate features are automatically discarded + * For more control over your capablities use xmppCapabilities:collectingMyCapabilities: +**/ +- (NSArray *)myFeaturesForXMPPCapabilities:(XMPPCapabilities *)sender; + +/** + * Invoked when capabilities have been discovered for an available JID. + * + * The caps element is the element response to a disco#info request. +**/ +- (void)xmppCapabilities:(XMPPCapabilities *)sender didDiscoverCapabilities:(NSXMLElement *)caps forJID:(XMPPJID *)jid; + +@end diff --git a/Extensions/XEP-0115/XMPPCapabilities.m b/Extensions/XEP-0115/XMPPCapabilities.m new file mode 100644 index 0000000..8917080 --- /dev/null +++ b/Extensions/XEP-0115/XMPPCapabilities.m @@ -0,0 +1,1781 @@ +#import "XMPP.h" +#import "XMPPLogging.h" +#import "XMPPCapabilities.h" +#import "NSData+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +/** + * Defines the timeout for a capabilities request. + * + * There are two reasons to have a timeout: + * - To prevent the discoRequest variables from growing indefinitely if responses are not received. + * - If a request is sent to a jid broadcasting a capabilities hash, and it does not respond within the timeout, + * we can then send a request to a different jid broadcasting the same capabilities hash. + * + * Remember, if multiple jids all broadcast the same capabilities hash, + * we only (initially) send a disco request to the first jid. + * This is an obvious optimization to remove unnecessary traffic and cpu load. + * + * However, if that jid doesn't respond within a sensible time period, + * we should move on to the next jid in the list. +**/ +#define CAPABILITIES_REQUEST_TIMEOUT 30.0 // seconds + +/** + * Define various xmlns values. +**/ +#define XMLNS_DISCO_INFO @"http://jabber.org/protocol/disco#info" +#define XMLNS_CAPS @"http://jabber.org/protocol/caps" + +/** + * Application identifier. + * According to the XEP it is RECOMMENDED for the value of the 'node' attribute to be an HTTP URL. +**/ +#ifndef DISCO_NODE + #define DISCO_NODE @"https://github.com/robbiehanson/XMPPFramework" +#endif + +@interface GCDTimerWrapper : NSObject +{ + dispatch_source_t timer; +} + +- (id)initWithDispatchTimer:(dispatch_source_t)aTimer; +- (void)cancel; + +@end + +@interface XMPPCapabilities (PrivateAPI) + +- (void)continueCollectMyCapabilities:(NSXMLElement *)query; + +- (void)maybeQueryNextJidWithHashKey:(NSString *)key dueToHashMismatch:(BOOL)hashMismatch; + +- (void)setupTimeoutForDiscoRequestFromJID:(XMPPJID *)jid; +- (void)setupTimeoutForDiscoRequestFromJID:(XMPPJID *)jid withHashKey:(NSString *)key; + +- (void)cancelTimeoutForDiscoRequestFromJID:(XMPPJID *)jid; + +- (void)processTimeoutWithHashKey:(NSString *)key; +- (void)processTimeoutWithJID:(XMPPJID *)jid; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPCapabilities + +@dynamic xmppCapabilitiesStorage; +@dynamic autoFetchHashedCapabilities; +@dynamic autoFetchNonHashedCapabilities; +@dynamic autoFetchMyServerCapabilities; + +- (id)init +{ + // This will cause a crash - it's designed to. + // Only the init methods listed in XMPPCapabilities.h are supported. + + return [self initWithCapabilitiesStorage:nil dispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + // This will cause a crash - it's designed to. + // Only the init methods listed in XMPPCapabilities.h are supported. + + return [self initWithCapabilitiesStorage:nil dispatchQueue:queue]; +} + +- (id)initWithCapabilitiesStorage:(id )storage +{ + return [self initWithCapabilitiesStorage:storage dispatchQueue:NULL]; +} + +- (id)initWithCapabilitiesStorage:(id )storage dispatchQueue:(dispatch_queue_t)queue +{ + NSParameterAssert(storage != nil); + + if ((self = [super initWithDispatchQueue:queue])) + { + if ([storage configureWithParent:self queue:moduleQueue]) + { + xmppCapabilitiesStorage = storage; + } + else + { + XMPPLogError(@"%@: %@ - Unable to configure storage!", THIS_FILE, THIS_METHOD); + } + + myCapabilitiesNode = DISCO_NODE; + + // discoRequestJidSet: + // + // A set which contains every JID for which a current disco request applies. + // Note that one disco request may satisfy multiple jids in this set. + // This is the case if multiple jids broadcast the same capabilities hash. + // When this happens we send a single disco request to one of the jids, + // but every single jid with that hash is included in this set. + // This allows us to quickly and easily see if there is an outstanding disco request for a jid. + // + // discoRequestHashDict: + // + // A dictionary which tells us about disco requests that have been sent concerning hashed capabilities. + // It maps from hash (key=hash+hashAlgorithm) to an array of jids that use this hash. + // + // discoTimerJidDict: + // + // A dictionary that contains all the timers for timing out disco requests. + // It maps from jid to associated timer. + + discoRequestJidSet = [[NSMutableSet alloc] init]; + discoRequestHashDict = [[NSMutableDictionary alloc] init]; + discoTimerJidDict = [[NSMutableDictionary alloc] init]; + + autoFetchHashedCapabilities = YES; + autoFetchNonHashedCapabilities = NO; + autoFetchMyServerCapabilities = NO; + } + return self; +} + +- (void)dealloc +{ + for (GCDTimerWrapper *timerWrapper in discoTimerJidDict) + { + [timerWrapper cancel]; + } + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id )xmppCapabilitiesStorage +{ + return xmppCapabilitiesStorage; +} + +- (NSString *)myCapabilitiesNode +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return myCapabilitiesNode; + } + else + { + __block NSString *result; + + dispatch_sync(moduleQueue, ^{ + result = myCapabilitiesNode; + }); + + return result; + } +} + +- (void)setMyCapabilitiesNode:(NSString *)flag +{ + NSAssert([flag length], @"myCapabilitiesNode MUST NOT be nil"); + + dispatch_block_t block = ^{ + myCapabilitiesNode = flag; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (BOOL)autoFetchHashedCapabilities +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = autoFetchHashedCapabilities; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setAutoFetchHashedCapabilities:(BOOL)flag +{ + dispatch_block_t block = ^{ + autoFetchHashedCapabilities = flag; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (BOOL)autoFetchNonHashedCapabilities +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = autoFetchNonHashedCapabilities; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setAutoFetchNonHashedCapabilities:(BOOL)flag +{ + dispatch_block_t block = ^{ + autoFetchNonHashedCapabilities = flag; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (BOOL)autoFetchMyServerCapabilities +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = autoFetchMyServerCapabilities; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setAutoFetchMyServerCapabilities:(BOOL)flag +{ + dispatch_block_t block = ^{ + autoFetchMyServerCapabilities = flag; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Hashing +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +static NSString* encodeLt(NSString *str) +{ + // From the RFC: + // + // If the string "<" appears in any of the hash values, + // then that value MUST NOT convert it to "<" because + // completing such a conversion would open the protocol to trivial attacks. + // + // All of the XML libraries perform this conversion for us automatically (which makes sense). + // Furthermore, it is illegal for an attribute or namespace value to have a raw "<" character (as per XML). + // So the solution is very simple: + // Just convert any '<' characters to the escaped "<" string. + + return [str stringByReplacingOccurrencesOfString:@"<" withString:@"<"]; +} + +static NSInteger sortIdentities(NSXMLElement *identity1, NSXMLElement *identity2, void *context) +{ + // Sort the service discovery identities by category and then by type and then by xml:lang (if it exists). + // + // All sort operations MUST be performed using "i;octet" collation as specified in Section 9.3 of RFC 4790. + + NSComparisonResult result; + + NSString *category1 = [identity1 attributeStringValueForName:@"category" withDefaultValue:@""]; + NSString *category2 = [identity2 attributeStringValueForName:@"category" withDefaultValue:@""]; + + category1 = encodeLt(category1); + category2 = encodeLt(category2); + + result = [category1 compare:category2 options:NSLiteralSearch]; + if (result != NSOrderedSame) + { + return result; + } + + NSString *type1 = [identity1 attributeStringValueForName:@"type" withDefaultValue:@""]; + NSString *type2 = [identity2 attributeStringValueForName:@"type" withDefaultValue:@""]; + + type1 = encodeLt(type1); + type2 = encodeLt(type2); + + result = [type1 compare:type2 options:NSLiteralSearch]; + if (result != NSOrderedSame) + { + return result; + } + + NSString *lang1 = [identity1 attributeStringValueForName:@"xml:lang" withDefaultValue:@""]; + NSString *lang2 = [identity2 attributeStringValueForName:@"xml:lang" withDefaultValue:@""]; + + lang1 = encodeLt(lang1); + lang2 = encodeLt(lang2); + + result = [lang1 compare:lang2 options:NSLiteralSearch]; + if (result != NSOrderedSame) + { + return result; + } + + NSString *name1 = [identity1 attributeStringValueForName:@"name" withDefaultValue:@""]; + NSString *name2 = [identity2 attributeStringValueForName:@"name" withDefaultValue:@""]; + + name1 = encodeLt(name1); + name2 = encodeLt(name2); + + return [name1 compare:name2 options:NSLiteralSearch]; +} + +static NSInteger sortFeatures(NSXMLElement *feature1, NSXMLElement *feature2, void *context) +{ + // All sort operations MUST be performed using "i;octet" collation as specified in Section 9.3 of RFC 4790. + + NSString *var1 = [feature1 attributeStringValueForName:@"var" withDefaultValue:@""]; + NSString *var2 = [feature2 attributeStringValueForName:@"var" withDefaultValue:@""]; + + var1 = encodeLt(var1); + var2 = encodeLt(var2); + + return [var1 compare:var2 options:NSLiteralSearch]; +} + +static NSString* extractFormTypeValue(NSXMLElement *form) +{ + // From the RFC: + // + // If the FORM_TYPE field is not of type "hidden" or the form does not + // include a FORM_TYPE field, ignore the form but continue processing. + // + // If the FORM_TYPE field contains more than one element with different XML character data, + // consider the entire response to be ill-formed. + + // This method will return: + // + // - The form type's value if it exists + // - An empty string if it does not contain a form type field (or the form type is not of type hidden) + // - Nil if the form type is invalid (contains more than one element which are different) + // + // In other words + // + // - Non-empty string -> proper form + // - Empty string -> ignore form + // - Nil -> Entire response is to be considered ill-formed + // + // The returned value is properly encoded via encodeLt() and contains the trailing '<' character. + + NSArray *fields = [form elementsForName:@"field"]; + for (NSXMLElement *field in fields) + { + NSString *var = [field attributeStringValueForName:@"var"]; + NSString *type = [field attributeStringValueForName:@"type"]; + + if ([var isEqualToString:@"FORM_TYPE"] && [type isEqualToString:@"hidden"]) + { + NSArray *values = [field elementsForName:@"value"]; + + if ([values count] > 0) + { + if ([values count] > 1) + { + NSString *baseValue = [values[0] stringValue]; + + NSUInteger i; + for (i = 1; i < [values count]; i++) + { + NSString *value = [values[i] stringValue]; + + if (![value isEqualToString:baseValue]) + { + // Multiple elements with differing XML character data + return nil; + } + } + } + + NSString *result = [[values lastObject] stringValue]; + if (result == nil) + { + // This is why the result contains the trailing '<' character. + result = @""; + } + + return [NSString stringWithFormat:@"%@<", encodeLt(result)]; + } + } + } + + return @""; +} + +static NSInteger sortForms(NSXMLElement *form1, NSXMLElement *form2, void *context) +{ + // Sort the forms by the FORM_TYPE (i.e., by the XML character data of the element. + // + // All sort operations MUST be performed using "i;octet" collation as specified in Section 9.3 of RFC 4790. + + NSString *formTypeValue1 = extractFormTypeValue(form1); + NSString *formTypeValue2 = extractFormTypeValue(form2); + + // The formTypeValue variable is guaranteed to be properly encoded. + + if (formTypeValue1) + { + if (formTypeValue2) + return [formTypeValue1 compare:formTypeValue2 options:NSLiteralSearch]; + else + return NSOrderedAscending; + } + else if (formTypeValue2) + { + return NSOrderedDescending; + } + else + { + return NSOrderedSame; + } +} + +static NSInteger sortFormFields(NSXMLElement *field1, NSXMLElement *field2, void *context) +{ + // Sort the fields by the "var" attribute. + // + // All sort operations MUST be performed using "i;octet" collation as specified in Section 9.3 of RFC 4790. + + NSString *var1 = [field1 attributeStringValueForName:@"var" withDefaultValue:@""]; + NSString *var2 = [field2 attributeStringValueForName:@"var" withDefaultValue:@""]; + + var1 = encodeLt(var1); + var2 = encodeLt(var2); + + return [var1 compare:var2 options:NSLiteralSearch]; +} + +static NSInteger sortFieldValues(NSXMLElement *value1, NSXMLElement *value2, void *context) +{ + NSString *str1 = [value1 stringValue]; + NSString *str2 = [value2 stringValue]; + + if (str1 == nil) str1 = @""; + if (str2 == nil) str2 = @""; + + str1 = encodeLt(str1); + str2 = encodeLt(str2); + + return [str1 compare:str2 options:NSLiteralSearch]; +} + ++ (NSString *)hashCapabilitiesFromQuery:(NSXMLElement *)query +{ + if (query == nil) return nil; + + NSMutableSet *set = [NSMutableSet set]; + + NSMutableString *s = [NSMutableString string]; + + NSArray *identities = [[query elementsForName:@"identity"] sortedArrayUsingFunction:sortIdentities context:NULL]; + for (NSXMLElement *identity in identities) + { + // Format as: category / type / lang / name + + NSString *category = [identity attributeStringValueForName:@"category" withDefaultValue:@""]; + NSString *type = [identity attributeStringValueForName:@"type" withDefaultValue:@""]; + NSString *lang = [identity attributeStringValueForName:@"xml:lang" withDefaultValue:@""]; + NSString *name = [identity attributeStringValueForName:@"name" withDefaultValue:@""]; + + category = encodeLt(category); + type = encodeLt(type); + lang = encodeLt(lang); + name = encodeLt(name); + + NSString *mash = [NSString stringWithFormat:@"%@/%@/%@/%@<", category, type, lang, name]; + + // Section 5.4, rule 3.3: + // + // If the response includes more than one service discovery identity with + // the same category/type/lang/name, consider the entire response to be ill-formed. + + if ([set containsObject:mash]) + { + return nil; + } + else + { + [set addObject:mash]; + } + + [s appendString:mash]; + } + + [set removeAllObjects]; + + + NSArray *features = [[query elementsForName:@"feature"] sortedArrayUsingFunction:sortFeatures context:NULL]; + for (NSXMLElement *feature in features) + { + NSString *var = [feature attributeStringValueForName:@"var" withDefaultValue:@""]; + + var = encodeLt(var); + + NSString *mash = [NSString stringWithFormat:@"%@<", var]; + + // Section 5.4, rule 3.4: + // + // If the response includes more than one service discovery feature with the + // same XML character data, consider the entire response to be ill-formed. + + if ([set containsObject:mash]) + { + return nil; + } + else + { + [set addObject:mash]; + } + + [s appendString:mash]; + } + + [set removeAllObjects]; + + NSArray *unsortedForms = [query elementsForLocalName:@"x" URI:@"jabber:x:data"]; + NSArray *forms = [unsortedForms sortedArrayUsingFunction:sortForms context:NULL]; + for (NSXMLElement *form in forms) + { + NSString *formTypeValue = extractFormTypeValue(form); + + if (formTypeValue == nil) + { + // Invalid according to section 5.4, rule 3.5 + return nil; + } + if ([formTypeValue length] == 0) + { + // Ignore according to section 5.4, rule 3.6 + continue; + } + + // Note: The formTypeValue is properly encoded and contains the trailing '<' character. + + [s appendString:formTypeValue]; + + NSArray *fields = [[form elementsForName:@"field"] sortedArrayUsingFunction:sortFormFields context:NULL]; + for (NSXMLElement *field in fields) + { + // For each field other than FORM_TYPE: + // + // 1. Append the value of the var attribute, followed by the '<' character. + // 2. Sort values by the XML character data of the element. + // 3. For each element, append the XML character data, followed by the '<' character. + + NSString *var = [field attributeStringValueForName:@"var" withDefaultValue:@""]; + + var = encodeLt(var); + + if ([var isEqualToString:@"FORM_TYPE"]) + { + continue; + } + + [s appendFormat:@"%@<", var]; + + NSArray *values = [[field elementsForName:@"value"] sortedArrayUsingFunction:sortFieldValues context:NULL]; + for (NSXMLElement *value in values) + { + NSString *str = [value stringValue]; + if (str == nil) + { + str = @""; + } + + str = encodeLt(str); + + [s appendFormat:@"%@<", str]; + } + } + } + + NSData *data = [s dataUsingEncoding:NSUTF8StringEncoding]; + NSData *hash = [data xmpp_sha1Digest]; + + return [hash xmpp_base64Encoded]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Key Conversions +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSString *)keyFromHash:(NSString *)hash algorithm:(NSString *)hashAlg +{ + return [NSString stringWithFormat:@"%@-%@", hash, hashAlg]; +} + +- (BOOL)getHash:(NSString **)hashPtr algorithm:(NSString **)hashAlgPtr fromKey:(NSString *)key +{ + if (key == nil) return NO; + + NSRange range = [key rangeOfString:@"-"]; + + if (range.location == NSNotFound) + { + return NO; + } + + if (hashPtr) + { + *hashPtr = [key substringToIndex:range.location]; + } + if (hashAlgPtr) + { + *hashAlgPtr = [key substringFromIndex:(range.location + range.length)]; + } + + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Logic +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)collectMyCapabilities +{ + // This method must be invoked on the moduleQueue + NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + if (collectingMyCapabilities) + { + XMPPLogInfo(@"%@: %@ - Existing collection already in progress", [self class], THIS_METHOD); + return; + } + + myCapabilitiesQuery = nil; + myCapabilitiesC = nil; + + collectingMyCapabilities = YES; + + // Create new query and add standard features + // + // + // + // + // + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMLNS_DISCO_INFO]; + + NSXMLElement *feature1 = [NSXMLElement elementWithName:@"feature"]; + [feature1 addAttributeWithName:@"var" stringValue:XMLNS_DISCO_INFO]; + + NSXMLElement *feature2 = [NSXMLElement elementWithName:@"feature"]; + [feature2 addAttributeWithName:@"var" stringValue:XMLNS_CAPS]; + + [query addChild:feature1]; + [query addChild:feature2]; + + // Now prompt the delegates to add any additional features. + + SEL collectingMyCapabilitiesSelector = @selector(xmppCapabilities:collectingMyCapabilities:); + SEL myFeaturesForXMPPCapabilitiesSelector = @selector(myFeaturesForXMPPCapabilities:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:collectingMyCapabilitiesSelector] + && ![multicastDelegate hasDelegateThatRespondsToSelector:myFeaturesForXMPPCapabilitiesSelector]) + { + // None of the delegates implement the method. + // Use a shortcut. + + [self continueCollectMyCapabilities:query]; + } + else + { + // Query all interested delegates. + // This must be done serially to allow them to alter the element in a thread-safe manner. + + GCDMulticastDelegateEnumerator *collectingMyCapabilitiesDelegateEnumerator = [multicastDelegate delegateEnumerator]; + GCDMulticastDelegateEnumerator *myFeaturesForXMPPCapabilitiesDelegateEnumerator = [multicastDelegate delegateEnumerator]; + + + dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(concurrentQueue, ^{ @autoreleasepool { + + + id del; + dispatch_queue_t dq; + + while ([collectingMyCapabilitiesDelegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:collectingMyCapabilitiesSelector]) + { + dispatch_sync(dq, ^{ @autoreleasepool { + + [del xmppCapabilities:self collectingMyCapabilities:query]; + }}); + } + + while ([myFeaturesForXMPPCapabilitiesDelegateEnumerator getNextDelegate:&del delegateQueue:&dq forSelector:myFeaturesForXMPPCapabilitiesSelector]) + { + dispatch_sync(dq, ^{ @autoreleasepool { + + NSArray *features = [del myFeaturesForXMPPCapabilities:self]; + + for(NSString *feature in features){ + + BOOL found = NO; + + //Check to see if the feature is already in my capabilities + for (NSXMLElement *childElement in query.children) { + + if([[childElement attributeStringValueForName:@"var"] isEqualToString:feature]) + { + found = YES; + break; + } + } + + //The feature is not already in our capabilities so add it + if(!found) + { + NSXMLElement *featureElement = [NSXMLElement elementWithName:@"feature"]; + [featureElement addAttributeWithName:@"var" stringValue:feature]; + [query addChild:featureElement]; + } + } + + }}); + } + + dispatch_async(moduleQueue, ^{ @autoreleasepool { + + [self continueCollectMyCapabilities:query]; + }}); + + }}); + } +} + +- (void)continueCollectMyCapabilities:(NSXMLElement *)query +{ + // This method must be invoked on the moduleQueue + NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + collectingMyCapabilities = NO; + + myCapabilitiesQuery = query; + + XMPPLogVerbose(@"%@: My capabilities:\n%@", THIS_FILE, + [query XMLStringWithOptions:(NSXMLNodeCompactEmptyElement | NSXMLNodePrettyPrint)]); + + NSString *hash = [self.class hashCapabilitiesFromQuery:query]; + + if (hash == nil) + { + XMPPLogWarn(@"%@: Unable to hash capabilites (in order to send in presense element)\n" + "Perhaps there are duplicate advertised features...\n%@", THIS_FILE, + [query XMLStringWithOptions:(NSXMLNodeCompactEmptyElement | NSXMLNodePrettyPrint)]); + return; + } + + NSString *hashAlg = @"sha-1"; + + // Cache the hash + + [xmppCapabilitiesStorage setCapabilities:query forHash:hash algorithm:hashAlg]; + + // Create the c element, which will be added to normal outgoing presence elements. + // + // + + myCapabilitiesC = [[NSXMLElement alloc] initWithName:@"c" xmlns:XMLNS_CAPS]; + [myCapabilitiesC addAttributeWithName:@"hash" stringValue:hashAlg]; + [myCapabilitiesC addAttributeWithName:@"node" stringValue:myCapabilitiesNode]; + [myCapabilitiesC addAttributeWithName:@"ver" stringValue:hash]; + + // If the collection process started when the stream was connected, + // and ended up taking so long as to not be available when the presence was sent, + // we should re-broadcast our presence now that we know what our capabilities are. + + [xmppStream resendMyPresence]; +} + +- (void)recollectMyCapabilities +{ + // This is a public method. + // It may be invoked on any thread/queue. + + dispatch_block_t block = ^{ @autoreleasepool { + + [self collectMyCapabilities]; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)sendDiscoInfoQueryTo:(XMPPJID *)jid withNode:(NSString *)node ver:(NSString *)ver +{ + // + // + // + // + // Note: + // Some xmpp clients will return an error if we don't specify the proper query node. + // Some xmpp clients will return an error if we don't include an id attribute in the iq. + + NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:XMLNS_DISCO_INFO]; + + if (node && ver) + { + NSString *nodeValue = [NSString stringWithFormat:@"%@#%@", node, ver]; + + [query addAttributeWithName:@"node" stringValue:nodeValue]; + } + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:jid elementID:[xmppStream generateUUID] child:query]; + + [xmppStream sendElement:iq]; +} + +- (void)fetchCapabilitiesForJID:(XMPPJID *)jid +{ + // This is a public method. + // It may be invoked on any thread/queue. + + dispatch_block_t block = ^{ @autoreleasepool { + + if ([discoRequestJidSet containsObject:jid]) + { + // We're already requesting capabilities concerning this JID + return; + } + + BOOL areCapabilitiesKnown; + BOOL haveFailedFetchingBefore; + + NSString *node = nil; + NSString *ver = nil; + NSString *hash = nil; + NSString *hashAlg = nil; + + [xmppCapabilitiesStorage getCapabilitiesKnown:&areCapabilitiesKnown + failed:&haveFailedFetchingBefore + node:&node + ver:&ver + ext:nil + hash:&hash + algorithm:&hashAlg + forJID:jid + xmppStream:xmppStream]; + + if (areCapabilitiesKnown) + { + // We already know the capabilities for this JID + return; + } + if (haveFailedFetchingBefore) + { + // We've already sent a fetch request to the JID in the past, which failed. + return; + } + + NSString *key = nil; + + if (hash && hashAlg) + { + // This jid is associated with a capabilities hash. + // + // Now, we've verified that the jid is not in the discoRequestJidSet. + // But consider the following scenario. + // + // - autoFetchCapabilities is false. + // - We receive 2 presence elements from 2 different jids, both advertising the same capabilities hash. + // - This method is called for the first jid. + // - This method is then immediately called for the second jid. + // + // Now since autoFetchCapabilities is false, the second jid will not be in the discoRequestJidSet. + // However, there is still a disco request that concerns the jid. + + key = [self keyFromHash:hash algorithm:hashAlg]; + NSMutableArray *jids = discoRequestHashDict[key]; + + if (jids) + { + // We're already requesting capabilities concerning this JID. + // That is, there is another JID with the same hash, and we've already sent a disco request to it. + + [jids addObject:jid]; + [discoRequestJidSet addObject:jid]; + + return; + } + + // The first object in the jids array is the index of the last jid that we've sent a disco request to. + // This is used in case the jid does not respond. + + NSNumber *requestIndexNum = @1; + jids = [@[requestIndexNum, jid] mutableCopy]; + + discoRequestHashDict[key] = jids; + [discoRequestJidSet addObject:jid]; + } + else + { + [discoRequestJidSet addObject:jid]; + } + + // Send disco#info query + + [self sendDiscoInfoQueryTo:jid withNode:node ver:ver]; + + // Setup request timeout + + if (key) + { + [self setupTimeoutForDiscoRequestFromJID:jid withHashKey:key]; + } + else + { + [self setupTimeoutForDiscoRequestFromJID:jid]; + } + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +/** + * Invoked when an available presence element is received with + * a capabilities child element that conforms to the XEP-0115 standard. +**/ +- (void)handlePresenceCapabilities:(NSXMLElement *)c fromJID:(XMPPJID *)jid +{ + // This method must be invoked on the moduleQueue + NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace2(@"%@: %@ %@", THIS_FILE, THIS_METHOD, jid); + + // + // + // + + NSString *node = [c attributeStringValueForName:@"node"]; + NSString *ver = [c attributeStringValueForName:@"ver"]; + NSString *hash = [c attributeStringValueForName:@"hash"]; + + if ((node == nil) || (ver == nil)) + { + // Invalid capabilities node! + + if (autoFetchNonHashedCapabilities) + { + [self fetchCapabilitiesForJID:jid]; + } + + return; + } + + // Note: We already checked the hash variable in the xmppStream:didReceivePresence: method below. + + // Remember: hash="sha-1" ver="ABC-Actual-Hash-DEF". + // It's a bit confusing as it was designed this way for backwards compatibility with v 1.4 and below. + + NSXMLElement *newCapabilities = nil; + + BOOL areCapabilitiesKnown = [xmppCapabilitiesStorage setCapabilitiesNode:node + ver:ver + ext:nil + hash:ver // Yes, this is correct (see above) + algorithm:hash // Ditto + forJID:jid + xmppStream:xmppStream + andGetNewCapabilities:&newCapabilities]; + if (areCapabilitiesKnown) + { + XMPPLogVerbose(@"%@: Capabilities already known for jid(%@) with hash(%@)", THIS_FILE, jid, ver); + + if (newCapabilities) + { + // This is the first time we've linked the jid with the set of capabilities. + // We didn't need to do any lookups due to hashing and caching. + + // Notify the delegate(s) + [multicastDelegate xmppCapabilities:self didDiscoverCapabilities:newCapabilities forJID:jid]; + } + + // The capabilities for this hash are already known + return; + } + + // Should we automatically fetch the capabilities? + + if (!autoFetchHashedCapabilities) + { + return; + } + + // Are we already fetching the capabilities? + + NSString *key = [self keyFromHash:ver algorithm:hash]; + NSMutableArray *jids = discoRequestHashDict[key]; + + if (jids) + { + XMPPLogVerbose(@"%@: We're already fetching capabilities for hash(%@)", THIS_FILE, ver); + + // Is the jid already included in this list? + // + // There are actually two ways we can answer this question. + // - Invoke containsObject on the array (jids) + // - Invoke containsObject on the set (discoRequestJidSet) + // + // This is much faster to do on a set. + + if (![discoRequestJidSet containsObject:jid]) + { + [discoRequestJidSet addObject:jid]; + [jids addObject:jid]; + } + + // We've already sent a disco request concerning this hash. + return; + } + + // We've never sent a request for this hash. + // Add the jid to the discoRequest variables. + + // Note: The first object in the jids array is the index of the last jid that we've sent a disco request to. + // This is used in case the jid does not respond. + // + // Here's the scenario: + // We receive 5 presence elements from 5 different jids, + // all advertising the same capabilities via the same hash. + // We don't want to waste bandwidth and cpu by sending a disco request to all 5 jids. + // So we send a disco request to the first jid. + // But then what happens if that jid never responds? + // Perhaps it went offline before it could get the message. + // After a period of time ellapses, we should send a request to the next jid in the list. + // So how do we know what the next jid in the list is? + // Via the requestIndexNum of course. + + NSNumber *requestIndexNum = @1; + jids = [@[requestIndexNum, jid] mutableCopy]; + + discoRequestHashDict[key] = jids; + [discoRequestJidSet addObject:jid]; + + // Send disco#info query + + [self sendDiscoInfoQueryTo:jid withNode:node ver:ver]; + + // Setup request timeout + + [self setupTimeoutForDiscoRequestFromJID:jid withHashKey:key]; +} + +/** + * Invoked when an available presence element is received with + * a capabilities child element that implements the legacy version of XEP-0115. +**/ +- (void)handleLegacyPresenceCapabilities:(NSXMLElement *)c fromJID:(XMPPJID *)jid +{ + // This method must be invoked on the moduleQueue + NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace2(@"%@: %@ %@", THIS_FILE, THIS_METHOD, jid); + + NSString *node = [c attributeStringValueForName:@"node"]; + NSString *ver = [c attributeStringValueForName:@"ver"]; + NSString *ext = [c attributeStringValueForName:@"ext"]; + + if ((node == nil) || (ver == nil)) + { + // Invalid capabilities node! + + if (autoFetchNonHashedCapabilities) + { + [self fetchCapabilitiesForJID:jid]; + } + + return; + } + + BOOL areCapabilitiesKnown = [xmppCapabilitiesStorage setCapabilitiesNode:node + ver:ver + ext:ext + hash:nil + algorithm:nil + forJID:jid + xmppStream:xmppStream + andGetNewCapabilities:nil]; + if (areCapabilitiesKnown) + { + XMPPLogVerbose(@"%@: Capabilities already known for jid(%@)", THIS_FILE, jid); + + // The capabilities for this jid are already known + return; + } + + // Should we automatically fetch the capabilities? + + if (!autoFetchNonHashedCapabilities) + { + return; + } + + // Are we already fetching the capabilities? + + if ([discoRequestJidSet containsObject:jid]) + { + XMPPLogVerbose(@"%@: We're already fetching capabilities for jid(%@)", THIS_FILE, jid); + + // We've already sent a disco request to this jid. + return; + } + + [discoRequestJidSet addObject:jid]; + + // Send disco#info query + + [self sendDiscoInfoQueryTo:jid withNode:node ver:ver]; + + // Setup request timeout + + [self setupTimeoutForDiscoRequestFromJID:jid]; +} + +/** + * Invoked when we receive a disco request (request for our capabilities). + * We should response with the proper disco response. +**/ +- (void)handleDiscoRequest:(XMPPIQ *)iqRequest +{ + // This method must be invoked on the moduleQueue + NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + if (myCapabilitiesQuery == nil) + { + // It appears we haven't collected our list of capabilites yet. + // This will need to be done before we can add the hash to the outgoing presence element. + + [self collectMyCapabilities]; + } + else if (myCapabilitiesC) + { + NSXMLElement *queryRequest = [iqRequest childElement]; + NSString *node = [queryRequest attributeStringValueForName:@"node"]; + + // + // + // + // + // + // + + NSXMLElement *query = [myCapabilitiesQuery copy]; + if (node) + { + [query addAttributeWithName:@"node" stringValue:node]; + } + + XMPPIQ *iqResponse = [XMPPIQ iqWithType:@"result" + to:[iqRequest from] + elementID:[iqRequest elementID] + child:query]; + + [xmppStream sendElement:iqResponse]; + } +} + +/** + * Invoked when we receive a response to one of our previously sent disco requests. +**/ +- (void)handleDiscoResponse:(NSXMLElement *)querySubElement fromJID:(XMPPJID *)jid +{ + // This method must be invoked on the moduleQueue + NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + // Remember XML hiearchy memory management rules. + // The passed parameter is a subnode of the IQ, and we need to pass it asynchronously to storge / delegate(s). + NSXMLElement *query = [querySubElement copy]; + + NSString *hash = nil; + NSString *hashAlg = nil; + + BOOL hashResponse = [xmppCapabilitiesStorage getCapabilitiesHash:&hash + algorithm:&hashAlg + forJID:jid + xmppStream:xmppStream]; + if (hashResponse) + { + XMPPLogVerbose(@"%@: %@ - Hash response...", THIS_FILE, THIS_METHOD); + + // Standard version 1.5+ + + NSString *key = [self keyFromHash:hash algorithm:hashAlg]; + + NSString *calculatedHash = [self.class hashCapabilitiesFromQuery:query]; + + if ([calculatedHash isEqualToString:hash]) + { + XMPPLogVerbose(@"%@: %@ - Hash matches!", THIS_FILE, THIS_METHOD); + + // Store the capabilities (associated with the hash) + [xmppCapabilitiesStorage setCapabilities:query forHash:hash algorithm:hashAlg]; + + // Remove the jid(s) from the discoRequest variables + NSArray *jids = discoRequestHashDict[key]; + + NSUInteger i; + for (i = 1; i < [jids count]; i++) + { + XMPPJID *currentJid = jids[i]; + + [discoRequestJidSet removeObject:currentJid]; + + // Notify the delegate(s) + [multicastDelegate xmppCapabilities:self didDiscoverCapabilities:query forJID:currentJid]; + } + + [discoRequestHashDict removeObjectForKey:key]; + + // Cancel the request timeout + [self cancelTimeoutForDiscoRequestFromJID:jid]; + } + else + { + XMPPLogWarn(@"%@: Hash mismatch! hash(%@) != calculatedHash(%@)", THIS_FILE, hash, calculatedHash); + + // Revoke the associated hash from the jid + [xmppCapabilitiesStorage clearCapabilitiesHashAndAlgorithmForJID:jid xmppStream:xmppStream]; + + // Now set the capabilities for the jid + [xmppCapabilitiesStorage setCapabilities:query forJID:jid xmppStream:xmppStream]; + + // Notify the delegate(s) + [multicastDelegate xmppCapabilities:self didDiscoverCapabilities:query forJID:jid]; + + // We'd still like to know what the capabilities are for this hash. + // Move onto the next one in the list (if there are more, otherwise stop). + [self maybeQueryNextJidWithHashKey:key dueToHashMismatch:YES]; + } + } + else + { + XMPPLogVerbose(@"%@: %@ - Non-Hash response", THIS_FILE, THIS_METHOD); + + // Store the capabilities (associated with the jid) + [xmppCapabilitiesStorage setCapabilities:query forJID:jid xmppStream:xmppStream]; + + // Remove the jid from the discoRequest variable + [discoRequestJidSet removeObject:jid]; + + // Cancel the request timeout + [self cancelTimeoutForDiscoRequestFromJID:jid]; + + // Notify the delegate(s) + [multicastDelegate xmppCapabilities:self didDiscoverCapabilities:query forJID:jid]; + } +} + +- (void)handleDiscoErrorResponse:(NSXMLElement *)querySubElement fromJID:(XMPPJID *)jid +{ + // This method must be invoked on the moduleQueue + NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + NSString *hash = nil; + NSString *hashAlg = nil; + + BOOL hashResponse = [xmppCapabilitiesStorage getCapabilitiesHash:&hash + algorithm:&hashAlg + forJID:jid + xmppStream:xmppStream]; + if (hashResponse) + { + NSString *key = [self keyFromHash:hash algorithm:hashAlg]; + + // We'd still like to know what the capabilities are for this hash. + // Move onto the next one in the list (if there are more, otherwise stop). + [self maybeQueryNextJidWithHashKey:key dueToHashMismatch:NO]; + } + else + { + // Make a note of the failure + [xmppCapabilitiesStorage setCapabilitiesFetchFailedForJID:jid xmppStream:xmppStream]; + + // Remove the jid from the discoRequest variable + [discoRequestJidSet removeObject:jid]; + + // Cancel the request timeout + [self cancelTimeoutForDiscoRequestFromJID:jid]; + } +} + +- (void)maybeQueryNextJidWithHashKey:(NSString *)key dueToHashMismatch:(BOOL)hashMismatch +{ + // This method must be invoked on the moduleQueue + NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + // Get the list of jids that have the same capabilities hash + + NSMutableArray *jids = discoRequestHashDict[key]; + if (jids == nil) + { + XMPPLogWarn(@"%@: %@ - Key doesn't exist in discoRequestHashDict", THIS_FILE, THIS_METHOD); + + return; + } + + // Get the index and jid of the fetch that just failed + + NSUInteger requestIndex = [jids[0] unsignedIntegerValue]; + XMPPJID *jid = jids[requestIndex]; + + // Release the associated timer + [self cancelTimeoutForDiscoRequestFromJID:jid]; + + if (hashMismatch) + { + // We need to remove the naughty jid from the lists. + + [discoRequestJidSet removeObject:jid]; + [jids removeObjectAtIndex:requestIndex]; + } + else + { + // We want to move onto the next jid in the list. + // Increment request index (and update object in jids array), + + requestIndex++; + jids[0] = @(requestIndex); + } + + // Do we have another jid that we can query? + // That is, another jid that was broadcasting the same capabilities hash. + + if (requestIndex < [jids count]) + { + jid = jids[requestIndex]; + + NSString *node = nil; + NSString *ver = nil; + + [xmppCapabilitiesStorage getCapabilitiesKnown:nil + failed:nil + node:&node + ver:&ver + ext:nil + hash:nil + algorithm:nil + forJID:jid + xmppStream:xmppStream]; + + // Send disco#info query + + [self sendDiscoInfoQueryTo:jid withNode:node ver:ver]; + + // Setup request timeout + + [self setupTimeoutForDiscoRequestFromJID:jid withHashKey:key]; + } + else + { + // We've queried every single jid that was broadcasting this capabilities hash. + // Nothing left to do now but wait. + // + // If one of the jids happens to eventually respond, + // then we'll still be able to link the capabilities to every jid with the same capabilities hash. + // + // This would be handled by the xmppCapabilitiesStorage class, + // via the setCapabilitiesForJID method. + + NSUInteger i; + for (i = 1; i < [jids count]; i++) + { + jid = jids[i]; + + [discoRequestJidSet removeObject:jid]; + [xmppCapabilitiesStorage setCapabilitiesFetchFailedForJID:jid xmppStream:xmppStream]; + } + + [discoRequestHashDict removeObjectForKey:key]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppStreamDidConnect:(XMPPStream *)sender +{ + // If this is the first time we've connected, start collecting our list of capabilities. + // We do this now so that the process is likely ready by the time we need to send a presence element. + + if (myCapabilitiesQuery == nil) + { + [self collectMyCapabilities]; + } +} + +- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender +{ + if (autoFetchMyServerCapabilities) + { + XMPPJID *myJID = [xmppStream myJID]; + XMPPJID *myServerJID = [XMPPJID jidWithUser:nil domain:[myJID domain] resource:nil]; + [self fetchCapabilitiesForJID:myServerJID]; + } +} + +- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence +{ + // This method is invoked on the moduleQueue. + + // XEP-0115 presence: + // + // + // + // + + NSString *type = [presence type]; + + XMPPJID *myJID = xmppStream.myJID; + if ([myJID isEqual:[presence from]]) + { + // Our own presence is being reflected back to us. + return; + } + + if ([type isEqualToString:@"unavailable"]) + { + [xmppCapabilitiesStorage clearNonPersistentCapabilitiesForJID:[presence from] xmppStream:xmppStream]; + } + else if ([type isEqualToString:@"available"]) + { + NSXMLElement *c = [presence elementForName:@"c" xmlns:XMLNS_CAPS]; + if (c == nil) + { + if (autoFetchNonHashedCapabilities) + { + [self fetchCapabilitiesForJID:[presence from]]; + } + } + else + { + NSString *hash = [c attributeStringValueForName:@"hash"]; + if (hash) + { + [self handlePresenceCapabilities:c fromJID:[presence from]]; + } + else + { + [self handleLegacyPresenceCapabilities:c fromJID:[presence from]]; + } + } + } +} + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + // This method is invoked on the moduleQueue. + + // Disco Request: + // + // + // + // + // + // Disco Response: + // + // + // + // + // + // + // + + NSXMLElement *query = [iq elementForName:@"query" xmlns:XMLNS_DISCO_INFO]; + if (query == nil) + { + return NO; + } + + NSString *type = [[iq attributeStringValueForName:@"type"] lowercaseString]; + if ([type isEqualToString:@"get"]) + { + NSString *node = [query attributeStringValueForName:@"node"]; + + if (node == nil || [node hasPrefix:myCapabilitiesNode]) + { + [self handleDiscoRequest:iq]; + } + else + { + return NO; + } + } + else if ([type isEqualToString:@"result"]) + { + [self handleDiscoResponse:query fromJID:[iq from]]; + } + else if ([type isEqualToString:@"error"]) + { + [self handleDiscoErrorResponse:query fromJID:[iq from]]; + } + else + { + return NO; + } + + return YES; +} + +- (XMPPPresence *)xmppStream:(XMPPStream *)sender willSendPresence:(XMPPPresence *)presence +{ + // This method is invoked on the moduleQueue. + + NSString *type = [presence type]; + + if ([type isEqualToString:@"unavailable"]) + { + [xmppCapabilitiesStorage clearAllNonPersistentCapabilitiesForXMPPStream:xmppStream]; + } + else if ([type isEqualToString:@"available"]) + { + if (myCapabilitiesQuery == nil) + { + // It appears we haven't collected our list of capabilites yet. + // This will need to be done before we can add the hash to the outgoing presence element. + + [self collectMyCapabilities]; + } + else if (myCapabilitiesC) + { + NSXMLElement *c = [myCapabilitiesC copy]; + NSXMLElement *oldC = [presence elementForName:c.name xmlns:c.xmlns]; + if (oldC) + { + [presence removeChildAtIndex:[presence.children indexOfObject:oldC]]; + [presence addChild:c]; + } + else + { + [presence addChild:c]; + } + } + } + + return presence; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Timers +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)setupTimeoutForDiscoRequestFromJID:(XMPPJID *)jid +{ + // This method must be invoked on the moduleQueue + NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + // If the timeout occurs, we will remove the jid from the discoRequestJidSet. + // If we eventually get a response (after the timeout) we will still be able to process it. + // The timeout simply prevents the set from growing infinitely. + + dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue); + + dispatch_source_set_event_handler(timer, ^{ @autoreleasepool { + + [self processTimeoutWithJID:jid]; + + dispatch_source_cancel(timer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(timer); + #endif + }}); + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (CAPABILITIES_REQUEST_TIMEOUT * NSEC_PER_SEC)); + + dispatch_source_set_timer(timer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(timer); + + // We also keep a reference to the timer in the discoTimerJidDict. + // This allows us to cancel the timer when we get a response to the disco request. + + GCDTimerWrapper *timerWrapper = [[GCDTimerWrapper alloc] initWithDispatchTimer:timer]; + + discoTimerJidDict[jid] = timerWrapper; +} + +- (void)setupTimeoutForDiscoRequestFromJID:(XMPPJID *)jid withHashKey:(NSString *)key +{ + // This method must be invoked on the moduleQueue + NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + // If the timeout occurs, we want to send a request to the next jid with the same capabilities hash. + // This list of jids is stored in the discoRequestHashDict. + // The key will allow us to fetch the jid list. + + dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue); + + dispatch_source_set_event_handler(timer, ^{ @autoreleasepool { + + [self processTimeoutWithHashKey:key]; + + dispatch_source_cancel(timer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(timer); + #endif + }}); + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (CAPABILITIES_REQUEST_TIMEOUT * NSEC_PER_SEC)); + + dispatch_source_set_timer(timer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(timer); + + // We also keep a reference to the timer in the discoTimerJidDict. + // This allows us to cancel the timer when we get a response to the disco request. + + GCDTimerWrapper *timerWrapper = [[GCDTimerWrapper alloc] initWithDispatchTimer:timer]; + + discoTimerJidDict[jid] = timerWrapper; +} + +- (void)cancelTimeoutForDiscoRequestFromJID:(XMPPJID *)jid +{ + // This method must be invoked on the moduleQueue + NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + GCDTimerWrapper *timerWrapper = discoTimerJidDict[jid]; + if (timerWrapper) + { + [timerWrapper cancel]; + [discoTimerJidDict removeObjectForKey:jid]; + } +} + +- (void)processTimeoutWithHashKey:(NSString *)key +{ + // This method must be invoked on the moduleQueue + NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + [self maybeQueryNextJidWithHashKey:key dueToHashMismatch:NO]; +} + +- (void)processTimeoutWithJID:(XMPPJID *)jid +{ + // This method must be invoked on the moduleQueue + NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + // We queried the jid for its capabilities, but it didn't answer us. + // Nothing left to do now but wait. + // + // If it happens to eventually respond, + // then we'll still be able to process the capabilities properly. + // + // But at this point we're going to consider the query to be done. + // This prevents our discoRequestJidSet from growing infinitely, + // and also opens up the possibility of sending it another query in the future. + + [discoRequestJidSet removeObject:jid]; + [xmppCapabilitiesStorage setCapabilitiesFetchFailedForJID:jid xmppStream:xmppStream]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation GCDTimerWrapper + +- (id)initWithDispatchTimer:(dispatch_source_t)aTimer +{ + if ((self = [super init])) + { + timer = aTimer; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(timer); + #endif + } + return self; +} + +- (void)cancel +{ + if (timer) + { + dispatch_source_cancel(timer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(timer); + #endif + timer = NULL; + } +} + +- (void)dealloc +{ + [self cancel]; +} + +@end diff --git a/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving.xcdatamodeld/.xccurrentversion b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving.xcdatamodeld/.xccurrentversion new file mode 100644 index 0000000..bea16b4 --- /dev/null +++ b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + XMPPMessageArchiving.xcdatamodel + + diff --git a/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving.xcdatamodeld/XMPPMessageArchiving.xcdatamodel/contents b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving.xcdatamodeld/XMPPMessageArchiving.xcdatamodel/contents new file mode 100644 index 0000000..c8636b6 --- /dev/null +++ b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving.xcdatamodeld/XMPPMessageArchiving.xcdatamodel/contents @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchivingCoreDataStorage.h b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchivingCoreDataStorage.h new file mode 100644 index 0000000..52f6e74 --- /dev/null +++ b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchivingCoreDataStorage.h @@ -0,0 +1,68 @@ +#import + +#import "XMPPCoreDataStorage.h" +#import "XMPPMessageArchiving.h" +#import "XMPPMessageArchiving_Message_CoreDataObject.h" +#import "XMPPMessageArchiving_Contact_CoreDataObject.h" + + +@interface XMPPMessageArchivingCoreDataStorage : XMPPCoreDataStorage +{ + /* Inherited protected variables from XMPPCoreDataStorage + + NSString *databaseFileName; + NSUInteger saveThreshold; + + dispatch_queue_t storageQueue; + + */ +} + +/** + * Convenience method to get an instance with the default database name. + * + * IMPORTANT: + * You are NOT required to use the sharedInstance. + * + * If your application uses multiple xmppStreams, and you use a sharedInstance of this class, + * then all of your streams share the same database store. You might get better performance if you create + * multiple instances of this class instead (using different database filenames), as this way you can have + * concurrent writes to multiple databases. +**/ ++ (instancetype)sharedInstance; + + +@property (strong) NSString *messageEntityName; +@property (strong) NSString *contactEntityName; + +- (NSEntityDescription *)messageEntity:(NSManagedObjectContext *)moc; +- (NSEntityDescription *)contactEntity:(NSManagedObjectContext *)moc; + +- (XMPPMessageArchiving_Contact_CoreDataObject *)contactForMessage:(XMPPMessageArchiving_Message_CoreDataObject *)msg; + +- (XMPPMessageArchiving_Contact_CoreDataObject *)contactWithJid:(XMPPJID *)contactJid + streamJid:(XMPPJID *)streamJid + managedObjectContext:(NSManagedObjectContext *)moc; + +- (XMPPMessageArchiving_Contact_CoreDataObject *)contactWithBareJidStr:(NSString *)contactBareJidStr + streamBareJidStr:(NSString *)streamBareJidStr + managedObjectContext:(NSManagedObjectContext *)moc; + +/* Inherited from XMPPCoreDataStorage + * Please see the XMPPCoreDataStorage header file for extensive documentation. + +- (id)initWithDatabaseFilename:(NSString *)databaseFileName storeOptions:(NSDictionary *)storeOptions; +- (id)initWithInMemoryStore; + +@property (readonly) NSString *databaseFileName; + +@property (readwrite) NSUInteger saveThreshold; + +@property (readonly) NSManagedObjectModel *managedObjectModel; +@property (readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator; + +@property (readonly) NSManagedObjectContext *mainThreadManagedObjectContext; + +*/ + +@end diff --git a/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchivingCoreDataStorage.m b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchivingCoreDataStorage.m new file mode 100644 index 0000000..fa5c36e --- /dev/null +++ b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchivingCoreDataStorage.m @@ -0,0 +1,488 @@ +#import "XMPPMessageArchivingCoreDataStorage.h" +#import "XMPPCoreDataStorageProtected.h" +#import "XMPPLogging.h" +#import "NSXMLElement+XEP_0203.h" +#import "XMPPMessage+XEP_0085.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // VERBOSE; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +@interface XMPPMessageArchivingCoreDataStorage () +{ + NSString *messageEntityName; + NSString *contactEntityName; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPMessageArchivingCoreDataStorage + +static XMPPMessageArchivingCoreDataStorage *sharedInstance; + ++ (instancetype)sharedInstance +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + sharedInstance = [[XMPPMessageArchivingCoreDataStorage alloc] initWithDatabaseFilename:nil storeOptions:nil]; + }); + + return sharedInstance; +} + +/** + * Documentation from the superclass (XMPPCoreDataStorage): + * + * If your subclass needs to do anything for init, it can do so easily by overriding this method. + * All public init methods will invoke this method at the end of their implementation. + * + * Important: If overriden you must invoke [super commonInit] at some point. +**/ +- (void)commonInit +{ + [super commonInit]; + + messageEntityName = @"XMPPMessageArchiving_Message_CoreDataObject"; + contactEntityName = @"XMPPMessageArchiving_Contact_CoreDataObject"; +} + +/** + * Documentation from the superclass (XMPPCoreDataStorage): + * + * Override me, if needed, to provide customized behavior. + * For example, you may want to perform cleanup of any non-persistent data before you start using the database. + * + * The default implementation does nothing. +**/ +- (void)didCreateManagedObjectContext +{ + // If there are any "composing" messages in the database, delete them (as they are temporary). + + NSManagedObjectContext *moc = [self managedObjectContext]; + NSEntityDescription *messageEntity = [self messageEntity:moc]; + + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"composing == YES"]; + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + fetchRequest.entity = messageEntity; + fetchRequest.predicate = predicate; + fetchRequest.fetchBatchSize = saveThreshold; + + NSError *error = nil; + NSArray *messages = [moc executeFetchRequest:fetchRequest error:&error]; + + if (messages == nil) + { + XMPPLogError(@"%@: %@ - Error executing fetchRequest: %@", [self class], THIS_METHOD, error); + return; + } + + NSUInteger count = 0; + + for (XMPPMessageArchiving_Message_CoreDataObject *message in messages) + { + [moc deleteObject:message]; + + if (++count > saveThreshold) + { + if (![moc save:&error]) + { + XMPPLogWarn(@"%@: Error saving - %@ %@", [self class], error, [error userInfo]); + [moc rollback]; + } + } + } + + if (count > 0) + { + if (![moc save:&error]) + { + XMPPLogWarn(@"%@: Error saving - %@ %@", [self class], error, [error userInfo]); + [moc rollback]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Internal API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)willInsertMessage:(XMPPMessageArchiving_Message_CoreDataObject *)message +{ + // Override hook +} + +- (void)didUpdateMessage:(XMPPMessageArchiving_Message_CoreDataObject *)message +{ + // Override hook +} + +- (void)willDeleteMessage:(XMPPMessageArchiving_Message_CoreDataObject *)message +{ + // Override hook +} + +- (void)willInsertContact:(XMPPMessageArchiving_Contact_CoreDataObject *)contact +{ + // Override hook +} + +- (void)didUpdateContact:(XMPPMessageArchiving_Contact_CoreDataObject *)contact +{ + // Override hook +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Private API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (XMPPMessageArchiving_Message_CoreDataObject *)composingMessageWithJid:(XMPPJID *)messageJid + streamJid:(XMPPJID *)streamJid + outgoing:(BOOL)isOutgoing + managedObjectContext:(NSManagedObjectContext *)moc +{ + XMPPMessageArchiving_Message_CoreDataObject *result = nil; + + NSEntityDescription *messageEntity = [self messageEntity:moc]; + + // Order matters: + // 1. composing - most likely not many with it set to YES in database + // 2. bareJidStr - splits database by number of conversations + // 3. outgoing - splits database in half + // 4. streamBareJidStr - might not limit database at all + + NSString *predicateFrmt = @"composing == YES AND bareJidStr == %@ AND outgoing == %@ AND streamBareJidStr == %@"; + NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateFrmt, + [messageJid bare], @(isOutgoing), + [streamJid bare]]; + + NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"timestamp" ascending:NO]; + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + fetchRequest.entity = messageEntity; + fetchRequest.predicate = predicate; + fetchRequest.sortDescriptors = @[sortDescriptor]; + fetchRequest.fetchLimit = 1; + + NSError *error = nil; + NSArray *results = [moc executeFetchRequest:fetchRequest error:&error]; + + if (results == nil || error) + { + XMPPLogError(@"%@: %@ - Error executing fetchRequest: %@", THIS_FILE, THIS_METHOD, fetchRequest); + } + else + { + result = (XMPPMessageArchiving_Message_CoreDataObject *)[results lastObject]; + } + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Public API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (XMPPMessageArchiving_Contact_CoreDataObject *)contactForMessage:(XMPPMessageArchiving_Message_CoreDataObject *)msg +{ + // Potential override hook + + return [self contactWithBareJidStr:msg.bareJidStr + streamBareJidStr:msg.streamBareJidStr + managedObjectContext:msg.managedObjectContext]; +} + +- (XMPPMessageArchiving_Contact_CoreDataObject *)contactWithJid:(XMPPJID *)contactJid + streamJid:(XMPPJID *)streamJid + managedObjectContext:(NSManagedObjectContext *)moc +{ + return [self contactWithBareJidStr:[contactJid bare] + streamBareJidStr:[streamJid bare] + managedObjectContext:moc]; +} + +- (XMPPMessageArchiving_Contact_CoreDataObject *)contactWithBareJidStr:(NSString *)contactBareJidStr + streamBareJidStr:(NSString *)streamBareJidStr + managedObjectContext:(NSManagedObjectContext *)moc +{ + NSEntityDescription *entity = [self contactEntity:moc]; + + NSPredicate *predicate; + if (streamBareJidStr) + { + predicate = [NSPredicate predicateWithFormat:@"bareJidStr == %@ AND streamBareJidStr == %@", + contactBareJidStr, streamBareJidStr]; + } + else + { + predicate = [NSPredicate predicateWithFormat:@"bareJidStr == %@", contactBareJidStr]; + } + + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + [fetchRequest setEntity:entity]; + [fetchRequest setFetchLimit:1]; + [fetchRequest setPredicate:predicate]; + + NSError *error = nil; + NSArray *results = [moc executeFetchRequest:fetchRequest error:&error]; + + if (results == nil) + { + XMPPLogError(@"%@: %@ - Fetch request error: %@", THIS_FILE, THIS_METHOD, error); + return nil; + } + else + { + return (XMPPMessageArchiving_Contact_CoreDataObject *)[results lastObject]; + } +} + +- (NSString *)messageEntityName +{ + __block NSString *result = nil; + + dispatch_block_t block = ^{ + result = messageEntityName; + }; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_sync(storageQueue, block); + + return result; +} + +- (void)setMessageEntityName:(NSString *)entityName +{ + dispatch_block_t block = ^{ + messageEntityName = entityName; + }; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_async(storageQueue, block); +} + +- (NSString *)contactEntityName +{ + __block NSString *result = nil; + + dispatch_block_t block = ^{ + result = contactEntityName; + }; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_sync(storageQueue, block); + + return result; +} + +- (void)setContactEntityName:(NSString *)entityName +{ + dispatch_block_t block = ^{ + contactEntityName = entityName; + }; + + if (dispatch_get_specific(storageQueueTag)) + block(); + else + dispatch_async(storageQueue, block); +} + +- (NSEntityDescription *)messageEntity:(NSManagedObjectContext *)moc +{ + // This is a public method, and may be invoked on any queue. + // So be sure to go through the public accessor for the entity name. + + return [NSEntityDescription entityForName:[self messageEntityName] inManagedObjectContext:moc]; +} + +- (NSEntityDescription *)contactEntity:(NSManagedObjectContext *)moc +{ + // This is a public method, and may be invoked on any queue. + // So be sure to go through the public accessor for the entity name. + + return [NSEntityDescription entityForName:[self contactEntityName] inManagedObjectContext:moc]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Storage Protocol +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)configureWithParent:(XMPPMessageArchiving *)aParent queue:(dispatch_queue_t)queue +{ + return [super configureWithParent:aParent queue:queue]; +} + +- (void)archiveMessage:(XMPPMessage *)message outgoing:(BOOL)isOutgoing xmppStream:(XMPPStream *)xmppStream +{ + // Message should either have a body, or be a composing notification + + NSString *messageBody = [[message elementForName:@"body"] stringValue]; + BOOL isComposing = NO; + BOOL shouldDeleteComposingMessage = NO; + + if ([messageBody length] == 0) + { + // Message doesn't have a body. + // Check to see if it has a chat state (composing, paused, etc). + + isComposing = [message hasComposingChatState]; + if (!isComposing) + { + if ([message hasChatState]) + { + // Message has non-composing chat state. + // So if there is a current composing message in the database, + // then we need to delete it. + shouldDeleteComposingMessage = YES; + } + else + { + // Message has no body and no chat state. + // Nothing to do with it. + return; + } + } + } + + [self scheduleBlock:^{ + + NSManagedObjectContext *moc = [self managedObjectContext]; + XMPPJID *myJid = [self myJIDForXMPPStream:xmppStream]; + + XMPPJID *messageJid = isOutgoing ? [message to] : [message from]; + + // Fetch-n-Update OR Insert new message + + XMPPMessageArchiving_Message_CoreDataObject *archivedMessage = + [self composingMessageWithJid:messageJid + streamJid:myJid + outgoing:isOutgoing + managedObjectContext:moc]; + + if (shouldDeleteComposingMessage) + { + if (archivedMessage) + { + [self willDeleteMessage:archivedMessage]; // Override hook + [moc deleteObject:archivedMessage]; + } + else + { + // Composing message has already been deleted (or never existed) + } + } + else + { + XMPPLogVerbose(@"Previous archivedMessage: %@", archivedMessage); + + BOOL didCreateNewArchivedMessage = NO; + if (archivedMessage == nil) + { + archivedMessage = (XMPPMessageArchiving_Message_CoreDataObject *) + [[NSManagedObject alloc] initWithEntity:[self messageEntity:moc] + insertIntoManagedObjectContext:nil]; + + didCreateNewArchivedMessage = YES; + } + + archivedMessage.message = message; + archivedMessage.body = messageBody; + + archivedMessage.bareJid = [messageJid bareJID]; + archivedMessage.streamBareJidStr = [myJid bare]; + + NSDate *timestamp = [message delayedDeliveryDate]; + if (timestamp) + archivedMessage.timestamp = timestamp; + else + archivedMessage.timestamp = [[NSDate alloc] init]; + + archivedMessage.thread = [[message elementForName:@"thread"] stringValue]; + archivedMessage.isOutgoing = isOutgoing; + archivedMessage.isComposing = isComposing; + + XMPPLogVerbose(@"New archivedMessage: %@", archivedMessage); + + if (didCreateNewArchivedMessage) // [archivedMessage isInserted] doesn't seem to work + { + XMPPLogVerbose(@"Inserting message..."); + + [archivedMessage willInsertObject]; // Override hook + [self willInsertMessage:archivedMessage]; // Override hook + [moc insertObject:archivedMessage]; + } + else + { + XMPPLogVerbose(@"Updating message..."); + + [archivedMessage didUpdateObject]; // Override hook + [self didUpdateMessage:archivedMessage]; // Override hook + } + + // Create or update contact (if message with actual content) + + if ([messageBody length] > 0) + { + BOOL didCreateNewContact = NO; + + XMPPMessageArchiving_Contact_CoreDataObject *contact = [self contactForMessage:archivedMessage]; + XMPPLogVerbose(@"Previous contact: %@", contact); + + if (contact == nil) + { + contact = (XMPPMessageArchiving_Contact_CoreDataObject *) + [[NSManagedObject alloc] initWithEntity:[self contactEntity:moc] + insertIntoManagedObjectContext:nil]; + + didCreateNewContact = YES; + } + + contact.streamBareJidStr = archivedMessage.streamBareJidStr; + contact.bareJid = archivedMessage.bareJid; + + contact.mostRecentMessageTimestamp = archivedMessage.timestamp; + contact.mostRecentMessageBody = archivedMessage.body; + contact.mostRecentMessageOutgoing = @(isOutgoing); + + XMPPLogVerbose(@"New contact: %@", contact); + + if (didCreateNewContact) // [contact isInserted] doesn't seem to work + { + XMPPLogVerbose(@"Inserting contact..."); + + [contact willInsertObject]; // Override hook + [self willInsertContact:contact]; // Override hook + [moc insertObject:contact]; + } + else + { + XMPPLogVerbose(@"Updating contact..."); + + [contact didUpdateObject]; // Override hook + [self didUpdateContact:contact]; // Override hook + } + } + } + }]; +} + +@end diff --git a/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving_Contact_CoreDataObject.h b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving_Contact_CoreDataObject.h new file mode 100644 index 0000000..a62ccb3 --- /dev/null +++ b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving_Contact_CoreDataObject.h @@ -0,0 +1,36 @@ +#import +#import +#import "XMPP.h" + + +@interface XMPPMessageArchiving_Contact_CoreDataObject : NSManagedObject + +@property (nonatomic, strong) XMPPJID * bareJid; // Transient (proper type, not on disk) +@property (nonatomic, strong) NSString * bareJidStr; // Shadow (binary data, written to disk) + +@property (nonatomic, strong) NSDate * mostRecentMessageTimestamp; +@property (nonatomic, strong) NSString * mostRecentMessageBody; +@property (nonatomic, strong) NSNumber * mostRecentMessageOutgoing; + +@property (nonatomic, strong) NSString * streamBareJidStr; + +/** + * This method is called immediately before the object is inserted into the managedObjectContext. + * At this point, all normal properties have been set. + * + * If you extend XMPPMessageArchiving_Contact_CoreDataObject, + * you can use this method as a hook to set your custom properties. +**/ +- (void)willInsertObject; + +/** + * This method is called after any properties on the object have been updated, + * due to a message being added to the conversation. + * At this point, any changed properties have been updated. + * + * If you extend XMPPMessageArchiving_Contact_CoreDataObject, + * you can use this method as a hook to update your custom properties. +**/ +- (void)didUpdateObject; + +@end diff --git a/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving_Contact_CoreDataObject.m b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving_Contact_CoreDataObject.m new file mode 100644 index 0000000..46ed37c --- /dev/null +++ b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving_Contact_CoreDataObject.m @@ -0,0 +1,94 @@ +#import "XMPPMessageArchiving_Contact_CoreDataObject.h" + + +@interface XMPPMessageArchiving_Contact_CoreDataObject () + +@property (nonatomic, strong) XMPPJID * primitiveBareJid; +@property (nonatomic, strong) NSString * primitiveBareJidStr; + +@end + + +@implementation XMPPMessageArchiving_Contact_CoreDataObject + +@dynamic bareJid, primitiveBareJid; +@dynamic bareJidStr, primitiveBareJidStr; +@dynamic mostRecentMessageTimestamp; +@dynamic mostRecentMessageBody; +@dynamic mostRecentMessageOutgoing; +@dynamic streamBareJidStr; + +#pragma mark Transient bareJid + +- (XMPPJID *)bareJid +{ + // Create and cache on demand + + [self willAccessValueForKey:@"bareJid"]; + XMPPJID *tmp = self.primitiveBareJid; + [self didAccessValueForKey:@"bareJid"]; + + if (tmp == nil) + { + NSString *bareJidStr = self.bareJidStr; + if (bareJidStr) + { + tmp = [XMPPJID jidWithString:bareJidStr]; + self.primitiveBareJid = tmp; + } + } + + return tmp; +} + +- (void)setBareJid:(XMPPJID *)bareJid +{ + if ([self.bareJid isEqualToJID:bareJid options:XMPPJIDCompareBare]) + { + return; // No change + } + + [self willChangeValueForKey:@"bareJid"]; + [self willChangeValueForKey:@"bareJidStr"]; + + self.primitiveBareJid = [bareJid bareJID]; + self.primitiveBareJidStr = [bareJid bare]; + + [self didChangeValueForKey:@"bareJid"]; + [self didChangeValueForKey:@"bareJidStr"]; +} + +- (void)setBareJidStr:(NSString *)bareJidStr +{ + if ([self.bareJidStr isEqualToString:bareJidStr]) + { + return; // No change + } + + [self willChangeValueForKey:@"bareJid"]; + [self willChangeValueForKey:@"bareJidStr"]; + + XMPPJID *bareJid = [[XMPPJID jidWithString:bareJidStr] bareJID]; + + self.primitiveBareJid = bareJid; + self.primitiveBareJidStr = [bareJid bare]; + + [self didChangeValueForKey:@"bareJid"]; + [self didChangeValueForKey:@"bareJidStr"]; +} + +#pragma mark Hooks + +- (void)willInsertObject +{ + // If you extend XMPPMessageArchiving_Contact_CoreDataObject, + // you can override this method to use as a hook to set your own custom properties. +} + +- (void)didUpdateObject +{ + // If you extend XMPPMessageArchiving_Contact_CoreDataObject, + // you can override this method to use as a hook to update your own custom properties. +} + +@end diff --git a/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving_Message_CoreDataObject.h b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving_Message_CoreDataObject.h new file mode 100644 index 0000000..e9d63ab --- /dev/null +++ b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving_Message_CoreDataObject.h @@ -0,0 +1,52 @@ +#import +#import +#import "XMPP.h" + + +@interface XMPPMessageArchiving_Message_CoreDataObject : NSManagedObject + +@property (nonatomic, strong) XMPPMessage * message; // Transient (proper type, not on disk) +@property (nonatomic, strong) NSString * messageStr; // Shadow (binary data, written to disk) + +/** + * This is the bare jid of the person you're having the conversation with. + * For example: robbiehanson@deusty.com + * + * Regardless of whether the message was incoming or outgoing, + * this will represent the "other" participant in the conversation. +**/ +@property (nonatomic, strong) XMPPJID * bareJid; // Transient (proper type, not on disk) +@property (nonatomic, strong) NSString * bareJidStr; // Shadow (binary data, written to disk) + +@property (nonatomic, strong) NSString * body; +@property (nonatomic, strong) NSString * thread; + +@property (nonatomic, strong) NSNumber * outgoing; // Use isOutgoing +@property (nonatomic, assign) BOOL isOutgoing; // Convenience property + +@property (nonatomic, strong) NSNumber * composing; // Use isComposing +@property (nonatomic, assign) BOOL isComposing; // Convenience property + +@property (nonatomic, strong) NSDate * timestamp; + +@property (nonatomic, strong) NSString * streamBareJidStr; + +/** + * This method is called immediately before the object is inserted into the managedObjectContext. + * At this point, all normal properties have been set. + * + * If you extend XMPPMessageArchiving_Message_CoreDataObject, + * you can use this method as a hook to set your custom properties. +**/ +- (void)willInsertObject; + +/** + * This method is called immediately after the message has been changed. + * At this point, all normal properties have been updated. + * + * If you extend XMPPMessageArchiving_Message_CoreDataObject, + * you can use this method as a hook to set your custom properties. +**/ +- (void)didUpdateObject; + +@end diff --git a/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving_Message_CoreDataObject.m b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving_Message_CoreDataObject.m new file mode 100644 index 0000000..60de9c3 --- /dev/null +++ b/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving_Message_CoreDataObject.m @@ -0,0 +1,171 @@ +#import "XMPPMessageArchiving_Message_CoreDataObject.h" + + +@interface XMPPMessageArchiving_Message_CoreDataObject () + +@property(nonatomic,strong) XMPPMessage * primitiveMessage; +@property(nonatomic,strong) NSString * primitiveMessageStr; + +@property(nonatomic,strong) XMPPJID * primitiveBareJid; +@property(nonatomic,strong) NSString * primitiveBareJidStr; + +@end + +@implementation XMPPMessageArchiving_Message_CoreDataObject + +@dynamic message, primitiveMessage; +@dynamic messageStr, primitiveMessageStr; +@dynamic bareJid, primitiveBareJid; +@dynamic bareJidStr, primitiveBareJidStr; +@dynamic body; +@dynamic thread; +@dynamic outgoing; +@dynamic composing; +@dynamic timestamp; +@dynamic streamBareJidStr; + +#pragma mark Transient message + +- (XMPPMessage *)message +{ + // Create and cache on demand + + [self willAccessValueForKey:@"message"]; + XMPPMessage *message = self.primitiveMessage; + [self didAccessValueForKey:@"message"]; + + if (message == nil) + { + NSString *messageStr = self.messageStr; + if (messageStr) + { + NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString:messageStr error:nil]; + message = [XMPPMessage messageFromElement:element]; + self.primitiveMessage = message; + } + } + + return message; +} + +- (void)setMessage:(XMPPMessage *)message +{ + [self willChangeValueForKey:@"message"]; + [self willChangeValueForKey:@"messageStr"]; + + self.primitiveMessage = message; + self.primitiveMessageStr = [message compactXMLString]; + + [self didChangeValueForKey:@"message"]; + [self didChangeValueForKey:@"messageStr"]; +} + +- (void)setMessageStr:(NSString *)messageStr +{ + [self willChangeValueForKey:@"message"]; + [self willChangeValueForKey:@"messageStr"]; + + NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString:messageStr error:nil]; + self.primitiveMessage = [XMPPMessage messageFromElement:element]; + self.primitiveMessageStr = messageStr; + + [self didChangeValueForKey:@"message"]; + [self didChangeValueForKey:@"messageStr"]; +} + +#pragma mark Transient bareJid + +- (XMPPJID *)bareJid +{ + // Create and cache on demand + + [self willAccessValueForKey:@"bareJid"]; + XMPPJID *tmp = self.primitiveBareJid; + [self didAccessValueForKey:@"bareJid"]; + + if (tmp == nil) + { + NSString *bareJidStr = self.bareJidStr; + if (bareJidStr) + { + tmp = [XMPPJID jidWithString:bareJidStr]; + self.primitiveBareJid = tmp; + } + } + + return tmp; +} + +- (void)setBareJid:(XMPPJID *)bareJid +{ + if ([self.bareJid isEqualToJID:bareJid options:XMPPJIDCompareBare]) + { + return; // No change + } + + [self willChangeValueForKey:@"bareJid"]; + [self willChangeValueForKey:@"bareJidStr"]; + + self.primitiveBareJid = [bareJid bareJID]; + self.primitiveBareJidStr = [bareJid bare]; + + [self didChangeValueForKey:@"bareJid"]; + [self didChangeValueForKey:@"bareJidStr"]; +} + +- (void)setBareJidStr:(NSString *)bareJidStr +{ + if ([self.bareJidStr isEqualToString:bareJidStr]) + { + return; // No change + } + + [self willChangeValueForKey:@"bareJid"]; + [self willChangeValueForKey:@"bareJidStr"]; + + XMPPJID *bareJid = [[XMPPJID jidWithString:bareJidStr] bareJID]; + + self.primitiveBareJid = bareJid; + self.primitiveBareJidStr = [bareJid bare]; + + [self didChangeValueForKey:@"bareJid"]; + [self didChangeValueForKey:@"bareJidStr"]; +} + +#pragma mark Convenience properties + +- (BOOL)isOutgoing +{ + return [self.outgoing boolValue]; +} + +- (void)setIsOutgoing:(BOOL)flag +{ + self.outgoing = @(flag); +} + +- (BOOL)isComposing +{ + return [self.composing boolValue]; +} + +- (void)setIsComposing:(BOOL)flag +{ + self.composing = @(flag); +} + +#pragma mark Hooks + +- (void)willInsertObject +{ + // If you extend XMPPMessageArchiving_Message_CoreDataObject, + // you can override this method to use as a hook to set your own custom properties. +} + +- (void)didUpdateObject +{ + // If you extend XMPPMessageArchiving_Message_CoreDataObject, + // you can override this method to use as a hook to set your own custom properties. +} + +@end diff --git a/Extensions/XEP-0136/XMPPMessageArchiving.h b/Extensions/XEP-0136/XMPPMessageArchiving.h new file mode 100644 index 0000000..716bc58 --- /dev/null +++ b/Extensions/XEP-0136/XMPPMessageArchiving.h @@ -0,0 +1,117 @@ +#import +#import "XMPP.h" + +#define _XMPP_MESSAGE_ARCHIVING_H + +@protocol XMPPMessageArchivingStorage; + +/** + * This class provides support for storing message history. + * The functionality is formalized in XEP-0136. +**/ +@interface XMPPMessageArchiving : XMPPModule +{ + @protected + + __strong id xmppMessageArchivingStorage; + + @private + + BOOL clientSideMessageArchivingOnly; + NSXMLElement *preferences; +} + +- (id)initWithMessageArchivingStorage:(id )storage; +- (id)initWithMessageArchivingStorage:(id )storage dispatchQueue:(dispatch_queue_t)queue; + +@property (readonly, strong) id xmppMessageArchivingStorage; + +/** + * XEP-0136 Message Archiving outlines a complex protocol for: + * + * - archiving messages on the xmpp server + * - allowing the client to sync it's client-side cache with the server side archive + * - allowing the client to configure archiving preferences (default, per contact, etc) + * + * There are times when this complication isn't necessary or possible. + * E.g. the server doesn't support the message archiving protocol. + * + * In this case you can simply set clientSideMessageArchivingOnly to YES, + * and this instance won't bother with any of the server protocol stuff. + * It will simply arhive outgoing and incoming messages. + * + * Note: Even when clientSideMessageArchivingOnly is YES, + * you can still take advantage of the preference methods to configure various options, + * such as how long to store messages, prefs for individual contacts, etc. +**/ +@property (readwrite, assign) BOOL clientSideMessageArchivingOnly; + +/** + * +**/ +@property (readwrite, copy) NSXMLElement *preferences; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPMessageArchivingStorage +@required + +// +// +// -- PUBLIC METHODS -- +// +// There are no public methods required by this protocol. +// +// Each individual roster storage class will provide a proper way to access/enumerate the +// users/resources according to the underlying storage mechanism. +// + + +// +// +// -- PRIVATE METHODS -- +// +// These methods are designed to be used ONLY by the XMPPMessageArchiving class. +// +// + +/** + * Configures the storage class, passing its parent and parent's dispatch queue. + * + * This method is called by the init method of the XMPPMessageArchiving class. + * This method is designed to inform the storage class of its parent + * and of the dispatch queue the parent will be operating on. + * + * The storage class may choose to operate on the same queue as its parent, + * or it may operate on its own internal dispatch queue. + * + * This method should return YES if it was configured properly. + * If a storage class is designed to be used with a single parent at a time, this method may return NO. + * The XMPPMessageArchiving class is configured to ignore the passed + * storage class in its init method if this method returns NO. +**/ +- (BOOL)configureWithParent:(XMPPMessageArchiving *)aParent queue:(dispatch_queue_t)queue; + +/** + * +**/ +- (void)archiveMessage:(XMPPMessage *)message outgoing:(BOOL)isOutgoing xmppStream:(XMPPStream *)stream; + +@optional + +/** + * The storage class may optionally persistently store the client preferences. +**/ +- (void)setPreferences:(NSXMLElement *)prefs forUser:(XMPPJID *)bareUserJid; + +/** + * The storage class may optionally persistently store the client preferences. + * This method is then used to fetch previously known preferences when the client first connects to the xmpp server. +**/ +- (NSXMLElement *)preferencesForUser:(XMPPJID *)bareUserJid; + +@end diff --git a/Extensions/XEP-0136/XMPPMessageArchiving.m b/Extensions/XEP-0136/XMPPMessageArchiving.m new file mode 100644 index 0000000..57e8ce0 --- /dev/null +++ b/Extensions/XEP-0136/XMPPMessageArchiving.m @@ -0,0 +1,431 @@ +#import "XMPPMessageArchiving.h" +#import "XMPPFramework.h" +#import "XMPPLogging.h" +#import "NSNumber+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +#define XMLNS_XMPP_ARCHIVE @"urn:xmpp:archive" + + +@implementation XMPPMessageArchiving + +- (id)init +{ + // This will cause a crash - it's designed to. + // Only the init methods listed in XMPPMessageArchiving.h are supported. + + return [self initWithMessageArchivingStorage:nil dispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + // This will cause a crash - it's designed to. + // Only the init methods listed in XMPPMessageArchiving.h are supported. + + return [self initWithMessageArchivingStorage:nil dispatchQueue:queue]; +} + +- (id)initWithMessageArchivingStorage:(id )storage +{ + return [self initWithMessageArchivingStorage:storage dispatchQueue:NULL]; +} + +- (id)initWithMessageArchivingStorage:(id )storage dispatchQueue:(dispatch_queue_t)queue +{ + NSParameterAssert(storage != nil); + + if ((self = [super initWithDispatchQueue:queue])) + { + if ([storage configureWithParent:self queue:moduleQueue]) + { + xmppMessageArchivingStorage = storage; + } + else + { + XMPPLogError(@"%@: %@ - Unable to configure storage!", THIS_FILE, THIS_METHOD); + } + + NSXMLElement *_default = [NSXMLElement elementWithName:@"default"]; + [_default addAttributeWithName:@"expire" stringValue:@"604800"]; + [_default addAttributeWithName:@"save" stringValue:@"body"]; + + NSXMLElement *pref = [NSXMLElement elementWithName:@"pref" xmlns:XMLNS_XMPP_ARCHIVE]; + [pref addChild:_default]; + + preferences = pref; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + XMPPLogTrace(); + + if ([super activate:aXmppStream]) + { + XMPPLogVerbose(@"%@: Activated", THIS_FILE); + + // Reserved for future potential use + + return YES; + } + + return NO; +} + +- (void)deactivate +{ + XMPPLogTrace(); + + // Reserved for future potential use + + [super deactivate]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id )xmppMessageArchivingStorage +{ + // Note: The xmppMessageArchivingStorage variable is read-only (set in the init method) + + return xmppMessageArchivingStorage; +} + +- (BOOL)clientSideMessageArchivingOnly +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = clientSideMessageArchivingOnly; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setClientSideMessageArchivingOnly:(BOOL)flag +{ + dispatch_block_t block = ^{ + clientSideMessageArchivingOnly = flag; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (NSXMLElement *)preferences +{ + __block NSXMLElement *result = nil; + + dispatch_block_t block = ^{ + + result = [preferences copy]; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setPreferences:(NSXMLElement *)newPreferences +{ + dispatch_block_t block = ^{ @autoreleasepool { + + // Update cached value + + preferences = [newPreferences copy]; + + // Update storage + + if ([xmppMessageArchivingStorage respondsToSelector:@selector(setPreferences:forUser:)]) + { + XMPPJID *myBareJid = [[xmppStream myJID] bareJID]; + + [xmppMessageArchivingStorage setPreferences:preferences forUser:myBareJid]; + } + + // Todo: + // + // - Send new pref to server (if changed) + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)shouldArchiveMessage:(XMPPMessage *)message outgoing:(BOOL)isOutgoing xmppStream:(XMPPStream *)xmppStream +{ + // XEP-0136 Section 2.9: Preferences precedence rules: + // + // When determining archiving preferences for a given message, the following rules shall apply: + // + // 1. 'save' value is taken from the element that matches the conversation, if present, + // else from the element that matches the contact (see JID Matching), if present, + // else from the default element. + // + // 2. 'otr' and 'expire' value are taken from the element that matches the contact, if present, + // else from the default element. + + NSXMLElement *match = nil; + + NSString *messageThread = [[message elementForName:@"thread"] stringValue]; + if (messageThread) + { + // First priority - matching session element + + for (NSXMLElement *session in [preferences elementsForName:@"session"]) + { + NSString *sessionThread = [session attributeStringValueForName:@"thread"]; + if ([messageThread isEqualToString:sessionThread]) + { + match = session; + break; + } + } + } + + if (match == nil) + { + // Second priority - matching item element + // + // + // XEP-0136 Section 10.1: JID Matching + // + // The following rules apply: + // + // 1. If the JID is of the form , only this particular JID matches. + // 2. If the JID is of the form , any resource matches. + // 3. If the JID is of the form , any node matches. + // + // However, having these rules only would make impossible a match like "all collections having JID + // exactly equal to bare JID/domain JID". Therefore, when the 'exactmatch' attribute is set to "true" or + // "1" on the , or element, a JID value such as "example.com" matches + // that exact JID only rather than <*@example.com>, <*@example.com/*>, or , and + // a JID value such as "localpart@example.com" matches that exact JID only rather than + // . + + XMPPJID *messageJid; + if (isOutgoing) + messageJid = [message to]; + else + messageJid = [message from]; + + NSXMLElement *match_full = nil; + NSXMLElement *match_bare = nil; + NSXMLElement *match_domain = nil; + + for (NSXMLElement *item in [preferences elementsForName:@"item"]) + { + XMPPJID *itemJid = [XMPPJID jidWithString:[item attributeStringValueForName:@"jid"]]; + + if (itemJid.resource) + { + BOOL match = [messageJid isEqualToJID:itemJid options:XMPPJIDCompareFull]; + + if (match && (match_full == nil)) + { + match_full = item; + } + } + else if (itemJid.user) + { + BOOL exactmatch = [item attributeBoolValueForName:@"exactmatch" withDefaultValue:NO]; + BOOL match; + + if (exactmatch) + match = [messageJid isEqualToJID:itemJid options:XMPPJIDCompareFull]; + else + match = [messageJid isEqualToJID:itemJid options:XMPPJIDCompareBare]; + + if (match && (match_bare == nil)) + { + match_bare = item; + } + } + else + { + BOOL exactmatch = [item attributeBoolValueForName:@"exactmatch" withDefaultValue:NO]; + BOOL match; + + if (exactmatch) + match = [messageJid isEqualToJID:itemJid options:XMPPJIDCompareFull]; + else + match = [messageJid isEqualToJID:itemJid options:XMPPJIDCompareDomain]; + + if (match && (match_domain == nil)) + { + match_domain = item; + } + } + } + + if (match_full) + match = match_full; + else if (match_bare) + match = match_bare; + else if (match_domain) + match = match_domain; + } + + if (match == nil) + { + // Third priority - default element + + match = [preferences elementForName:@"default"]; + } + + if (match == nil) + { + XMPPLogWarn(@"%@: No message archive rule found for message! Discarding...", THIS_FILE); + return NO; + } + + // The 'save' attribute specifies the user's default setting for Save Mode. + // The allowable values are: + // + // - body : the saving entity SHOULD save only elements. + // - false : the saving entity MUST save nothing. + // - message : the saving entity SHOULD save the full XML content of each element. + // - stream : the saving entity SHOULD save every byte that passes over the stream in either direction. + // + // Note: We currently only support body, and treat values of 'message' or 'stream' the same as 'body'. + + NSString *save = [[match attributeStringValueForName:@"save"] lowercaseString]; + + if ([save isEqualToString:@"false"]) + return NO; + else + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender +{ + XMPPLogTrace(); + + if (clientSideMessageArchivingOnly) return; + + // Fetch most recent preferences + + if ([xmppMessageArchivingStorage respondsToSelector:@selector(preferencesForUser:)]) + { + XMPPJID *myBareJid = [[xmppStream myJID] bareJID]; + + preferences = [xmppMessageArchivingStorage preferencesForUser:myBareJid]; + } + + // Request archiving preferences from server + // + // + // + // + + NSXMLElement *pref = [NSXMLElement elementWithName:@"pref" xmlns:XMLNS_XMPP_ARCHIVE]; + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:nil elementID:nil child:pref]; + + [sender sendElement:iq]; +} + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + NSString *type = [iq type]; + + if ([type isEqualToString:@"result"]) + { + NSXMLElement *pref = [iq elementForName:@"pref" xmlns:XMLNS_XMPP_ARCHIVE]; + if (pref) + { + [self setPreferences:pref]; + } + } + else if ([type isEqualToString:@"set"]) + { + // We receive the following type of IQ when we send a chat message within facebook from another device: + // + // + // + // Hi Jilr + // + // + + NSXMLElement *ownMessage = [iq elementForName:@"own-message" xmlns:@"http://www.facebook.com/xmpp/messages"]; + if (ownMessage) + { + BOOL isSelf = [ownMessage attributeBoolValueForName:@"self" withDefaultValue:NO]; + if (!isSelf) + { + NSString *bodyStr = [[ownMessage elementForName:@"body"] stringValue]; + if ([bodyStr length] > 0) + { + NSXMLElement *body = [NSXMLElement elementWithName:@"body" stringValue:bodyStr]; + + XMPPJID *to = [XMPPJID jidWithString:[ownMessage attributeStringValueForName:@"to"]]; + XMPPMessage *message = [XMPPMessage messageWithType:@"chat" to:to]; + [message addChild:body]; + + if ([self shouldArchiveMessage:message outgoing:YES xmppStream:sender]) + { + [xmppMessageArchivingStorage archiveMessage:message outgoing:YES xmppStream:sender]; + } + } + } + + return YES; + } + } + + return NO; +} + +- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message +{ + XMPPLogTrace(); + + if ([self shouldArchiveMessage:message outgoing:YES xmppStream:sender]) + { + [xmppMessageArchivingStorage archiveMessage:message outgoing:YES xmppStream:sender]; + } +} + +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message +{ + XMPPLogTrace(); + + if ([self shouldArchiveMessage:message outgoing:NO xmppStream:sender]) + { + [xmppMessageArchivingStorage archiveMessage:message outgoing:NO xmppStream:sender]; + } +} + +@end diff --git a/Extensions/XEP-0147/XMPPURI.h b/Extensions/XEP-0147/XMPPURI.h new file mode 100644 index 0000000..1dc90fa --- /dev/null +++ b/Extensions/XEP-0147/XMPPURI.h @@ -0,0 +1,69 @@ +// +// XMPPURI.h +// XMPPFramework +// +// Created by Christopher Ballinger on 5/15/15. +// Copyright (c) 2015 Chris Ballinger. All rights reserved. +// + +#import +#import "XMPPJID.h" + +/** + * For parsing and creating XMPP URIs RFC5122/XEP-0147 + * e.g. xmpp:username@domain.com?subscribe + * http://www.xmpp.org/extensions/xep-0147.html + */ +@interface XMPPURI : NSObject + +/** + * User JID. e.g. romeo@montague.net + * Example: xmpp:romeo@montague.net + */ +@property (nonatomic, strong, readonly) XMPPJID *jid; + +/** + * Account JID. (Optional) + * Used to specify an account with which to perform an action. + * For example 'guest@example.com' would be the authority portion of + * xmpp://guest@example.com/support@example.com?message + * so the application would show a dialog with an outgoing message + * to support@example.com from the user's account guest@example.com. + */ +@property (nonatomic, strong, readonly) XMPPJID *accountJID; + +/** + * XMPP query action. e.g. subscribe + * For example, the query action below would be 'subscribe' + * xmpp:romeo@montague.net?subscribe + * For full list: http://xmpp.org/registrar/querytypes.html + */ +@property (nonatomic, strong, readonly) NSString *queryAction; + +/** + * XMPP query parameters. e.g. subject=Test + * + * For example the query parameters for + * xmpp:romeo@montague.net?message;subject=Test%20Message;body=Here%27s%20a%20test%20message + * would be + * {"subject": "Test Message", + * "body": "Here's a test message"} + */ +@property (nonatomic, strong, readonly) NSDictionary *queryParameters; + +/** + * Generates URI string from jid, queryAction, and queryParameters + * e.g. xmpp:romeo@montague.net?subscribe + */ +- (NSString*) uriString; + +// Parsing XMPP URIs +- (instancetype) initWithURL:(NSURL*)url; +- (instancetype) initWithURIString:(NSString*)uriString; + +// Creating XMPP URIs +- (instancetype) initWithJID:(XMPPJID*)jid + queryAction:(NSString*)queryAction + queryParameters:(NSDictionary*)queryParameters; + +@end diff --git a/Extensions/XEP-0147/XMPPURI.m b/Extensions/XEP-0147/XMPPURI.m new file mode 100644 index 0000000..d3406d3 --- /dev/null +++ b/Extensions/XEP-0147/XMPPURI.m @@ -0,0 +1,108 @@ +// +// XMPPURI.m +// XMPPFramework +// +// Created by Christopher Ballinger on 5/15/15. +// Copyright (c) 2015 Chris Ballinger. All rights reserved. +// + +#import "XMPPURI.h" + +@implementation XMPPURI + +- (instancetype) initWithURIString:(NSString *)uriString { + if (self = [super init]) { + [self parseURIString:uriString]; + } + return self; +} + +- (instancetype) initWithURL:(NSURL *)url { + if (self = [self initWithURIString:url.absoluteString]) { + } + return self; +} + +- (instancetype) initWithJID:(XMPPJID*)jid + queryAction:(NSString*)queryAction + queryParameters:(NSDictionary*)queryParameters { + if (self = [super init]) { + _jid = [jid copy]; + _queryAction = [queryAction copy]; + _queryParameters = [queryParameters copy]; + } + return self; +} + +- (NSString*) uriString { + NSMutableString *uriString = [NSMutableString stringWithFormat:@"xmpp:%@", self.jid.bare]; + if (self.queryAction) { + [uriString appendFormat:@"?%@", self.queryAction]; + } + NSMutableCharacterSet *allowedCharacterSet = NSCharacterSet.URLQueryAllowedCharacterSet.mutableCopy; + [allowedCharacterSet removeCharactersInString:@"'"]; // what other characters should be removed? + [self.queryParameters enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { + NSString *value = [obj stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet]; + [uriString appendFormat:@";%@=%@", key, value]; + }]; + return uriString; +} + +- (void) parseURIString:(NSString*)uriString { + NSString *authority = nil; + // Parse authority component + if ([uriString containsString:@"://"]) { + NSRange fullRange = NSMakeRange(0, uriString.length); + NSRange startRange = [uriString rangeOfString:@"://"]; + NSUInteger trailingLocation = startRange.location + startRange.length; + NSRange trailingRange = NSMakeRange(trailingLocation, uriString.length - trailingLocation); + NSRange endRange = [uriString rangeOfString:@"/" options:0 range:trailingRange]; + NSUInteger authorityLocation = startRange.location + startRange.length; + NSRange authorityRange = NSMakeRange(authorityLocation, endRange.location - authorityLocation); + authority = [uriString substringWithRange:authorityRange]; + NSString *stringToRemove = [NSString stringWithFormat:@"://%@/", authority]; + uriString = [uriString stringByReplacingOccurrencesOfString:stringToRemove withString:@":" options:0 range:fullRange]; + } + if (authority) { + _accountJID = [XMPPJID jidWithString:authority]; + } + + NSArray *uriComponents = [uriString componentsSeparatedByString:@":"]; + NSString *scheme = nil; + NSString *jidString = nil; + + if (uriComponents.count >= 2) { + scheme = uriComponents[0]; + NSString *path = uriComponents[1]; + if ([path containsString:@"?"]) { + NSArray *queryComponents = [path componentsSeparatedByString:@"?"]; + jidString = queryComponents[0]; + NSString *query = queryComponents[1]; + NSArray *queryKeys = [query componentsSeparatedByString:@";"]; + + NSMutableDictionary *queryParameters = [NSMutableDictionary dictionaryWithCapacity:queryKeys.count]; + [queryKeys enumerateObjectsUsingBlock:^(NSString *queryItem, NSUInteger idx, BOOL *stop) { + if (idx == 0) { + _queryAction = queryItem; + } else { + NSArray *keyValue = [queryItem componentsSeparatedByString:@"="]; + if (keyValue.count == 2) { + NSString *key = keyValue[0]; + NSString *value = [keyValue[1] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + if (key && value) { + queryParameters[key] = value; + } + } + } + }]; + _queryParameters = queryParameters; + } else { + jidString = path; + } + } + if (jidString) { + _jid = [XMPPJID jidWithString:jidString]; + } +} + +@end diff --git a/Extensions/XEP-0153/XMPPvCardAvatarModule.h b/Extensions/XEP-0153/XMPPvCardAvatarModule.h new file mode 100644 index 0000000..4a58e76 --- /dev/null +++ b/Extensions/XEP-0153/XMPPvCardAvatarModule.h @@ -0,0 +1,93 @@ +// +// XMPPvCardAvatarModule.h +// XEP-0153 vCard-Based Avatars +// +// Created by Eric Chamberlain on 3/9/11. +// Copyright 2011 RF.com. All rights reserved. + +/* + * NOTE: Currently this implementation only supports downloading and caching avatars. + */ + + +#import + +#if TARGET_OS_IPHONE + #import +#else + #import +#endif + +#import "XMPP.h" +#import "XMPPvCardTempModule.h" + +#define _XMPP_VCARD_AVATAR_MODULE_H + +@protocol XMPPvCardAvatarStorage; + + +@interface XMPPvCardAvatarModule : XMPPModule +{ + __strong XMPPvCardTempModule *_xmppvCardTempModule; + __strong id _moduleStorage; + + BOOL _autoClearMyvcard; +} + +@property(nonatomic, strong, readonly) XMPPvCardTempModule *xmppvCardTempModule; + + +/* + * XEP-0153 Section 4.2 rule 1 + * + * A client MUST NOT advertise an avatar image without first downloading the current vCard. + * Once it has done this, it MAY advertise an image. + * + * Default YES + */ +@property(nonatomic, assign) BOOL autoClearMyvcard; + + +- (id)initWithvCardTempModule:(XMPPvCardTempModule *)xmppvCardTempModule; +- (id)initWithvCardTempModule:(XMPPvCardTempModule *)xmppvCardTempModule dispatchQueue:(dispatch_queue_t)queue; + + +- (NSData *)photoDataForJID:(XMPPJID *)jid; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPvCardAvatarDelegate + +#if TARGET_OS_IPHONE +- (void)xmppvCardAvatarModule:(XMPPvCardAvatarModule *)vCardTempModule + didReceivePhoto:(UIImage *)photo + forJID:(XMPPJID *)jid; +#else +- (void)xmppvCardAvatarModule:(XMPPvCardAvatarModule *)vCardTempModule + didReceivePhoto:(NSImage *)photo + forJID:(XMPPJID *)jid; +#endif + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPvCardAvatarStorage + +- (NSData *)photoDataForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream; +- (NSString *)photoHashForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream; + +/** + * Clears the vCardTemp from the store. + * This is used so we can clear any cached vCardTemp's for the JID. +**/ +- (void)clearvCardTempForJID:(XMPPJID *)jid xmppStream:(XMPPStream *)stream; + +@end + diff --git a/Extensions/XEP-0153/XMPPvCardAvatarModule.m b/Extensions/XEP-0153/XMPPvCardAvatarModule.m new file mode 100755 index 0000000..c2c39f9 --- /dev/null +++ b/Extensions/XEP-0153/XMPPvCardAvatarModule.m @@ -0,0 +1,305 @@ +// +// XMPPvCardAvatarModule.h +// XEP-0153 vCard-Based Avatars +// +// Created by Eric Chamberlain on 3/9/11. +// Copyright 2011 RF.com. All rights reserved. + +#import "XMPPvCardAvatarModule.h" + +#import "NSData+XMPP.h" +#import "NSXMLElement+XMPP.h" +#import "XMPPLogging.h" +#import "XMPPPresence.h" +#import "XMPPStream.h" +#import "XMPPvCardTempModule.h" +#import "XMPPvCardTemp.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +NSString *const kXMPPvCardAvatarElement = @"x"; +NSString *const kXMPPvCardAvatarNS = @"vcard-temp:x:update"; +NSString *const kXMPPvCardAvatarPhotoElement = @"photo"; + + +@implementation XMPPvCardAvatarModule + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Init/dealloc +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)init +{ + // This will cause a crash - it's designed to. + // Only the init methods listed in XMPPvCardAvatarModule.h are supported. + + return [self initWithvCardTempModule:nil dispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + // This will cause a crash - it's designed to. + // Only the init methods listed in XMPPvCardAvatarModule.h are supported. + + return [self initWithvCardTempModule:nil dispatchQueue:NULL]; +} + +- (id)initWithvCardTempModule:(XMPPvCardTempModule *)xmppvCardTempModule +{ + return [self initWithvCardTempModule:xmppvCardTempModule dispatchQueue:NULL]; +} + +- (id)initWithvCardTempModule:(XMPPvCardTempModule *)xmppvCardTempModule dispatchQueue:(dispatch_queue_t)queue +{ + NSParameterAssert(xmppvCardTempModule != nil); + + if ((self = [super initWithDispatchQueue:queue])) { + _xmppvCardTempModule = xmppvCardTempModule; + + // we don't need to call the storage configureWithParent:queue: method, + // because the vCardTempModule already did that. + _moduleStorage = (id )xmppvCardTempModule.xmppvCardTempModuleStorage; + + [_xmppvCardTempModule addDelegate:self delegateQueue:moduleQueue]; + + _autoClearMyvcard = YES; + } + return self; +} + + +- (void)dealloc { + [_xmppvCardTempModule removeDelegate:self]; + + _moduleStorage = nil; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + +- (BOOL)autoClearMyvcard +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = _autoClearMyvcard; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setAutoClearMyvcard:(BOOL)flag +{ + dispatch_block_t block = ^{ + _autoClearMyvcard = flag; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Public +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSData *)photoDataForJID:(XMPPJID *)jid +{ + // This is a public method, so it may be invoked on any thread/queue. + // + // The vCardTempModule is thread safe. + // The moduleStorage should be thread safe. (User may be using custom module storage class). + // The multicastDelegate is NOT thread safe. + + __block NSData *photoData; + + dispatch_block_t block = ^{ @autoreleasepool { + + photoData = [_moduleStorage photoDataForJID:jid xmppStream:xmppStream]; + + if (photoData == nil) + { + [_xmppvCardTempModule vCardTempForJID:jid shouldFetch:YES]; + } + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return photoData; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStreamDelegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppStreamDidConnect:(XMPPStream *)sender { + XMPPLogTrace(); + + if(self.autoClearMyvcard) + { + /* + * XEP-0153 Section 4.2 rule 1 + * + * A client MUST NOT advertise an avatar image without first downloading the current vCard. + * Once it has done this, it MAY advertise an image. + */ + [_moduleStorage clearvCardTempForJID:[sender myJID] xmppStream:sender]; + } +} + + +- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender { + XMPPLogTrace(); + [_xmppvCardTempModule fetchvCardTempForJID:[sender myJID] ignoreStorage:YES]; +} + + +- (XMPPPresence *)xmppStream:(XMPPStream *)sender willSendPresence:(XMPPPresence *)presence { + XMPPLogTrace(); + + NSXMLElement *currentXElement = [presence elementForName:kXMPPvCardAvatarElement xmlns:kXMPPvCardAvatarNS]; + + //If there is already a x element then remove it + if(currentXElement) + { + NSUInteger currentXElementIndex = [[presence children] indexOfObject:currentXElement]; + + if(currentXElementIndex != NSNotFound) + { + [presence removeChildAtIndex:currentXElementIndex]; + } + } + // add our photo info to the presence stanza + NSXMLElement *photoElement = nil; + NSXMLElement *xElement = [NSXMLElement elementWithName:kXMPPvCardAvatarElement xmlns:kXMPPvCardAvatarNS]; + + NSString *photoHash = [_moduleStorage photoHashForJID:[sender myJID] xmppStream:sender]; + + if (photoHash != nil) + { + photoElement = [NSXMLElement elementWithName:kXMPPvCardAvatarPhotoElement stringValue:photoHash]; + } else { + photoElement = [NSXMLElement elementWithName:kXMPPvCardAvatarPhotoElement]; + } + + [xElement addChild:photoElement]; + [presence addChild:xElement]; + + // Question: If photoElement is nil, should we be adding xElement? + + return presence; +} + + +- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence { + XMPPLogTrace(); + + NSXMLElement *xElement = [presence elementForName:kXMPPvCardAvatarElement xmlns:kXMPPvCardAvatarNS]; + + if (xElement == nil) { + return; + } + + NSXMLElement *photoElement = [xElement elementForName:kXMPPvCardAvatarPhotoElement]; + + if (photoElement == nil) { + return; + } + + NSString *photoHash = [photoElement stringValue]; + + XMPPJID *jid = [presence from]; + + NSString *savedPhotoHash = [_moduleStorage photoHashForJID:jid xmppStream:xmppStream]; + + // check the hash + if ([photoHash caseInsensitiveCompare:savedPhotoHash] != NSOrderedSame + && !([photoHash length] == 0 && [savedPhotoHash length] == 0)) { + [_xmppvCardTempModule fetchvCardTempForJID:jid ignoreStorage:YES]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPvCardTempModuleDelegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppvCardTempModule:(XMPPvCardTempModule *)vCardTempModule + didReceivevCardTemp:(XMPPvCardTemp *)vCardTemp + forJID:(XMPPJID *)jid +{ + XMPPLogTrace(); + + if (vCardTemp.photo != nil) + { + #if TARGET_OS_IPHONE + UIImage *photo = [UIImage imageWithData:vCardTemp.photo]; + #else + NSImage *photo = [[NSImage alloc] initWithData:vCardTemp.photo]; + #endif + + if (photo != nil) + { + [multicastDelegate xmppvCardAvatarModule:self + didReceivePhoto:photo + forJID:jid]; + } + } + + /* + * XEP-0153 4.1.3 + * If the client subsequently obtains an avatar image (e.g., by updating or retrieving the vCard), + * it SHOULD then publish a new stanza with character data in the element. + */ + + if ([[xmppStream myJID] isEqualToJID:jid options:XMPPJIDCompareBare]) + { + XMPPPresence *presence = xmppStream.myPresence; + + if(presence) + { + [xmppStream sendElement:presence]; + } + + + } +} + +- (void)xmppvCardTempModuleDidUpdateMyvCard:(XMPPvCardTempModule *)vCardTempModule{ + //The vCard has been updated on the server so we need to cache it + [_xmppvCardTempModule fetchvCardTempForJID:[xmppStream myJID] ignoreStorage:NO]; +} + +- (void)xmppvCardTempModule:(XMPPvCardTempModule *)vCardTempModule failedToUpdateMyvCard:(NSXMLElement *)error{ + //The vCard failed to update so we fetch the current one from the server + [_xmppvCardTempModule fetchvCardTempForJID:[xmppStream myJID] ignoreStorage:YES]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Getter/setter +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@synthesize xmppvCardTempModule = _xmppvCardTempModule; + + +@end diff --git a/Extensions/XEP-0172/XMPPMessage+XEP_0172.h b/Extensions/XEP-0172/XMPPMessage+XEP_0172.h new file mode 100644 index 0000000..b686913 --- /dev/null +++ b/Extensions/XEP-0172/XMPPMessage+XEP_0172.h @@ -0,0 +1,8 @@ +#import +#import "XMPPMessage.h" + +@interface XMPPMessage (XEP_0172) + +- (NSString *)nick; + +@end diff --git a/Extensions/XEP-0172/XMPPMessage+XEP_0172.m b/Extensions/XEP-0172/XMPPMessage+XEP_0172.m new file mode 100644 index 0000000..8cc97d5 --- /dev/null +++ b/Extensions/XEP-0172/XMPPMessage+XEP_0172.m @@ -0,0 +1,13 @@ +#import "XMPPMessage+XEP_0172.h" +#import "NSXMLElement+XMPP.h" + +#define XMLNS_NICK @"http://jabber.org/protocol/nick" + +@implementation XMPPMessage (XEP_0172) + +- (NSString *)nick +{ + return [[self elementForName:@"nick" xmlns:XMLNS_NICK] stringValue]; +} + +@end diff --git a/Extensions/XEP-0172/XMPPPresence+XEP_0172.h b/Extensions/XEP-0172/XMPPPresence+XEP_0172.h new file mode 100644 index 0000000..04edadc --- /dev/null +++ b/Extensions/XEP-0172/XMPPPresence+XEP_0172.h @@ -0,0 +1,8 @@ +#import +#import "XMPPPresence.h" + +@interface XMPPPresence (XEP_0172) + +- (NSString *)nick; + +@end diff --git a/Extensions/XEP-0172/XMPPPresence+XEP_0172.m b/Extensions/XEP-0172/XMPPPresence+XEP_0172.m new file mode 100644 index 0000000..0cde58e --- /dev/null +++ b/Extensions/XEP-0172/XMPPPresence+XEP_0172.m @@ -0,0 +1,12 @@ +#import "XMPPPresence+XEP_0172.h" +#import "NSXMLElement+XMPP.h" + +#define XMLNS_NICK @"http://jabber.org/protocol/nick" + +@implementation XMPPPresence (XEP_0172) + +- (NSString *)nick{ + return [[self elementForName:@"nick" xmlns:XMLNS_NICK] stringValue]; +} + +@end diff --git a/Extensions/XEP-0184/XMPPMessage+XEP_0184.h b/Extensions/XEP-0184/XMPPMessage+XEP_0184.h new file mode 100644 index 0000000..877e5c3 --- /dev/null +++ b/Extensions/XEP-0184/XMPPMessage+XEP_0184.h @@ -0,0 +1,14 @@ +#import +#import "XMPPMessage.h" + + +@interface XMPPMessage (XEP_0184) + +- (BOOL)hasReceiptRequest; +- (BOOL)hasReceiptResponse; +- (NSString *)receiptResponseID; +- (XMPPMessage *)generateReceiptResponse; + +- (void)addReceiptRequest; + +@end diff --git a/Extensions/XEP-0184/XMPPMessage+XEP_0184.m b/Extensions/XEP-0184/XMPPMessage+XEP_0184.m new file mode 100644 index 0000000..aa4b061 --- /dev/null +++ b/Extensions/XEP-0184/XMPPMessage+XEP_0184.m @@ -0,0 +1,64 @@ +#import "XMPPMessage+XEP_0184.h" +#import "NSXMLElement+XMPP.h" + + +@implementation XMPPMessage (XEP_0184) + +- (BOOL)hasReceiptRequest +{ + NSXMLElement *receiptRequest = [self elementForName:@"request" xmlns:@"urn:xmpp:receipts"]; + + return (receiptRequest != nil); +} + +- (BOOL)hasReceiptResponse +{ + NSXMLElement *receiptResponse = [self elementForName:@"received" xmlns:@"urn:xmpp:receipts"]; + + return (receiptResponse != nil); +} + +- (NSString *)receiptResponseID +{ + NSXMLElement *receiptResponse = [self elementForName:@"received" xmlns:@"urn:xmpp:receipts"]; + + return [receiptResponse attributeStringValueForName:@"id"]; +} + +- (XMPPMessage *)generateReceiptResponse +{ + // Example: + // + // + // + // + + NSXMLElement *received = [NSXMLElement elementWithName:@"received" xmlns:@"urn:xmpp:receipts"]; + + NSXMLElement *message = [NSXMLElement elementWithName:@"message"]; + + NSString *to = [self fromStr]; + if (to) + { + [message addAttributeWithName:@"to" stringValue:to]; + } + + NSString *msgid = [self elementID]; + if (msgid) + { + [received addAttributeWithName:@"id" stringValue:msgid]; + } + + [message addChild:received]; + + return [[self class] messageFromElement:message]; +} + + +- (void)addReceiptRequest +{ + NSXMLElement *receiptRequest = [NSXMLElement elementWithName:@"request" xmlns:@"urn:xmpp:receipts"]; + [self addChild:receiptRequest]; +} + +@end diff --git a/Extensions/XEP-0184/XMPPMessageDeliveryReceipts.h b/Extensions/XEP-0184/XMPPMessageDeliveryReceipts.h new file mode 100644 index 0000000..16070fe --- /dev/null +++ b/Extensions/XEP-0184/XMPPMessageDeliveryReceipts.h @@ -0,0 +1,32 @@ +#import "XMPPModule.h" + +#define _XMPP_MESSAGE_DELIVERY_RECEIPTS_H + +/** + * XMPPMessageDeliveryReceipts can be configured to automatically send delivery receipts and requests in accordance to XEP-0184 +**/ + +@interface XMPPMessageDeliveryReceipts : XMPPModule + +/** + * Automatically add message delivery requests to outgoing messages, in all situations that are permitted in XEP-0184 + * + * - Message MUST NOT be of type 'error' or 'groupchat' + * - Message MUST have an id + * - Message MUST NOT have a delivery receipt or request + * - To must either be a bare JID or a full JID that advertises the urn:xmpp:receipts capability + * + * Default NO +**/ + +@property (assign) BOOL autoSendMessageDeliveryRequests; + +/** + * Automatically send message delivery receipts when a message with a delivery request is received + * + * Default NO +**/ + +@property (assign) BOOL autoSendMessageDeliveryReceipts; + +@end \ No newline at end of file diff --git a/Extensions/XEP-0184/XMPPMessageDeliveryReceipts.m b/Extensions/XEP-0184/XMPPMessageDeliveryReceipts.m new file mode 100644 index 0000000..564bdd9 --- /dev/null +++ b/Extensions/XEP-0184/XMPPMessageDeliveryReceipts.m @@ -0,0 +1,210 @@ +#import "XMPPMessageDeliveryReceipts.h" +#import "XMPPMessage+XEP_0184.h" +#import "XMPPFramework.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#define XMLNS_URN_XMPP_RECEIPTS @"urn:xmpp:receipts" + +@implementation XMPPMessageDeliveryReceipts + +@synthesize autoSendMessageDeliveryRequests; +@synthesize autoSendMessageDeliveryReceipts; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Init/Dealloc +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if((self = [super initWithDispatchQueue:queue])) + { + autoSendMessageDeliveryRequests = NO; + autoSendMessageDeliveryReceipts = NO; + } + + return self; +} + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPModule +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + if ([super activate:aXmppStream]) + { +#ifdef _XMPP_CAPABILITIES_H + [xmppStream autoAddDelegate:self delegateQueue:moduleQueue toModulesOfClass:[XMPPCapabilities class]]; +#endif + return YES; + } + + return NO; +} + +- (void)deactivate +{ +#ifdef _XMPP_CAPABILITIES_H + [xmppStream removeAutoDelegate:self delegateQueue:moduleQueue fromModulesOfClass:[XMPPCapabilities class]]; +#endif + + [super deactivate]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)autoSendMessageDeliveryRequests +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = autoSendMessageDeliveryRequests; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setAutoSendMessageDeliveryRequests:(BOOL)flag +{ + dispatch_block_t block = ^{ + autoSendMessageDeliveryRequests = flag; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (BOOL)autoSendMessageDeliveryReceipts +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = autoSendMessageDeliveryReceipts; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setAutoSendMessageDeliveryReceipts:(BOOL)flag +{ + dispatch_block_t block = ^{ + autoSendMessageDeliveryReceipts = flag; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message +{ + if([message hasReceiptRequest]) + { + if(self.autoSendMessageDeliveryReceipts) + { + XMPPMessage *generatedReceiptResponse = [message generateReceiptResponse]; + [sender sendElement:generatedReceiptResponse]; + } + } +} + +- (XMPPMessage *)xmppStream:(XMPPStream *)sender willSendMessage:(XMPPMessage *)message +{ + if(self.autoSendMessageDeliveryRequests + && [message to] + && ![message isErrorMessage] && ![[[message attributeForName:@"type"] stringValue] isEqualToString:@"groupchat"] + && [[message elementID] length] + && ![message hasReceiptRequest] && ![message hasReceiptResponse]) + { + +#ifdef _XMPP_CAPABILITIES_H + BOOL addReceiptRequest = NO; + + __block XMPPCapabilities *xmppCapabilities = nil; + + [xmppStream enumerateModulesOfClass:[XMPPCapabilities class] withBlock:^(XMPPModule *module, NSUInteger idx, BOOL *stop) { + xmppCapabilities = (XMPPCapabilities *)module; + }]; + + if([[message to] isFull] && [xmppCapabilities.xmppCapabilitiesStorage areCapabilitiesKnownForJID:[message to] xmppStream:sender]) + { + NSXMLElement *capabilities = [xmppCapabilities.xmppCapabilitiesStorage capabilitiesForJID:[message to] xmppStream:xmppStream]; + + for(NSXMLElement *feature in [capabilities children]) + { + if([[feature name] isEqualToString:@"feature"] + && [[feature attributeStringValueForName:@"var"] isEqualToString:XMLNS_URN_XMPP_RECEIPTS]) + { + addReceiptRequest = YES; + break; + } + + } + + } + else + { + addReceiptRequest = YES; + } +#else + BOOL addReceiptRequest = YES; +#endif + + if(addReceiptRequest) + { + [message addReceiptRequest]; + } + } + + return message; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPCapabilities delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifdef _XMPP_CAPABILITIES_H +/** + * If an XMPPCapabilites instance is used we want to advertise our support for XEP-0184. + **/ +- (void)xmppCapabilities:(XMPPCapabilities *)sender collectingMyCapabilities:(NSXMLElement *)query +{ + // This method is invoked on the moduleQueue. + + // + // ... + // + // ... + // + + NSXMLElement *messageDeliveryReceiptsFeatureElement = [NSXMLElement elementWithName:@"feature"]; + [messageDeliveryReceiptsFeatureElement addAttributeWithName:@"var" stringValue:XMLNS_URN_XMPP_RECEIPTS]; + + [query addChild:messageDeliveryReceiptsFeatureElement]; +} +#endif + +@end diff --git a/Extensions/XEP-0191/XMPPBlocking.h b/Extensions/XEP-0191/XMPPBlocking.h new file mode 100644 index 0000000..36734fe --- /dev/null +++ b/Extensions/XEP-0191/XMPPBlocking.h @@ -0,0 +1,132 @@ +#import +#import "XMPPModule.h" + +#if TARGET_OS_IPHONE +#import "DDXML.h" +#endif + +#define _XMPP_BLOCKING_H + +@class XMPPIQ; + +extern NSString *const XMPPBlockingErrorDomain; + +typedef enum XMPPBlockingErrorCode +{ + XMPPBlockingQueryTimeout, // No response from server + XMPPBlockingDisconnect, // XMPP disconnection + +} XMPPBlockingErrorCode; + +@interface XMPPBlocking : XMPPModule +{ + BOOL autoRetrieveBlockingListItems; + BOOL autoClearBlockingListInfo; + + NSMutableDictionary *blockingDict; + + NSMutableDictionary *pendingQueries; +} + +/** + * Whether or not the module should automatically retrieve the blocking list rules. + * If this property is enabled, then the rules for each blocking list are automatically fetched. + * + * In other words, if the blocking list names are fetched (either automatically, or via retrieveListItems), + * then the module will automatically fetch the associated rules. + * + * The default value is YES. + **/ +@property (readwrite, assign) BOOL autoRetrieveBlockingListItems; + +/** + * Whether the module should automatically clear the blocking list info when the client disconnects. + * + * As per the XEP, if there are multiple resources signed in for the user, + * and one resource makes changes to a blocking list, all other resources are "pushed" a notification. + * However, if our client is disconnected when another resource makes the changes, + * then the only way we can find out about the changes are to redownload the blocking lists. + * + * It is recommended to clear the blocking list to assure we have the correct info. + * However, there may be specific situations in which an xmpp client can be sure the blocking list won't change. + * + * The default value is YES. + **/ +@property (readwrite, assign) BOOL autoClearBlockingListInfo; + +/** + * Blocking dict + */ +@property (readonly, strong) NSMutableDictionary *blockingDict; + +/** + * Manual fetch of list names and rules, and manual control over when to clear stored info. + **/ +- (void)retrieveBlockingListItems; +- (void)clearBlockingListInfo; + +/** + * Returns the blocking list. + * + * The result is an array or blocking items (NSXMLElement's). + **/ +- (NSArray*)blockingList; + +/** + * Block JID. + */ +- (void)blockJID:(XMPPJID*)xmppJID; + +/** + * Unblock JID. + */ +- (void)unblockJID:(XMPPJID*)xmppJID; + +/** + * Return whether a jid is in blocking list or not. + */ +- (BOOL)containsJID:(XMPPJID*)xmppJID; + +/** + * Unblock all. + */ +- (void)unblockAll; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPBlockingDelegate +@optional + +/** + * The following delegate methods correspond almost exactly with the action methods of the class. + * There are a few possible ways in which an action could fail: + * + * 1. We receive an error response from the server. + * 2. We receive no response from the server, and the query times out. + * 3. We get disconnected before we receive the response. + * + * In case number 1, the error will be an XMPPIQ of type='error'. + * + * In case number 2 or 3, the error will be an NSError + * with domain=XMPPPrivacyErrorDomain and code from the XMPPBlockingErrorCode enumeration. + **/ + +- (void)xmppBlocking:(XMPPBlocking *)sender didReceivedBlockingList:(NSArray*)blockingList; +- (void)xmppBlocking:(XMPPBlocking *)sender didNotReceivedBlockingListDueToError:(id)error; + +- (void)xmppBlocking:(XMPPBlocking *)sender didReceivePushWithBlockingList:(NSString *)name; + +- (void)xmppBlocking:(XMPPBlocking *)sender didBlockJID:(XMPPJID*)xmppJID; +- (void)xmppBlocking:(XMPPBlocking *)sender didNotBlockJID:(XMPPJID*)xmppJID error:(id)error; + +- (void)xmppBlocking:(XMPPBlocking *)sender didUnblockJID:(XMPPJID*)xmppJID; +- (void)xmppBlocking:(XMPPBlocking *)sender didNotUnblockJID:(XMPPJID*)xmppJID error:(id)error; + +- (void)xmppBlocking:(XMPPBlocking *)sender didUnblockAllWithError:(id)error; +- (void)xmppBlocking:(XMPPBlocking *)sender didNotUnblockAllDueToError:(id)error; + +@end \ No newline at end of file diff --git a/Extensions/XEP-0191/XMPPBlocking.m b/Extensions/XEP-0191/XMPPBlocking.m new file mode 100644 index 0000000..e36d0e1 --- /dev/null +++ b/Extensions/XEP-0191/XMPPBlocking.m @@ -0,0 +1,644 @@ +#import "XMPP.h" +#import "XMPPLogging.h" +#import "XMPPBlocking.h" +#import "NSNumber+XMPP.h" + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#ifdef DEBUG +static const int xmppLogLevel = XMPP_LOG_LEVEL_VERBOSE; // | XMPP_LOG_FLAG_TRACE; +#else +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +#define QUERY_TIMEOUT 30.0 // NSTimeInterval (double) = seconds + +NSString *const XMPPBlockingErrorDomain = @"XMPPBlockingErrorDomain"; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +typedef enum XMPPBlockingQueryInfoType { + FetchBlockingList, + BlockUser, + UnblockUser, + UnblockAll, + +} XMPPBlockingQueryInfoType; + +@interface XMPPBlockingQueryInfo : NSObject +{ + XMPPBlockingQueryInfoType type; + XMPPJID *blockingXMPPJID; + NSArray *blockingListItems; + + dispatch_source_t timer; +} + +@property (nonatomic, readonly) XMPPBlockingQueryInfoType type; +@property (nonatomic, readonly) NSArray *blockingListItems; + +@property (nonatomic, readwrite) XMPPJID *blockingXMPPJID; +@property (nonatomic, readwrite) dispatch_source_t timer; + +- (void)cancel; + ++ (XMPPBlockingQueryInfo *)queryInfoWithType:(XMPPBlockingQueryInfoType)type; ++ (XMPPBlockingQueryInfo *)queryInfoWithType:(XMPPBlockingQueryInfoType)type jid:(XMPPJID *)jid; ++ (XMPPBlockingQueryInfo *)queryInfoWithType:(XMPPBlockingQueryInfoType)type jid:(XMPPJID *)jid items:(NSArray *)items; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPBlocking (/* Must be nameless for properties */) + +- (void)addQueryInfo:(XMPPBlockingQueryInfo *)qi withKey:(NSString *)uuid; +- (void)queryTimeout:(NSString *)uuid; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPBlocking + +@synthesize blockingDict = _blockingDict; + +- (id)init +{ + return [self initWithDispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super initWithDispatchQueue:queue])) + { + autoRetrieveBlockingListItems = YES; + autoClearBlockingListInfo = YES; + + blockingDict = [[NSMutableDictionary alloc] init]; + + pendingQueries = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + if ([super activate:aXmppStream]) + { + // Reserved for possible future use. + + return YES; + } + + return NO; +} + +- (void)deactivate +{ + // Reserved for possible future use. + + [super deactivate]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)autoRetrieveBlockingListItems +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return autoRetrieveBlockingListItems; + } + else + { + __block BOOL result; + + dispatch_sync(moduleQueue, ^{ + result = autoRetrieveBlockingListItems; + }); + + return result; + } +} + +- (void)setAutoRetrieveBlockingListItems:(BOOL)flag +{ + dispatch_block_t block = ^{ + + autoRetrieveBlockingListItems = flag; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (BOOL)autoClearBlockingListInfo +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return autoClearBlockingListInfo; + } + else + { + __block BOOL result; + + dispatch_sync(moduleQueue, ^{ + result = autoClearBlockingListInfo; + }); + + return result; + } +} + +- (void)setAutoClearBlockingListInfo:(BOOL)flag +{ + dispatch_block_t block = ^{ + + autoClearBlockingListInfo = flag; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Public API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)retrieveBlockingListItems +{ + XMPPLogTrace(); + + // + // + // + + NSXMLElement *block = [NSXMLElement elementWithName:@"blocklist" xmlns:@"urn:xmpp:blocking"]; + + NSString *uuid = [xmppStream generateUUID]; + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:nil elementID:uuid child:block]; + + [xmppStream sendElement:iq]; + + XMPPBlockingQueryInfo *qi = [XMPPBlockingQueryInfo queryInfoWithType:FetchBlockingList]; + [self addQueryInfo:qi withKey:uuid]; +} + +- (void)clearBlockingListInfo +{ + XMPPLogTrace(); + + if (dispatch_get_specific(moduleQueueTag)) + { + [blockingDict removeAllObjects]; + } + else + { + dispatch_async(moduleQueue, ^{ @autoreleasepool { + + [blockingDict removeAllObjects]; + }}); + } +} + +- (NSArray*)blockingList +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return [blockingDict allKeys]; + } + else + { + __block NSArray *result; + + dispatch_sync(moduleQueue, ^{ @autoreleasepool { + + result = [[blockingDict allKeys] copy]; + }}); + + return result; + } +} + +- (void)blockJID:(XMPPJID*)xmppJID +{ + XMPPLogTrace(); + + id value = blockingDict[[xmppJID full]]; + if (value == nil) + { + blockingDict[[xmppJID full]] = [NSNull null]; + } + + // + // + // + // + // + + NSXMLElement *block = [NSXMLElement elementWithName:@"block" xmlns:@"urn:xmpp:blocking"]; + NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; + [item addAttributeWithName:@"jid" stringValue:[xmppJID full]]; + [block addChild:item]; + + NSString *uuid = [xmppStream generateUUID]; + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:nil elementID:uuid child:block]; + [iq addAttributeWithName:@"from" stringValue:xmppStream.myJID.bare]; + + [xmppStream sendElement:iq]; + + XMPPBlockingQueryInfo *qi = [XMPPBlockingQueryInfo queryInfoWithType:BlockUser]; + qi.blockingXMPPJID = xmppJID; + [self addQueryInfo:qi withKey:uuid]; +} + +- (void)unblockJID:(XMPPJID*)xmppJID +{ + XMPPLogTrace(); + + id value = blockingDict[[xmppJID full]]; + if (value != nil) + { + [blockingDict removeObjectForKey:[xmppJID full]]; + } + + // + // + // + // + // + + NSXMLElement *block = [NSXMLElement elementWithName:@"unblock" xmlns:@"urn:xmpp:blocking"]; + NSXMLElement *item = [NSXMLElement elementWithName:@"item"]; + [item addAttributeWithName:@"jid" stringValue:[xmppJID full]]; + [block addChild:item]; + + NSString *uuid = [xmppStream generateUUID]; + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:nil elementID:uuid child:block]; + + [xmppStream sendElement:iq]; + + XMPPBlockingQueryInfo *qi = [XMPPBlockingQueryInfo queryInfoWithType:UnblockUser]; + qi.blockingXMPPJID = xmppJID; + [self addQueryInfo:qi withKey:uuid]; +} + +- (BOOL)containsJID:(XMPPJID*)xmppJID +{ + if (blockingDict[[xmppJID full]]) + { + return true; + } + return false; +} + +/** + * Unblock all. + */ +- (void)unblockAll +{ + XMPPLogTrace(); + + // + // + // + + NSXMLElement *block = [NSXMLElement elementWithName:@"unblock" xmlns:@"urn:xmpp:blocking"]; + + NSString *uuid = [xmppStream generateUUID]; + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" to:nil elementID:uuid child:block]; + + [xmppStream sendElement:iq]; + + XMPPBlockingQueryInfo *qi = [XMPPBlockingQueryInfo queryInfoWithType:UnblockAll]; + [self addQueryInfo:qi withKey:uuid]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Query Processing +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)addQueryInfo:(XMPPBlockingQueryInfo *)queryInfo withKey:(NSString *)uuid +{ + // Setup timer + + dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue); + + dispatch_source_set_event_handler(timer, ^{ @autoreleasepool { + + [self queryTimeout:uuid]; + }}); + + dispatch_time_t fireTime = dispatch_time(DISPATCH_TIME_NOW, (QUERY_TIMEOUT * NSEC_PER_SEC)); + + dispatch_source_set_timer(timer, fireTime, DISPATCH_TIME_FOREVER, 1.0); + dispatch_resume(timer); + + queryInfo.timer = timer; + + // Add to dictionary + pendingQueries[uuid] = queryInfo; +} + +- (void)removeQueryInfo:(XMPPBlockingQueryInfo *)queryInfo withKey:(NSString *)uuid +{ + // Invalidate timer + [queryInfo cancel]; + + // Remove from dictionary + [pendingQueries removeObjectForKey:uuid]; +} + +- (void)processQuery:(XMPPBlockingQueryInfo *)queryInfo withFailureCode:(XMPPBlockingErrorCode)errorCode +{ + NSError *error = [NSError errorWithDomain:XMPPBlockingErrorDomain code:errorCode userInfo:nil]; + + if (queryInfo.type == FetchBlockingList) + { + [multicastDelegate xmppBlocking:self didNotReceivedBlockingListDueToError:error]; + } + else if (queryInfo.type == BlockUser) + { + [multicastDelegate xmppBlocking:self didNotBlockJID:queryInfo.blockingXMPPJID error:error]; + } + else if (queryInfo.type == UnblockUser) + { + [multicastDelegate xmppBlocking:self didNotUnblockJID:queryInfo.blockingXMPPJID error:error]; + } + else if (queryInfo.type == UnblockAll) + { + [multicastDelegate xmppBlocking:self didNotUnblockAllDueToError:error]; + } +} + +- (void)queryTimeout:(NSString *)uuid +{ + XMPPBlockingQueryInfo *queryInfo = pendingQueries[uuid]; + if (queryInfo) + { + [self processQuery:queryInfo withFailureCode:XMPPBlockingQueryTimeout]; + [self removeQueryInfo:queryInfo withKey:uuid]; + } +} + +- (void)processQueryResponse:(XMPPIQ *)iq withInfo:(XMPPBlockingQueryInfo *)queryInfo +{ + if (queryInfo.type == FetchBlockingList) + { + // Blocking List Query Response: + // + // + // + // + // + // + // + + if ([[iq type] isEqualToString:@"result"]) + { + NSXMLElement *blocklist = [iq elementForName:@"blocklist" xmlns:@"urn:xmpp:blocking"]; + if (blocklist == nil) return; + + NSArray *listItems = [blocklist elementsForName:@"item"]; + for (NSXMLElement *listItem in listItems) + { + NSString *name = [listItem attributeStringValueForName:@"jid"]; + if (name) + { + id value = blockingDict[name]; + if (value == nil) + { + blockingDict[name] = [NSNull null]; + } + } + } + + [multicastDelegate xmppBlocking:self didReceivedBlockingList:[self blockingList]]; + [self removeQueryInfo:queryInfo withKey:[iq elementID]]; + } + else + { + [multicastDelegate xmppBlocking:self didNotReceivedBlockingListDueToError:iq]; + } + } + else if (queryInfo.type == BlockUser) + { + // + + if ([[iq type] isEqualToString:@"result"]) + { + [self removeQueryInfo:queryInfo withKey:[iq elementID]]; + [multicastDelegate xmppBlocking:self didBlockJID:queryInfo.blockingXMPPJID]; + } + else + { + [blockingDict removeObjectForKey:[queryInfo.blockingXMPPJID full]]; + [multicastDelegate xmppBlocking:self didNotBlockJID:queryInfo.blockingXMPPJID error:iq]; + } + } + else if (queryInfo.type == UnblockUser) + { + // + + if ([[iq type] isEqualToString:@"result"]) + { + [self removeQueryInfo:queryInfo withKey:[iq elementID]]; + [multicastDelegate xmppBlocking:self didUnblockJID:queryInfo.blockingXMPPJID]; + } + else + { + XMPPBlockingQueryInfo *queryInfo = pendingQueries[[iq elementID]]; + + id value = blockingDict[[queryInfo.blockingXMPPJID full]]; + if (value == nil) + { + blockingDict[[queryInfo.blockingXMPPJID full]] = [NSNull null]; + } + + [multicastDelegate xmppBlocking:self didNotBlockJID:queryInfo.blockingXMPPJID error:iq]; + } + } + else if (queryInfo.type == UnblockAll) + { + // + + if ([[iq type] isEqualToString:@"result"]) + { + [self removeQueryInfo:queryInfo withKey:[iq elementID]]; + [multicastDelegate xmppBlocking:self didUnblockAllWithError:nil]; + } + else + { + XMPPBlockingQueryInfo *queryInfo = pendingQueries[[iq elementID]]; + + id value = blockingDict[[queryInfo.blockingXMPPJID full]]; + if (value == nil) + { + blockingDict[queryInfo.blockingXMPPJID] = [NSNull null]; + } + + [multicastDelegate xmppBlocking:self didNotUnblockAllDueToError:iq]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender +{ + if (self.autoRetrieveBlockingListItems) + { + [self retrieveBlockingListItems]; + } +} + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + NSString *type = [iq type]; + + if ([type isEqualToString:@"set"]) + { + NSXMLElement *block = [iq elementForName:@"block" xmlns:@"urn:xmpp:blocking"]; + NSXMLElement *unblock = [iq elementForName:@"unblock" xmlns:@"urn:xmpp:blocking"]; + + if (block || unblock) + { + NSXMLElement *list = [block elementForName:@"item"]; + + if (!list) + { + list = [unblock elementForName:@"item"]; + } + + NSString *itemName = [list attributeStringValueForName:@"jid"]; + if (itemName == nil) + { + return NO; + } + + [multicastDelegate xmppBlocking:self didReceivePushWithBlockingList:itemName]; + + XMPPIQ *iqResponse = [XMPPIQ iqWithType:@"result" to:[iq from] elementID:[iq elementID]]; + [xmppStream sendElement:iqResponse]; + + if (self.autoRetrieveBlockingListItems) + { + [self retrieveBlockingListItems]; + } + + return YES; + } + } + else + { + // This may be a response to a query we sent + + XMPPBlockingQueryInfo *queryInfo = pendingQueries[[iq elementID]]; + + + if (queryInfo) + { + [self processQueryResponse:iq withInfo:queryInfo]; + + return YES; + } + } + + return NO; +} + +-(void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error +{ + // If there are any pending queries, + // they just failed due to the disconnection. + + for (NSString *uuid in pendingQueries) + { + XMPPBlockingQueryInfo *queryInfo = pendingQueries[uuid]; + + [self processQuery:queryInfo withFailureCode:XMPPBlockingDisconnect]; + } + + // Clear the list of pending queries + + [pendingQueries removeAllObjects]; + + // Maybe clear all stored blocking info + + if (self.autoClearBlockingListInfo) + { + [self clearBlockingListInfo]; + } +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPBlockingQueryInfo + +@synthesize type; +@synthesize blockingXMPPJID; +@synthesize blockingListItems; +@synthesize timer; + +- (id)initWithType:(XMPPBlockingQueryInfoType)aType jid:(XMPPJID *)jid items:(NSArray *)items +{ + if ((self = [super init])) + { + type = aType; + blockingXMPPJID = [jid copy]; + blockingListItems = [items copy]; + } + return self; +} + +- (void)cancel +{ + if (timer) + { + dispatch_source_cancel(timer); +#if !OS_OBJECT_USE_OBJC + dispatch_release(timer); +#endif + timer = NULL; + } +} + +- (void)dealloc +{ + [self cancel]; +} + ++ (XMPPBlockingQueryInfo *)queryInfoWithType:(XMPPBlockingQueryInfoType)type +{ + return [self queryInfoWithType:type jid:nil items:nil]; +} + ++ (XMPPBlockingQueryInfo *)queryInfoWithType:(XMPPBlockingQueryInfoType)type jid:(XMPPJID *)jid +{ + return [self queryInfoWithType:type jid:jid items:nil]; +} + ++ (XMPPBlockingQueryInfo *)queryInfoWithType:(XMPPBlockingQueryInfoType)type jid:(XMPPJID *)jid items:(NSArray *)items +{ + return [[XMPPBlockingQueryInfo alloc] initWithType:type jid:jid items:items]; +} + +@end diff --git a/Extensions/XEP-0198/Memory Storage/XMPPStreamManagementMemoryStorage.h b/Extensions/XEP-0198/Memory Storage/XMPPStreamManagementMemoryStorage.h new file mode 100644 index 0000000..b8e4999 --- /dev/null +++ b/Extensions/XEP-0198/Memory Storage/XMPPStreamManagementMemoryStorage.h @@ -0,0 +1,14 @@ +#import +#import "XMPPStreamManagement.h" + +/** + * This class provides an in-memory only storage system for XMPPStreamManagement. + * As such, it will only support stream resumption so long as the application doesn't terminate. + * + * This class should be considered primarily for testing. + * An application making use of stream management should likely transition + * to a persistent storage layer before distribution. +**/ +@interface XMPPStreamManagementMemoryStorage : NSObject + +@end diff --git a/Extensions/XEP-0198/Memory Storage/XMPPStreamManagementMemoryStorage.m b/Extensions/XEP-0198/Memory Storage/XMPPStreamManagementMemoryStorage.m new file mode 100644 index 0000000..8a4397a --- /dev/null +++ b/Extensions/XEP-0198/Memory Storage/XMPPStreamManagementMemoryStorage.m @@ -0,0 +1,211 @@ +#import "XMPPStreamManagementMemoryStorage.h" +#import + + +@interface XMPPStreamManagementMemoryStorage () + +@property (atomic, weak, readwrite) XMPPStreamManagement *parent; +@end + +#pragma mark - + +@implementation XMPPStreamManagementMemoryStorage +{ + int32_t isConfigured; + + NSString *resumptionId; + uint32_t timeout; + + NSDate *lastDisconnect; + uint32_t lastHandledByClient; + uint32_t lastHandledByServer; + NSArray *pendingOutgoingStanzas; + +} + +- (BOOL)configureWithParent:(XMPPStreamManagement *)parent queue:(dispatch_queue_t)queue +{ + // This implementation only supports a single xmppStream. + // You must create multiple instances for multiple xmppStreams. + + return OSAtomicCompareAndSwap32(0, 1, &isConfigured); +} + +/** + * Invoked after we receive from the server. + * + * @param resumptionId + * The ID required to resume the session, given to us by the server. + * + * @param timeout + * The timeout in seconds. + * After a disconnect, the server will maintain our state for this long. + * If we attempt to resume the session after this timeout it likely won't work. + * + * @param lastDisconnect + * Used to reset the lastDisconnect value. + * This value is often updated during the session, to ensure it closely resemble the date the server will use. + * That is, if the client application is killed (or crashes) we want a relatively accurate lastDisconnect date. + * + * @param stream + * The associated xmppStream (standard parameter for storage classes) + * + * This method should also nil out the following values (if needed) associated with the account: + * - lastHandledByClient + * - lastHandledByServer + * - pendingOutgoingStanzas +**/ +- (void)setResumptionId:(NSString *)inResumptionId + timeout:(uint32_t)inTimeout + lastDisconnect:(NSDate *)inLastDisconnect + forStream:(XMPPStream *)stream +{ + resumptionId = inResumptionId; + timeout = inTimeout; + lastDisconnect = inLastDisconnect; + + lastHandledByClient = 0; + lastHandledByServer = 0; + pendingOutgoingStanzas = nil; +} + +/** + * This method is invoked ** often ** during stream operation. + * It is not invoked when the xmppStream is disconnected. + * + * Important: See the note [in XMPPStreamManagement.h]: "Optimizing storage demands during active stream usage" + * + * @param date + * Updates the previous lastDisconnect value. + * + * @param lastHandledByClient + * The most recent 'h' value we can safely send to the server. + * + * @param stream + * The associated xmppStream (standard parameter for storage classes) +**/ +- (void)setLastDisconnect:(NSDate *)inLastDisconnect + lastHandledByClient:(uint32_t)inLastHandledByClient + forStream:(XMPPStream *)stream +{ + lastDisconnect = inLastDisconnect; + lastHandledByClient = inLastHandledByClient; +} + +/** + * This method is invoked ** often ** during stream operation. + * It is not invoked when the xmppStream is disconnected. + * + * Important: See the note [in XMPPStreamManagement.h]: "Optimizing storage demands during active stream usage" + * + * @param date + * Updates the previous lastDisconnect value. + * + * @param lastHandledByServer + * The most recent 'h' value we've received from the server. + * + * @param pendingOutgoingStanzas + * An array of XMPPStreamManagementOutgoingStanza objects. + * The storage layer is in charge of properly persisting this array, including: + * - the array count + * - the stanzaId of each element, including those that are nil + * + * @param stream + * The associated xmppStream (standard parameter for storage classes) +**/ +- (void)setLastDisconnect:(NSDate *)inLastDisconnect + lastHandledByServer:(uint32_t)inLastHandledByServer + pendingOutgoingStanzas:(NSArray *)inPendingOutgoingStanzas + forStream:(XMPPStream *)stream +{ + lastDisconnect = inLastDisconnect; + lastHandledByServer = inLastHandledByServer; + pendingOutgoingStanzas = inPendingOutgoingStanzas; +} + +/** + * This method is invoked immediately after an accidental disconnect. + * And may be invoked post-disconnect if the state changes, such as for the following edge cases: + * + * - due to continued processing of stanzas received pre-disconnect, + * that are just now being marked as handled by the delegate(s) + * - due to a delayed response from the delegate(s), + * such that we didn't receive the stanzaId for an outgoing stanza until after the disconnect occurred. + * + * This method is not invoked if stream management is started on a connected xmppStream. + * + * @param date + * This value will be the actual disconnect date. + * + * @param lastHandledByClient + * The most recent 'h' value we can safely send to the server. + * + * @param lastHandledByServer + * The most recent 'h' value we've received from the server. + * + * @param pendingOutgoingStanzas + * An array of XMPPStreamManagementOutgoingStanza objects. + * The storage layer is in charge of properly persisting this array, including: + * - the array count + * - the stanzaId of each element, including those that are nil + * + * @param stream + * The associated xmppStream (standard parameter for storage classes) +**/ +- (void)setLastDisconnect:(NSDate *)inLastDisconnect + lastHandledByClient:(uint32_t)inLastHandledByClient + lastHandledByServer:(uint32_t)inLastHandledByServer + pendingOutgoingStanzas:(NSArray *)inPendingOutgoingStanzas + forStream:(XMPPStream *)stream +{ + lastDisconnect = inLastDisconnect; + lastHandledByClient = inLastHandledByClient; + lastHandledByServer = inLastHandledByServer; + pendingOutgoingStanzas = inPendingOutgoingStanzas; +} + +/** + * Invoked when the extension needs values from a previous session. + * This method is used to get values needed in order to determine if it can resume a previous stream. +**/ +- (void)getResumptionId:(NSString **)resumptionIdPtr + timeout:(uint32_t *)timeoutPtr + lastDisconnect:(NSDate **)lastDisconnectPtr + forStream:(XMPPStream *)stream +{ + if (resumptionIdPtr) *resumptionIdPtr = resumptionId; + if (timeoutPtr) *timeoutPtr = timeout; + if (lastDisconnectPtr) *lastDisconnectPtr = lastDisconnect; +} + +/** + * Invoked when the extension needs values from a previous session. + * This method is used to get values needed in order to resume a previous stream. +**/ +- (void)getLastHandledByClient:(uint32_t *)lastHandledByClientPtr + lastHandledByServer:(uint32_t *)lastHandledByServerPtr + pendingOutgoingStanzas:(NSArray **)pendingOutgoingStanzasPtr + forStream:(XMPPStream *)stream; +{ + if (lastHandledByClientPtr) *lastHandledByClientPtr = lastHandledByClient; + if (lastHandledByServerPtr) *lastHandledByServerPtr = lastHandledByServer; + if (pendingOutgoingStanzasPtr) *pendingOutgoingStanzasPtr = pendingOutgoingStanzas; +} + +/** + * Instructs the storage layer to remove all values stored for the given stream. + * This occurs after the extension detects a "cleanly closed stream", + * in which case the stream cannot be resumed next time. +**/ +- (void)removeAllForStream:(XMPPStream *)stream +{ + resumptionId = nil; + timeout = 0; + + lastDisconnect = nil; + lastHandledByClient = 0; + lastHandledByServer = 0; + pendingOutgoingStanzas = nil; +} + +@end diff --git a/Extensions/XEP-0198/Private/XMPPStreamManagementStanzas.h b/Extensions/XEP-0198/Private/XMPPStreamManagementStanzas.h new file mode 100644 index 0000000..78732ef --- /dev/null +++ b/Extensions/XEP-0198/Private/XMPPStreamManagementStanzas.h @@ -0,0 +1,35 @@ +#import +#import "XMPPElement.h" + +/** + * An outgoing stanza. + * + * The translation from element to stanzaId may be an asynchronous process, + * so this structure is used to assist in the process. +**/ +@interface XMPPStreamManagementOutgoingStanza : NSObject + +- (instancetype)initAwaitingStanzaId; +- (instancetype)initWithStanzaId:(id)stanzaId; + +@property (nonatomic, strong, readwrite) id stanzaId; +@property (nonatomic, assign, readwrite) BOOL awaitingStanzaId; + +@end + +#pragma mark - + +/** + * An incoming stanza. + * + * The translation from element to stanzaId may be an asynchronous process, + * so this structure is used to assist in the process. +**/ +@interface XMPPStreamManagementIncomingStanza : NSObject + +- (instancetype)initWithStanzaId:(id)stanzaId isHandled:(BOOL)isHandled; + +@property (nonatomic, strong, readwrite) id stanzaId; +@property (nonatomic, assign, readwrite) BOOL isHandled; + +@end diff --git a/Extensions/XEP-0198/Private/XMPPStreamManagementStanzas.m b/Extensions/XEP-0198/Private/XMPPStreamManagementStanzas.m new file mode 100644 index 0000000..e36f888 --- /dev/null +++ b/Extensions/XEP-0198/Private/XMPPStreamManagementStanzas.m @@ -0,0 +1,85 @@ +#import "XMPPStreamManagementStanzas.h" + + +@implementation XMPPStreamManagementOutgoingStanza + +@synthesize awaitingStanzaId = awaitingStanzaId; +@synthesize stanzaId = stanzaId; + +/** + * Use when the stanzaId is unknown, and we are awaiting a stanzaId from the delegate(s). +**/ +- (instancetype)initAwaitingStanzaId +{ + if ((self = [super init])) + { + awaitingStanzaId = YES; + } + return self; +} + +/** + * Use when the stanzaId is known, meaning we are NOT awaiting a stanzaId from the delegate(s). + * The stanzaId may be nil. +**/ +- (instancetype)initWithStanzaId:(id)inStanzaId +{ + if ((self = [super init])) + { + stanzaId = inStanzaId; + awaitingStanzaId = NO; + } + return self; +} + +/* NSCopying */ + +- (id)copyWithZone:(NSZone *)zone +{ + XMPPStreamManagementOutgoingStanza *copy = [[XMPPStreamManagementOutgoingStanza alloc] init]; + copy->awaitingStanzaId = awaitingStanzaId; + copy->stanzaId = stanzaId; + + return copy; +} + +/* NSCoding */ + +- (id)initWithCoder:(NSCoder *)decoder +{ + if ((self = [super init])) + { + awaitingStanzaId = [decoder decodeBoolForKey:@"awaitingStanzaId"]; + stanzaId = [decoder decodeObjectForKey:@"stanzaId"]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder +{ + [coder encodeBool:awaitingStanzaId forKey:@"awaitingStanzaId"]; + [coder encodeObject:stanzaId forKey:@"stanzaId"]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPStreamManagementIncomingStanza + +@synthesize stanzaId = stanzaId; +@synthesize isHandled = isHandled; + +- (instancetype)initWithStanzaId:(id)inStanzaId isHandled:(BOOL)inIsHandled +{ + if ((self = [super init])) + { + stanzaId = inStanzaId; + isHandled = inIsHandled; + } + return self; +} + +@end diff --git a/Extensions/XEP-0198/XMPPStreamManagement.h b/Extensions/XEP-0198/XMPPStreamManagement.h new file mode 100644 index 0000000..f49fec4 --- /dev/null +++ b/Extensions/XEP-0198/XMPPStreamManagement.h @@ -0,0 +1,617 @@ +#import +#import "XMPP.h" + +#define _XMPP_STREAM_MANAGEMENT_H + +@protocol XMPPStreamManagementStorage; + + +@interface XMPPStreamManagement : XMPPModule + +/** + * The XMPPStreamManagement extension implements XEP-0198: + * http://xmpp.org/extensions/xep-0198.html + * + * @param storage + * You must configure the extension with a storage module. + * A persistent storage layer is recommended for distribution. + * For testing, or if you're not planning on using stream resumption, then the memory storage solution will work. + * + * @param queue + * The standard dispatch_queue option, with which to run the extension on. +**/ +- (id)initWithStorage:(id )storage; +- (id)initWithStorage:(id )storage dispatchQueue:(dispatch_queue_t)queue; + +@property (nonatomic, strong, readonly) id storage; + +#pragma mark Enable + +/** + * This method sends the stanza to the server to request enabling stream management. + * + * XEP-0198 specifies that the stanza should only be sent by clients after authentication, + * and after binding has occurred. + * + * The servers response is reported via the delegate methods: + * @see xmppStreamManagement:wasEnabled: + * @see xmppStreamManagement:wasNotEnabled: + * + * @param supportsResumption + * Whether the client should request resumptions support. + * If YES, the resume attribute will be included. E.g. + * + * @param maxTimeout + * Allows you to specify the client's preferred maximum resumption time. + * This is optional, and will only be sent if you provide a positive value (maxTimeout > 0.0). + * Note that XEP-0198 only supports sending this value in seconds. + * So it the provided maxTimeout includes millisecond precision, this will be ignored via truncation + * (rounding down to nearest whole seconds value). + * + * @see supportsStreamManagement +**/ +- (void)enableStreamManagementWithResumption:(BOOL)supportsResumption maxTimeout:(NSTimeInterval)maxTimeout; + +#pragma mark Resume + +/** + * If set to YES, then the extension will automatically attempt to resume any sessions that appear resumable. + * + * That is, if the canResumeStream method would return YES, then the module will automatically plug into the xmppStream, + * and attempts to resume the session. If the attempt fails, the xmppStream will automatically fall back to + * the standard binding process. + * + * Remember: If the extension does not believe that resumption is possible, then it won't attempt to resume. + * That is, if it doesn't have data in storage that matches the current connection, or the data is expired, + * then it allows the xmppStream to perform standard binding immediately, without attempting to resume. + * + * If you wish to handle stream resumption manually, then you can simply implement xmppStreamWillBind:, + * and return this extension instance according to your own conditions. + * + * In order to determine if a stream was resumed, you should invoke didResumeWithAckedStanzaIds:serverResponse: + * from within the xmppStreamDidAuthenticate: callback. + * + * The default value is NO. +**/ +@property (atomic, readwrite) BOOL autoResume; + +/** + * This method is meant to be called by other extensions when they receive an xmppStreamDidAuthenticate callback. + * + * Returns YES if the stream was resumed during the authentication process. + * Returns NO otherwise (if resume wasn't available, or it failed). + * + * Other extensions may wish to skip certain setup processes that aren't + * needed if the stream was resumed (since the previous session state has been restored server-side). +**/ +@property (atomic, readonly) BOOL didResume; + +/** + * This method is meant to be called when you receive an xmppStreamDidAuthenticate callback. + * + * It is used instead of a standard delegate method in order to provide a cleaner API. + * By using this method, one can put all the logic for handling authentication in a single place. + * But more importantly, it solves several subtle timing and threading issues. + * + * > A delegate method could have hit either before or after xmppStreamDidAuthenticate, depending on thread scheduling. + * > We could have queued it up, and forced it to hit after. + * > But your code would likely still have needed to add a check within xmppStreamDidAuthenticate... + * + * @param stanzaIdsPtr (optional) + * Just like the stanzaIdsPtr provided in xmppStreamManagement:didReceiveAckForStanzaIds:. + * This comes from the h value provided within the stanza sent by the server. + * + * @param responsePtr (optional) + * Returns the response we got from the server. Either or . + * This will be nil if resume wasn't tried. + * + * @return + * YES if the stream was resumed. + * NO otherwise. +**/ +- (BOOL)didResumeWithAckedStanzaIds:(NSArray **)stanzaIdsPtr + serverResponse:(NSXMLElement **)responsePtr; + +/** + * Returns YES if the stream can be resumed. + * + * This would be the case if there's an available resumptionId for the authenticated xmppStream, + * and the timeout from the last stream has not been exceeded. +**/ +- (BOOL)canResumeStream; + + +#pragma mark Requesting Acks + +/** + * Sends a request element, requesting the server reply with an ack . + * + * You can also configure the extension to automatically sends requests. + * @see automaticallyRequestAcksAfterStanzaCount:orTimeout: + * + * When the server replies with an ack, the delegate method will be invoked. + * @see xmppStreamManagement:didReceiveAckForStanzaIds: +**/ +- (void)requestAck; + +/** + * The module can be configured to automatically request acks (send ) based on your criteria. + * The algorithm to do this takes into account: + * + * - The number of stanzas that have been sent since the last request was sent. + * - The amount of time that has elapsed since the first stanza (after the last request) was sent. + * + * So, for example, if you set the stanzaCount to 5, and the timeout to 2.0 seconds then: + * - Sending 5 stanzas back-to-back will automatically trigger an outgoing request + * - Sending 1 stanza will automatically trigger an outgoing request to be sent 2.0 seconds later, + * which will get preempted if 4 more stanzas are sent before the 2.0 second timer expires. + * + * In other words, whichever event takes place FIRST will trigger the request to be sent. + * + * You can disable either trigger by setting its value to zero. + * So, for example, if you only want to use a timeout of 5 seconds, + * then you could set the stanzaCount to zero and the timeout to 5 seconds. + * + * @param stanzaCount + * The stanzaCount to use for the auto request algorithm. + * If stanzaCount is zero, then the number of stanzas will be ignored in the algorithm. + * + * @param timeout + * The timeout to use for the auto request algorithm. + * If the timeout is zero (or negative), then the timer will be ignored in the algorithm. + * + * The default stanzaCount is 0 (disabled). + * The default timeout is 0.0 seconds (disabled). +**/ +- (void)automaticallyRequestAcksAfterStanzaCount:(NSUInteger)stanzaCount orTimeout:(NSTimeInterval)timeout; + +/** + * Returns the current auto-request configuration. + * + * @see automaticallyRequestAcksAfterStanzaCount:orTimeout: +**/ +- (void)getAutomaticallyRequestAcksAfterStanzaCount:(NSUInteger *)stanzaCountPtr orTimeout:(NSTimeInterval *)timeoutPtr; + + +#pragma mark Sending Acks + +/** + * Sends an unrequested ack element, acking the server's recently received (and handled) elements. + * + * You can also configure the extension to automatically sends acks. + * @see automaticallySendAcksAfterStanzaCount:orTimeout: + * + * Keep in mind that the extension will automatically send an ack if it receives an explicit request. +**/ +- (void)sendAck; + +/** + * The module can be configured to automatically send unrequested acks. + * That is, rather than waiting to receive explicit requests from the server, + * the client automatically sends them based on configurable criteria. + * + * The algorithm to do this takes into account: + * + * - The number of stanzas that have been received since the last ack was sent. + * - The amount of time that has elapsed since the first stanza (after the last ack) was received. + * + * In other words, whichever event takes place FIRST will trigger the request to be sent. + * You can disable either trigger by setting its value to zero. + * + * As would be expected, if you manually send an unrequested ack (via the sendAck method), + * or if an ack is sent out in response to a received request from the server, + * then the stanzaCount & timeout are reset. + * + * @param stanzaCount + * The stanzaCount to use for the auto ack algorithm. + * If stanzaCount is zero, then the number of stanzas will be ignored in the algorithm. + * + * @param timeout + * The timeout to sue fo the auto ack algorithm. + * If the timeout is zero (or negative), then the timer will be ignored in the algorithm. + * + * The default stanzaCount is 0 (disabled). + * The default timeout is 0.0 seconds (disabled). +**/ +- (void)automaticallySendAcksAfterStanzaCount:(NSUInteger)stanzaCount orTimeout:(NSTimeInterval)timeout; + +/** + * Returns the current "auto-send unrequested acks" configuration. + * + * @see automaticallySendAcksAfterStanzaCount:orTimeout: +**/ +- (void)getAutomaticallySendAcksAfterStanzaCount:(NSUInteger *)stanzaCountPtr orTimeout:(NSTimeInterval *)timeoutPtr; + +/** + * If an explicit request is received from the server, should we delay sending the ack ? + * From XEP-0198 : + * + * > When an element ("request") is received, the recipient MUST acknowledge it by sending an element + * > to the sender containing a value of 'h' that is equal to the number of stanzas handled by the recipient of + * > the element. The response SHOULD be sent as soon as possible after receiving the element, + * > and MUST NOT be withheld for any condition other than a timeout. For example, a client with a slow connection + * > might want to collect many stanzas over a period of time before acking, and a server might want to throttle + * > incoming stanzas. + * + * Thus the XEP recommends that you do not use a delay. + * However, it acknowledges that there may be certain situations in which a delay could prove helpful. + * + * The default value is 0.0 (as recommended by XEP-0198) +**/ +@property (atomic, assign, readwrite) NSTimeInterval ackResponseDelay; + +/** + * It's critically important to understand what an ACK means. + * + * Every ACK contains an 'h' attribute, which stands for "handled". + * To paraphrase XEP-0198 (in client-side terminology): + * + * Acknowledging a previously ­received element indicates that the stanza has been "handled" by the client. + * By "handled" we mean that the client has successfully processed the stanza + * (including possibly saving the item to the database if needed); + * Until a stanza has been affirmed as handled by the client, that stanza is the responsibility of the server + * (e.g., to resend it or generate an error if it is never affirmed as handled by the client). + * + * This means that if your processing of certain elements includes saving them to a database, + * then you should not mark those elements as handled until after your database has confirmed the data is on disk. + * + * You should note that this is a critical component of any networking app that claims to have "reliable messaging". + * + * By default, all elements will be marked as handled as soon as they arrive. + * You'll want to override the default behavior for important elements that require proper handling by your app. + * For example, messages that need to be saved to the database. + * Here's how to do so: + * + * - Implement the delegate method xmppStreamManagement:getIsHandled:stanzaId:forReceivedElement: + * + * This method is invoked for all received elements. + * You can inspect the element, and if it is important and requires special handling by the app, + * then flag the element as NOT handled (overriding the default). + * Also assign the element a "stanzaId". This can be anything you want, such as the elementID, + * or maybe something more app-specific (e.g. something you already use that's associated with the message). + * + * - Handle the important element however you need to + * + * If you're saving something to the database, + * then wait until after the database commit has completed successfully. + * + * - Notify the module that the element has been handled via the method markHandledStanzaId: + * + * You must pass the stanzaId that you returned from the delegate method. + * + * + * @see xmppStreamManagement:getIsHandled:stanzaId:forReceivedElement: +**/ +- (void)markHandledStanzaId:(id)stanzaId; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPStreamManagementDelegate +@optional + +/** + * Notifies delegates of the server's response from sending the stanza. +**/ +- (void)xmppStreamManagement:(XMPPStreamManagement *)sender wasEnabled:(NSXMLElement *)enabled; +- (void)xmppStreamManagement:(XMPPStreamManagement *)sender wasNotEnabled:(NSXMLElement *)failed; + +/** + * Notifies delegates that a request for an ack from the server was sent. +**/ +- (void)xmppStreamManagementDidRequestAck:(XMPPStreamManagement *)sender; + +/** + * Invoked when an ack is received from the server, and new stanzas have been acked. + * + * @param stanzaIds + * Includes all "stanzaIds" of sent elements that were just acked. + * + * What is a "stanzaId" ? + * + * A stanzaId is a unique identifier that ** YOU can provide ** in order to track an element. + * It could simply be the elementId of the sent element. Or, + * it could be something custom that you provide in order to properly lookup a message in your data store. + * + * For more information, see the delegate method xmppStreamManagement:stanzaIdForSentElement: +**/ +- (void)xmppStreamManagement:(XMPPStreamManagement *)sender didReceiveAckForStanzaIds:(NSArray *)stanzaIds; + +/** + * XEP-0198 reports the following regarding duplicate stanzas: + * + * Because unacknowledged stanzas might have been received by the other party, + * resending them might result in duplicates; there is no way to prevent such a + * result in this protocol, although use of the XMPP 'id' attribute on all stanzas + * can at least assist the intended recipients in weeding out duplicate stanzas. + * + * In other words, there are edge cases in which you might receive duplicates. + * And the proper way to fix this is to use some kind of identifier in order to detect duplicates. + * + * What kind of identifier to use is up to you. (It's app specific.) + * The XEP notes that you might use the 'id' attribute for this purpose. And this is certainly the most common case. + * However, you may have an alternative scheme that works better for your purposes. + * In which case you can use this delegate method to opt-in. + * + * For example: + * You store all your messages in YapDatabase, which is a collection/key/value storage system. + * Perhaps the collection is the conversationId, and the key is a messageId. + * Therefore, to efficiently lookup a message in your datastore you'd prefer a collection/key tuple. + * + * To achieve this, you would implement this method, and return a YapCollectionKey object for message elements. + * This way, when the xmppStreamManagement:didReceiveAckForStanzaIds: method is invoked, + * you'll get a list that contains your collection/key tuple objects. And then you can quickly and efficiently + * fetch and update your message objects. + * + * If there are no delegates that implement this method, + * or all delegates return nil, then the stanza's elementId is used as the stanzaId. + * + * If the stanza isn't assigned a stanzaId (via a delegate method), + * and it doesn't have an elementId, then it isn't reported in the acked stanzaIds array. +**/ +- (id)xmppStreamManagement:(XMPPStreamManagement *)sender stanzaIdForSentElement:(XMPPElement *)element; + +/** + * It's critically important to understand what an ACK means. + * + * Every ACK contains an 'h' attribute, which stands for "handled". + * To paraphrase XEP-0198 (in client-side terminology): + * + * Acknowledging a previously ­received element indicates that the stanza has been "handled" by the client. + * By "handled" we mean that the client has successfully processed the stanza + * (including possibly saving the item to the database if needed); + * Until a stanza has been affirmed as handled by the client, that stanza is the responsibility of the server + * (e.g., to resend it or generate an error if it is never affirmed as handled by the client). + * + * This means that if your processing of certain elements includes saving them to a database, + * then you should not mark those elements as handled until after your database has confirmed the data is on disk. + * + * You should note that this is a critical component of any networking app that claims to have "reliable messaging". + * + * By default, all elements will be marked as handled as soon as they arrive. + * You'll want to override the default behavior for important elements that require proper handling by your app. + * For example, messages that need to be saved to the database. + * Here's how to do so: + * + * - Implement the delegate method xmppStreamManagement:getIsHandled:stanzaId:forReceivedElement: + * + * This method is invoked for all received elements. + * You can inspect the element, and if it is important and requires special handling by the app, + * then flag the element as NOT handled (overriding the default). + * Also assign the element a "stanzaId". This can be anything you want, such as the elementID, + * or maybe something more app-specific (e.g. something you already use that's associated with the message). + * + * - Handle the important element however you need to + * + * If you're saving something to the database, + * then wait until after the database commit has completed successfully. + * + * - Notify the module that the element has been handled via the method markHandledStanzaId: + * + * You must pass the stanzaId that you returned from this delegate method. + * + * + * @see markHandledStanzaId: +**/ +- (void)xmppStreamManagement:(XMPPStreamManagement *)sender + getIsHandled:(BOOL *)isHandledPtr + stanzaId:(id *)stanzaIdPtr + forReceivedElement:(XMPPElement *)element; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPStreamManagementStorage +@required + +// +// +// -- PRIVATE METHODS -- +// +// These methods are designed to be used ONLY by the XMPPStreamManagement class. +// +// + +/** + * Configures the storage class, passing it's parent and the parent's dispatch queue. + * + * This method is called by the init methods of the XMPPStreamManagement class. + * This method is designed to inform the storage class of it's parent + * and of the dispatch queue the parent will be operating on. + * + * A storage class may choose to operate on the same queue as it's parent, + * as the majority of the time it will be getting called by the parent. + * If both are operating on the same queue, the combination may run faster. + * + * Some storage classes support multiple xmppStreams, + * and may choose to operate on their own internal queue. + * + * This method should return YES if it was configured properly. + * It should return NO only if configuration failed. + * For example, a storage class designed to be used only with a single xmppStream is being added to a second stream. +**/ +- (BOOL)configureWithParent:(XMPPStreamManagement *)parent queue:(dispatch_queue_t)queue; + +/** + * Invoked after we receive from the server. + * + * @param resumptionId + * The ID required to resume the session, given to us by the server. + * + * @param timeout + * The timeout in seconds. + * After a disconnect, the server will maintain our state for this long. + * If we attempt to resume the session after this timeout it likely won't work. + * + * @param lastDisconnect + * Used to reset the lastDisconnect value. + * This value is often updated during the session, to ensure it closely resemble the date the server will use. + * That is, if the client application is killed (or crashes) we want a relatively accurate lastDisconnect date. + * + * @param stream + * The associated xmppStream (standard parameter for storage classes) + * + * This method should also nil out the following values (if needed) associated with the account: + * - lastHandledByClient + * - lastHandledByServer + * - pendingOutgoingStanzas +**/ +- (void)setResumptionId:(NSString *)resumptionId + timeout:(uint32_t)timeout + lastDisconnect:(NSDate *)date + forStream:(XMPPStream *)stream; + +/** + * This method is invoked ** often ** during stream operation. + * It is not invoked when the xmppStream is disconnected. + * + * Important: See the note below: "Optimizing storage demands during active stream usage" + * + * @param date + * Updates the previous lastDisconnect value. + * + * @param lastHandledByClient + * The most recent 'h' value we can safely send to the server. + * + * @param stream + * The associated xmppStream (standard parameter for storage classes) +**/ +- (void)setLastDisconnect:(NSDate *)date + lastHandledByClient:(uint32_t)lastHandledByClient + forStream:(XMPPStream *)stream; + +/** + * This method is invoked ** often ** during stream operation. + * It is not invoked when the xmppStream is disconnected. + * + * Important: See the note below: "Optimizing storage demands during active stream usage" + * + * @param date + * Updates the previous lastDisconnect value. + * + * @param lastHandledByServer + * The most recent 'h' value we've received from the server. + * + * @param pendingOutgoingStanzas + * An array of XMPPStreamManagementOutgoingStanza objects. + * The storage layer is in charge of properly persisting this array, including: + * - the array count + * - the stanzaId of each element, including those that are nil + * + * @param stream + * The associated xmppStream (standard parameter for storage classes) +**/ +- (void)setLastDisconnect:(NSDate *)date + lastHandledByServer:(uint32_t)lastHandledByServer + pendingOutgoingStanzas:(NSArray *)pendingOutgoingStanzas + forStream:(XMPPStream *)stream; + + +/// ***** Optimizing storage demands during active stream usage ***** +/// +/// There are 2 methods that are invoked frequently during stream activity: +/// +/// - setLastDisconnect:lastHandledByClient:forStream: +/// - setLastDisconnect:lastHandledByServer:pendingOutgoingStanzas:forStream: +/// +/// They are invoked any time the 'h' values change, or whenver the pendingStanzaIds change. +/// In other words, they are invoked continually as stanzas get sent and received. +/// And it is the job of the storage layer to decide how to handle the traffic. +/// There are a few things to consider here: +/// +/// - How much chatter does the xmppStream do? +/// - How fast is the storage layer? +/// - How does the overhead on the storage layer affect the rest of the app? +/// +/// If your xmppStream isn't very chatty, and you've got a fast concurrent database, +/// then you may be able to simply pipe all these method calls to the database without thinking. +/// However, if your xmppStream is always constantly sending/receiving presence stanzas, and pinging the server, +/// then you might consider a bit of optimzation here. Below is a simple recommendation for how to accomplish this. +/// +/// You could choose to queue the changes from these method calls, and dump them to the database after a timeout. +/// Thus you'll be able to consolidate a large traffic surge into a small handful of database operations. +/// +/// Also, you could expose a 'flush' operation on the storage layer. +/// And invoke the flush operation when the app is backgrounded, or about to quit. + + +/** + * This method is invoked immediately after an accidental disconnect. + * And may be invoked post-disconnect if the state changes, such as for the following edge cases: + * + * - due to continued processing of stanzas received pre-disconnect, + * that are just now being marked as handled by the delegate(s) + * - due to a delayed response from the delegate(s), + * such that we didn't receive the stanzaId for an outgoing stanza until after the disconnect occurred. + * + * This method is not invoked if stream management is started on a connected xmppStream. + * + * @param date + * This value will be the actual disconnect date. + * + * @param lastHandledByClient + * The most recent 'h' value we can safely send to the server. + * + * @param lastHandledByServer + * The most recent 'h' value we've received from the server. + * + * @param pendingOutgoingStanzas + * An array of XMPPStreamManagementOutgoingStanza objects. + * The storage layer is in charge of properly persisting this array, including: + * - the array count + * - the stanzaId of each element, including those that are nil + * + * @param stream + * The associated xmppStream (standard parameter for storage classes) +**/ +- (void)setLastDisconnect:(NSDate *)date + lastHandledByClient:(uint32_t)lastHandledByClient + lastHandledByServer:(uint32_t)lastHandledByServer + pendingOutgoingStanzas:(NSArray *)pendingOutgoingStanzas + forStream:(XMPPStream *)stream; + +/** + * Invoked when the extension needs values from a previous session. + * This method is used to get values needed in order to determine if it can resume a previous stream. +**/ +- (void)getResumptionId:(NSString **)resumptionIdPtr + timeout:(uint32_t *)timeoutPtr + lastDisconnect:(NSDate **)lastDisconnectPtr + forStream:(XMPPStream *)stream; + +/** + * Invoked when the extension needs values from a previous session. + * This method is used to get values needed in order to resume a previous stream. +**/ +- (void)getLastHandledByClient:(uint32_t *)lastHandledByClientPtr + lastHandledByServer:(uint32_t *)lastHandledByServerPtr + pendingOutgoingStanzas:(NSArray **)pendingOutgoingStanzasPtr + forStream:(XMPPStream *)stream; + +/** + * Instructs the storage layer to remove all values stored for the given stream. + * This occurs after the extension detects a "cleanly closed stream", + * in which case the stream cannot be resumed next time. +**/ +- (void)removeAllForStream:(XMPPStream *)stream; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPStream (XMPPStreamManagement) + +/** + * Returns whether or not the server's includes . +**/ +- (BOOL)supportsStreamManagement; + +@end diff --git a/Extensions/XEP-0198/XMPPStreamManagement.m b/Extensions/XEP-0198/XMPPStreamManagement.m new file mode 100644 index 0000000..8e49094 --- /dev/null +++ b/Extensions/XEP-0198/XMPPStreamManagement.m @@ -0,0 +1,1929 @@ +#import "XMPPStreamManagement.h" +#import "XMPPStreamManagementStanzas.h" +#import "XMPPInternal.h" +#import "XMPPTimer.h" +#import "XMPPLogging.h" +#import "NSNumber+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +/** + * Define various xmlns values. +**/ +#define XMLNS_STREAM_MANAGEMENT @"urn:xmpp:sm:3" + +/** + * Seeing a return statements within an inner block + * can sometimes be mistaken for a return point of the enclosing method. + * This makes inline blocks a bit easier to read. +**/ +#define return_from_block return + + +@implementation XMPPStreamManagement +{ + // Storage module (may be nil) + + id storage; + + // State machine + + BOOL isStarted; // either or received from server + BOOL enableQueued; // the element is queued in xmppStream + BOOL enableSent; // the element has been sent through xmppStream + + BOOL wasCleanDisconnect; // xmppStream sent + + BOOL didAttemptResume; + BOOL didResume; + + NSXMLElement *resume_response; + NSArray *resume_stanzaIds; + + NSDate *disconnectDate; + + // Configuration + + BOOL autoResume; + + NSUInteger autoRequest_stanzaCount; + NSTimeInterval autoRequest_timeout; + + NSUInteger autoAck_stanzaCount; + NSTimeInterval autoAck_timeout; + + NSTimeInterval ackResponseDelay; + + // Enable + + uint32_t requestedMax; + + // Tracking outgoing stanzas + + uint32_t lastHandledByServer; // last h value received from server + + NSMutableArray *unackedByServer; // array of XMPPStreamManagementOutgoingStanza objects + NSUInteger unackedByServer_lastRequestOffset; // represents point at which we last sent a request + + NSArray *prev_unackedByServer; // from previous connection, used when resuming session + + NSMutableArray *unprocessedReceivedAcks; // acks received from server that we haven't processed yet + + XMPPTimer *autoRequestTimer; // timer to fire a request + + // Tracking incoming stanzas + + uint32_t lastHandledByClient; // latest h value we can send to the server + + NSMutableArray *unackedByClient; // array of XMPPStreamManagementIncomingStanza objects + NSUInteger unackedByClient_lastAckOffset; // number of items removed from array, but ack not sent to server + + NSMutableArray *pendingHandledStanzaIds;// edge case handling + NSUInteger outstandingStanzaIds; // edge case handling + defensive programming + + XMPPTimer *autoAckTimer; // timer to fire ack at server + XMPPTimer *ackResponseTimer; // timer for ackResponseDelay +} + +@synthesize storage = storage; + +- (id)init +{ + // This will cause a crash - it's designed to. + // Only the init methods listed in XMPPStreamManagement.h are supported. + + return [self initWithStorage:nil dispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + // This will cause a crash - it's designed to. + // Only the init methods listed in XMPPStreamManagement.h are supported. + + return [self initWithStorage:nil dispatchQueue:queue]; +} + +- (id)initWithStorage:(id )inStorage +{ + return [self initWithStorage:inStorage dispatchQueue:NULL]; +} + +- (id)initWithStorage:(id )inStorage dispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super initWithDispatchQueue:queue])) + { + if ([inStorage configureWithParent:self queue:moduleQueue]) { + storage = inStorage; + } + else { + XMPPLogError(@"%@: %@ - Unable to configure storage!", THIS_FILE, THIS_METHOD); + } + + unackedByServer = [[NSMutableArray alloc] init]; + unackedByClient = [[NSMutableArray alloc] init]; + } + return self; +} + +- (NSSet *)xep0198Elements +{ + return [NSSet setWithObjects:@"r", @"a", @"enable", @"enabled", @"resume", @"resumed", @"failed", nil]; +} + +- (void)didActivate +{ + [xmppStream registerCustomElementNames:[self xep0198Elements]]; +} + +- (void)didDeactivate +{ + [xmppStream unregisterCustomElementNames:[self xep0198Elements]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)autoResume +{ + XMPPLogTrace(); + + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = autoResume; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setAutoResume:(BOOL)newAutoResume +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ + autoResume = newAutoResume; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)automaticallyRequestAcksAfterStanzaCount:(NSUInteger)stanzaCount orTimeout:(NSTimeInterval)timeout +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool{ + + autoRequest_stanzaCount = stanzaCount; + autoRequest_timeout = MAX(0.0, timeout); + + if (autoRequestTimer) { + [autoRequestTimer updateTimeout:autoRequest_timeout fromOriginalStartTime:YES]; + } + if (isStarted) { + [self maybeRequestAck]; + } + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)getAutomaticallyRequestAcksAfterStanzaCount:(NSUInteger *)stanzaCountPtr orTimeout:(NSTimeInterval *)timeoutPtr +{ + XMPPLogTrace(); + + __block NSUInteger stanzaCount = 0; + __block NSTimeInterval timeout = 0.0; + + dispatch_block_t block = ^{ + + stanzaCount = autoRequest_stanzaCount; + timeout = autoRequest_timeout; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + if (stanzaCountPtr) *stanzaCountPtr = stanzaCount; + if (timeoutPtr) *timeoutPtr = timeout; +} + +- (void)automaticallySendAcksAfterStanzaCount:(NSUInteger)stanzaCount orTimeout:(NSTimeInterval)timeout +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool{ + + autoAck_stanzaCount = stanzaCount; + autoAck_timeout = MAX(0.0, timeout); + + if (autoAckTimer) { + [autoAckTimer updateTimeout:autoAck_timeout fromOriginalStartTime:YES]; + } + if (isStarted) { + [self maybeSendAck]; + } + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)getAutomaticallySendAcksAfterStanzaCount:(NSUInteger *)stanzaCountPtr orTimeout:(NSTimeInterval *)timeoutPtr +{ + XMPPLogTrace(); + + __block NSUInteger stanzaCount = 0; + __block NSTimeInterval timeout = 0.0; + + dispatch_block_t block = ^{ + + stanzaCount = autoAck_stanzaCount; + timeout = autoAck_timeout; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + if (stanzaCountPtr) *stanzaCountPtr = stanzaCount; + if (timeoutPtr) *timeoutPtr = timeout; +} + +- (NSTimeInterval)ackResponseDelay +{ + XMPPLogTrace(); + + __block NSUInteger delay = 0.0; + + dispatch_block_t block = ^{ + + delay = ackResponseDelay; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return delay; +} + +- (void)setAckResponseDelay:(NSTimeInterval)delay +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ + + ackResponseDelay = delay; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Enable +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method sends the stanza to the server to request enabling stream management. + * + * XEP-0198 specifies that the stanza should only be sent by clients after authentication, + * and after binding has occurred. + * + * The servers response is reported via the delegate methods: + * @see xmppStreamManagement:wasEnabled: + * @see xmppStreamManagement:wasNotEnabled: + * + * @param supportsResumption + * Whether the client should request resumptions support. + * If YES, the resume attribute will be included. E.g. + * + * @param maxTimeout + * Allows you to specify the client's preferred maximum resumption time. + * This is optional, and will only be sent if you provide a positive value (maxTimeout > 0.0). + * Note that XEP-0198 only supports sending this value in seconds. + * So it the provided maxTimeout includes millisecond precision, this will be ignored via truncation + * (rounding down to nearest whole seconds value). + * + * @see supportsStreamManagement +**/ +- (void)enableStreamManagementWithResumption:(BOOL)supportsResumption maxTimeout:(NSTimeInterval)maxTimeout +{ + dispatch_block_t block = ^{ @autoreleasepool{ + + if (isStarted) + { + XMPPLogWarn(@"Stream management is already enabled/resumed."); + return; + } + if (enableQueued || enableSent) + { + XMPPLogWarn(@"Stream management is already started (pending response from server)."); + return; + } + + // State transition cleanup + + [unackedByServer removeAllObjects]; + unackedByServer_lastRequestOffset = 0; + + [unackedByClient removeAllObjects]; + unackedByClient_lastAckOffset = 0; + + unprocessedReceivedAcks = nil; + + pendingHandledStanzaIds = nil; + outstandingStanzaIds = 0; + + // Send enable stanza: + // + // + + NSXMLElement *enable = [NSXMLElement elementWithName:@"enable" xmlns:XMLNS_STREAM_MANAGEMENT]; + + if (supportsResumption) { + [enable addAttributeWithName:@"resume" stringValue:@"true"]; + } + if (maxTimeout > 0.0) { + [enable addAttributeWithName:@"max" stringValue:[NSString stringWithFormat:@"%.0f", maxTimeout]]; + } + + [xmppStream sendElement:enable]; + + enableQueued = YES; + requestedMax = (maxTimeout > 0.0) ? (uint32_t)maxTimeout : (uint32_t)0; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Resume +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Utility method for handling canResume logic. +**/ +- (BOOL)canResumeStreamWithResumptionId:(NSString *)resumptionId + timeout:(uint32_t)timeout + lastDisconnect:(NSDate *)lastDisconnect +{ + if (resumptionId == nil) { + XMPPLogVerbose(@"%@: Cannot resume stream: resumptionId is nil", THIS_FILE); + return NO; + } + if (lastDisconnect == nil) { + XMPPLogVerbose(@"%@: Cannot resume stream: lastDisconnect is nil", THIS_FILE); + return NO; + } + + NSTimeInterval elapsed = [lastDisconnect timeIntervalSinceNow] * -1.0; + + if (elapsed < 0.0) // lastDisconnect is in the future ? + { + XMPPLogVerbose(@"%@: Cannot resume stream: invalid lastDisconnect - appears to be in future", THIS_FILE); + return NO; + } + if ((uint32_t)elapsed > timeout) // too much time has elapsed + { + XMPPLogVerbose(@"%@: Cannot resume stream: elapsed(%u) > timeout(%u)", THIS_FILE, (uint32_t)elapsed, timeout); + return NO; + } + + return YES; +} + +/** + * Returns YES if the stream can be resumed. + * + * This would be the case if there's an available resumptionId for the authenticated xmppStream, + * and the timeout from the last stream has not been exceeded. +**/ +- (BOOL)canResumeStream +{ + XMPPLogTrace(); + + // This is a PUBLIC method + + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool{ + + if (isStarted || enableQueued || enableSent) { + return_from_block; + } + + NSString *resumptionId = nil; + uint32_t timeout = 0; + NSDate *lastDisconnect = nil; + + [storage getResumptionId:&resumptionId + timeout:&timeout + lastDisconnect:&lastDisconnect + forStream:xmppStream]; + + result = [self canResumeStreamWithResumptionId:resumptionId timeout:timeout lastDisconnect:lastDisconnect]; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +/** + * Internal method that handles sending the element, and the corresponding state transition. +**/ +- (void)sendResumeRequestWithResumptionId:(NSString *)resumptionId +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool { + + // State transition cleanup + + [unackedByServer removeAllObjects]; + unackedByServer_lastRequestOffset = 0; + + [unackedByClient removeAllObjects]; + unackedByClient_lastAckOffset = 0; + + unprocessedReceivedAcks = nil; + + pendingHandledStanzaIds = nil; + outstandingStanzaIds = 0; + + // Restore our state from the last stream + + uint32_t newLastHandledByClient = 0; + uint32_t newLastHandledByServer = 0; + NSArray *pendingOutgoingStanzas = nil; + + [storage getLastHandledByClient:&newLastHandledByClient + lastHandledByServer:&newLastHandledByServer + pendingOutgoingStanzas:&pendingOutgoingStanzas + forStream:xmppStream]; + + lastHandledByClient = newLastHandledByClient; + lastHandledByServer = newLastHandledByServer; + + if ([pendingOutgoingStanzas count] > 0) { + prev_unackedByServer = [[NSMutableArray alloc] initWithArray:pendingOutgoingStanzas copyItems:YES]; + } + + XMPPLogVerbose(@"%@: Attempting to resume: lastHandledByClient(%u) lastHandledByServer(%u)", + THIS_FILE, lastHandledByClient, lastHandledByServer); + + // Send the resume stanza: + // + // + + NSXMLElement *resume = [NSXMLElement elementWithName:@"resume" xmlns:XMLNS_STREAM_MANAGEMENT]; + [resume addAttributeWithName:@"previd" stringValue:resumptionId]; + [resume addAttributeWithName:@"h" stringValue:[NSString stringWithFormat:@"%u", lastHandledByClient]]; + + [xmppStream sendBindElement:resume]; + + didAttemptResume = YES; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +/** + * Internal method to handle processing a resumed response from the server. +**/ +- (void)processResumed:(NSXMLElement *)resumed +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool { + + uint32_t h = [resumed attributeUInt32ValueForName:@"h" withDefaultValue:lastHandledByServer]; + + uint32_t diff; + if (h >= lastHandledByServer) + diff = h - lastHandledByServer; + else + diff = (UINT32_MAX - lastHandledByServer) + h; + + // IMPORTATNT: + // This code path uses prev_unackedByServer (NOT unackedByServer). + // This is because the ack has to do with stanzas sent from the previous connection. + + if (diff > [prev_unackedByServer count]) + { + XMPPLogWarn(@"Unexpected h value from resume: lastH=%lu, newH=%lu, numPendingStanzas=%lu", + (unsigned long)lastHandledByServer, + (unsigned long)h, + (unsigned long)[prev_unackedByServer count]); + + diff = (uint32_t)[prev_unackedByServer count]; + } + + NSMutableArray *stanzaIds = [NSMutableArray arrayWithCapacity:(NSUInteger)diff]; + + for (uint32_t i = 0; i < diff; i++) + { + XMPPStreamManagementOutgoingStanza *outgoingStanza = prev_unackedByServer[(NSUInteger) i]; + + if (outgoingStanza.stanzaId) { + [stanzaIds addObject:outgoingStanza.stanzaId]; + } + } + + lastHandledByServer = h; + + XMPPLogVerbose(@"%@: processResumed: lastHandledByServer(%u)", THIS_FILE, lastHandledByServer); + + isStarted = YES; + didResume = YES; + + prev_unackedByServer = nil; + + resume_response = resumed; + resume_stanzaIds = [stanzaIds copy]; + + // Update storage + + [storage setLastDisconnect:[NSDate date] + lastHandledByServer:lastHandledByServer + pendingOutgoingStanzas:nil + forStream:xmppStream]; + + // Notify delegate + + [multicastDelegate xmppStreamManagement:self didReceiveAckForStanzaIds:stanzaIds]; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +/** + * This method is meant to be called by other extensions when they receive an xmppStreamDidAuthenticate callback. + * + * Returns YES if the stream was resumed during the authentication process. + * Returns NO otherwise (if resume wasn't available, or it failed). + * + * Other extensions may wish to skip certain setup processes that aren't + * needed if the stream was resumed (since the previous session state has been restored server-side). +**/ +- (BOOL)didResume +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = didResume; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +/** + * This method is meant to be called when you receive an xmppStreamDidAuthenticate callback. + * + * It is used instead of a standard delegate method in order to provide a cleaner API. + * By using this method, one can put all the logic for handling authentication in a single place. + * But more importantly, it solves several subtle timing and threading issues. + * + * > A delegate method could have hit either before or after xmppStreamDidAuthenticate, depending on thread scheduling. + * > We could have queued it up, and forced it to hit after. + * > But your code would likely still have needed to add a check within xmppStreamDidAuthenticate... + * + * @param stanzaIdsPtr (optional) + * Just like the stanzaIdsPtr provided in xmppStreamManagement:didReceiveAckForStanzaIds:. + * This comes from the h value provided within the stanza sent by the server. + * + * @param responsePtr (optional) + * Returns the response we got from the server. Either or . + * This will be nil if resume wasn't tried. + * + * @return + * YES if the stream was resumed. + * NO otherwise. +**/ +- (BOOL)didResumeWithAckedStanzaIds:(NSArray **)stanzaIdsPtr + serverResponse:(NSXMLElement **)responsePtr +{ + __block BOOL result = NO; + __block NSArray *stanzaIds = nil; + __block NSXMLElement *response = nil; + + dispatch_block_t block = ^{ + + result = didResume; + stanzaIds = resume_stanzaIds; + response = resume_response; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + if (stanzaIdsPtr) *stanzaIdsPtr = stanzaIds; + if (responsePtr) *responsePtr = response; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPCustomBinding Protocol +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Attempts to start the custom binding process. + * + * If it isn't possible to start the process (perhaps due to missing information), + * this method should return XMPP_BIND_FAIL and set an appropriate error message. + * + * If binding isn't needed (for example, because custom SASL authentication already handled it), + * this method should return XMPP_BIND_SUCCESS. + * In this case, xmppStream will immediately move to its post-binding operations. + * + * Otherwise this method should send whatever stanzas are needed to begin the binding process. + * And then return XMPP_BIND_CONTINUE. + * + * This method is called by automatically XMPPStream. + * You MUST NOT invoke this method manually. +**/ +- (XMPPBindResult)start:(NSError **)errPtr +{ + XMPPLogTrace(); + + // Fetch the resumptionId, + // and check to see if we can resume the stream. + + NSString *resumptionId = nil; + uint32_t timeout = 0; + NSDate *lastDisconnect = nil; + + [storage getResumptionId:&resumptionId + timeout:&timeout + lastDisconnect:&lastDisconnect + forStream:xmppStream]; + + if (![self canResumeStreamWithResumptionId:resumptionId timeout:timeout lastDisconnect:lastDisconnect]) + { + return XMPP_BIND_FAIL_FALLBACK; + } + + // Start the resume proces + [self sendResumeRequestWithResumptionId:resumptionId]; + + return XMPP_BIND_CONTINUE; +} + +/** + * After the custom binding process has started, all incoming xmpp stanzas are routed to this method. + * The method should process the stanza as appropriate, and return the coresponding result. + * If the process is not yet complete, it should return XMPP_BIND_CONTINUE, + * meaning the xmpp stream will continue to forward all incoming xmpp stanzas to this method. + * + * This method is called automatically by XMPPStream. + * You MUST NOT invoke this method manually. +**/ +- (XMPPBindResult)handleBind:(NSXMLElement *)element withError:(NSError **)errPtr +{ + XMPPLogTrace(); + + NSString *elementName = [element name]; + + if ([elementName isEqualToString:@"resumed"]) + { + [self processResumed:element]; + + return XMPP_BIND_SUCCESS; + } + else + { + if (![elementName isEqualToString:@"failed"]) { + XMPPLogError(@"%@: Received unrecognized response from server: %@", THIS_METHOD, element); + } + + dispatch_async(moduleQueue, ^{ @autoreleasepool { + + didResume = NO; + resume_response = element; + + prev_unackedByServer = nil; + }}); + + return XMPP_BIND_FAIL_FALLBACK; + } +} + +/** + * Optionally implement this method to override the default behavior. + * By default behavior, we mean the behavior normally taken by xmppStream, which is: + * + * - IF the server includes in its stream:features + * - AND xmppStream.skipStartSession property is NOT set + * - THEN xmppStream will send the session start request, and await the response before transitioning to authenticated + * + * Thus if you implement this method and return YES, then xmppStream will skip starting a session, + * regardless of the stream:features and the current xmppStream.skipStartSession property value. + * + * If you implement this method and return NO, then xmppStream will follow the default behavior detailed above. + * This means that, even if this method returns NO, the xmppStream may still skip starting a session if + * the server doesn't require it via its stream:features, + * or if the user has explicitly forbidden it via the xmppStream.skipStartSession property. + * + * The default value is NO. +**/ +- (BOOL)shouldSkipStartSessionAfterSuccessfulBinding +{ + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Requesting Acks +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Sends a request element, requesting the server reply with an ack . + * + * You can also configure the extension to automatically sends requests. + * @see automaticallyRequestAcksAfterStanzaCount:orTimeout: + * + * When the server replies with an ack, the delegate method will be invoked. + * @see xmppStreamManagement:didReceiveAckForStanzaIds: +**/ +- (void)requestAck +{ + XMPPLogTrace(); + + // This is a PUBLIC method + + dispatch_block_t block = ^{ @autoreleasepool{ + + if (isStarted || enableQueued || enableSent) + { + [self _requestAck]; + } + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)_requestAck +{ + XMPPLogTrace(); + + if (isStarted || enableQueued || enableSent) + { + // Send the XML element + + NSXMLElement *r = [NSXMLElement elementWithName:@"r" xmlns:XMLNS_STREAM_MANAGEMENT]; + [xmppStream sendElement:r]; + + // Reset offset + + unackedByServer_lastRequestOffset = [unackedByServer count]; + } + + [autoRequestTimer cancel]; + autoRequestTimer = nil; +} + +- (BOOL)maybeRequestAck +{ + XMPPLogTrace(); + + if (!isStarted && !(enableQueued || enableSent)) + { + // cannot request ack if not started (or at least sent ) + return NO; + } + if ((autoRequest_stanzaCount == 0) && (autoRequest_timeout == 0.0)) + { + // auto request disabled + return NO; + } + + NSUInteger pending = [unackedByServer count] - unackedByServer_lastRequestOffset; + if (pending == 0) + { + // nothing new to request + return NO; + } + + if ((autoRequest_stanzaCount > 0) && (pending >= autoRequest_stanzaCount)) + { + [self _requestAck]; + return YES; + } + else if ((autoRequest_timeout > 0.0) && (autoRequestTimer == nil)) + { + __weak id weakSelf = self; + autoRequestTimer = [[XMPPTimer alloc] initWithQueue:moduleQueue eventHandler:^{ @autoreleasepool{ + + [weakSelf _requestAck]; + }}]; + + [autoRequestTimer startWithTimeout:autoRequest_timeout interval:0]; + } + + return NO; +} + +/** + * This method is invoked from one of the xmppStream:didSendX: methods. +**/ +- (void)processSentElement:(XMPPElement *)element +{ + XMPPLogTrace(); + + SEL selector = @selector(xmppStreamManagement:stanzaIdForSentElement:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // There are not any delegates that respond to the selector. + // So the stanzaId is the elementId (if there is one). + + NSString *elementId = [element elementID]; + + XMPPStreamManagementOutgoingStanza *stanza = + [[XMPPStreamManagementOutgoingStanza alloc] initWithStanzaId:elementId]; + [unackedByServer addObject:stanza]; + + [self updateStoredPendingOutgoingStanzas]; + + // At bottom of this method: + // [self maybeRequestAck]; + } + else + { + // We need to query the delegate(s) to see if there's a specific stanzaId for this element. + // This is an asynchronous process, so we put a placeholder in the array for now. + + XMPPStreamManagementOutgoingStanza *stanza = + [[XMPPStreamManagementOutgoingStanza alloc] initAwaitingStanzaId]; + [unackedByServer addObject:stanza]; + + // Start the asynchronous process to find the proper stanzaId + + GCDMulticastDelegateEnumerator *enumerator = [multicastDelegate delegateEnumerator]; + + dispatch_queue_t concurrentQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(concurrentQ, ^{ @autoreleasepool { + + id stanzaId = nil; + + id delegate = nil; + dispatch_queue_t dq = NULL; + + while ([enumerator getNextDelegate:&delegate delegateQueue:&dq forSelector:selector]) + { + stanzaId = [delegate xmppStreamManagement:self stanzaIdForSentElement:element]; + if (stanzaId) + { + break; + } + } + + if (stanzaId == nil) + { + stanzaId = [element elementID]; + } + + dispatch_async(moduleQueue, ^{ @autoreleasepool{ + + // Set the stanzaId. + stanza.stanzaId = stanzaId; + stanza.awaitingStanzaId = NO; + + // It's possible that we received an ack from the sever (acking our stanza) + // before we were able to determine its stanzaId. + // This edge case is handled by storing the ack in the pendingAcks array for later processing. + // We may be able to process it now. + + BOOL dequeuedPendingAck = NO; + + while ([unprocessedReceivedAcks count] > 0) + { + NSXMLElement *ack = unprocessedReceivedAcks[0]; + + if ([self processReceivedAck:ack]) + { + [unprocessedReceivedAcks removeObjectAtIndex:0]; + dequeuedPendingAck = YES; + } + else + { + break; + } + } + + if (!dequeuedPendingAck) + { + [self updateStoredPendingOutgoingStanzas]; + } + }}); + }}); + } + + XMPPLogVerbose(@"%@: processSentElement (%@): lastHandledByServer(%u) pending(%lu)", + THIS_FILE, [element name], lastHandledByServer, (unsigned long)[unackedByServer count]); + + [self maybeRequestAck]; +} + +/** + * This method is invoked when an ack arrives. + * + * It attempts to process the ack. + * That is, there should be adequate outgoing stanzas (in the unackedByServer array) which have a set stanzaId. + * + * Because stanzaId's are set by the delegate(s), its possible (although unlikely) that we receive an ack before + * the delegate tells us the proper stanzaId for a sent element. When this occurs, we won't be able to completely + * process the ack. However, this method will process as many as possible (while maintaining serial order). + * + * @return + * YES if the ack can be marked as 100% processed. + * NO otherwise (if we're still awaiting a stanzaId from a delegate), + * in which case the caller MUST store the ack in the unprocessedReceivedAcks array. +**/ +- (BOOL)processReceivedAck:(NSXMLElement *)ack +{ + XMPPLogTrace(); + + uint32_t h = 0; + if (![NSNumber xmpp_parseString:[ack attributeStringValueForName:@"h"] intoUInt32:&h]) + { + XMPPLogError(@"Error parsing h value from ack: %@", [ack compactXMLString]); + return YES; + } + + uint32_t diff; + if (h >= lastHandledByServer) + diff = h - lastHandledByServer; + else + diff = (UINT32_MAX - lastHandledByServer) + h; + + if (diff == 0) + { + // shortcut: server is reporting no new stanzas have been processed + return YES; + } + + if (diff > [unackedByServer count]) + { + XMPPLogWarn(@"Unexpected h value from ack: lastH=%lu, newH=%lu, numPendingStanzas=%lu", + (unsigned long)lastHandledByServer, + (unsigned long)h, + (unsigned long)[unackedByServer count]); + + diff = (uint32_t)[unackedByServer count]; + } + + BOOL canProcessEntireAck = YES; + NSUInteger processed = 0; + + NSMutableArray *stanzaIds = [NSMutableArray arrayWithCapacity:(NSUInteger)diff]; + + for (uint32_t i = 0; i < diff; i++) + { + XMPPStreamManagementOutgoingStanza *outgoingStanza = unackedByServer[(NSUInteger) i]; + + if ([outgoingStanza awaitingStanzaId]) + { + canProcessEntireAck = NO; + break; + } + else + { + if (outgoingStanza.stanzaId) { + [stanzaIds addObject:outgoingStanza.stanzaId]; + } + processed++; + } + } + + if (canProcessEntireAck || processed > 0) + { + if (canProcessEntireAck) + { + [unackedByServer removeObjectsInRange:NSMakeRange(0, (NSUInteger)diff)]; + if (unackedByServer_lastRequestOffset > diff) + unackedByServer_lastRequestOffset -= diff; + else + unackedByServer_lastRequestOffset = 0; + + lastHandledByServer = h; + + XMPPLogVerbose(@"%@: processReceivedAck (fully processed): lastHandledByServer(%u) pending(%lu)", + THIS_FILE, lastHandledByServer, (unsigned long)[unackedByServer count]); + } + else // if (processed > 0) + { + [unackedByServer removeObjectsInRange:NSMakeRange(0, processed)]; + if (unackedByServer_lastRequestOffset > processed) + unackedByServer_lastRequestOffset -= processed; + else + unackedByServer_lastRequestOffset = 0; + + lastHandledByServer += processed; + + XMPPLogVerbose(@"%@: processReceivedAck (partially processed): lastHandledByServer(%u) pending(%lu)", + THIS_FILE, lastHandledByServer, (unsigned long)[unackedByServer count]); + } + + // Update storage + + NSArray *pending = [[NSArray alloc] initWithArray:unackedByServer copyItems:YES]; + + if (isStarted) + { + [storage setLastDisconnect:[NSDate date] + lastHandledByServer:lastHandledByServer + pendingOutgoingStanzas:pending + forStream:xmppStream]; + } + else // edge case + { + [storage setLastDisconnect:disconnectDate + lastHandledByClient:lastHandledByClient + lastHandledByServer:lastHandledByServer + pendingOutgoingStanzas:pending + forStream:xmppStream]; + } + + // Notify delegate + + [multicastDelegate xmppStreamManagement:self didReceiveAckForStanzaIds:stanzaIds]; + } + else + { + XMPPLogVerbose(@"%@: processReceivedAck (unprocessed): lastHandledByServer(%u) pending(%lu)", + THIS_FILE, lastHandledByServer, (unsigned long)[unackedByServer count]); + } + + return canProcessEntireAck; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Sending Acks +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Sends an unrequested ack element, acking the server's recently received (and handled) elements. + * + * You can also configure the extension to automatically sends acks. + * @see automaticallySendAcksAfterStanzaCount:orTimeout: + * + * Keep in mind that the extension will automatically send an ack if it receives an explicit request. +**/ +- (void)sendAck +{ + XMPPLogTrace(); + + // This is a PUBLIC method + + dispatch_block_t block = ^{ @autoreleasepool{ + + if (isStarted) + { + [self _sendAck]; + } + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +/** + * Sends the ack element, and discards newly acked stanzas from the queue. +**/ +- (void)_sendAck +{ + NSUInteger pending = 0; + for (XMPPStreamManagementIncomingStanza *stanza in unackedByClient) + { + if (stanza.isHandled) + pending++; + else + break; + } + + if (pending > 0) + { + [unackedByClient removeObjectsInRange:NSMakeRange(0, pending)]; + unackedByClient_lastAckOffset += pending; + lastHandledByClient += pending; + + XMPPLogVerbose(@"%@: sendAck: lastHandledByClient(%u) inc(%lu) totalPending(%lu)", THIS_FILE, + lastHandledByClient, + (unsigned long)pending, + (unsigned long)unackedByClient_lastAckOffset); + + // Update info in storage. + + if (isStarted) + { + [storage setLastDisconnect:[NSDate date] + lastHandledByClient:lastHandledByClient + forStream:xmppStream]; + } + else // edge case + { + // An incoming stanza got markedAsHandled post-disconnect + + NSArray *pending = [[NSArray alloc] initWithArray:unackedByServer copyItems:YES]; + + [storage setLastDisconnect:disconnectDate + lastHandledByClient:lastHandledByClient + lastHandledByServer:lastHandledByServer + pendingOutgoingStanzas:pending + forStream:xmppStream]; + } + } + + if (isStarted) + { + // Send the XML element + + NSXMLElement *a = [NSXMLElement elementWithName:@"a" xmlns:XMLNS_STREAM_MANAGEMENT]; + + NSString *h = [NSString stringWithFormat:@"%u", (unsigned int)lastHandledByClient]; + [a addAttributeWithName:@"h" stringValue:h]; + + [xmppStream sendElement:a]; + + // Reset offset + + unackedByClient_lastAckOffset = 0; + } + + // Stop the timer(s) + + [autoAckTimer cancel]; + autoAckTimer = nil; + + [ackResponseTimer cancel]; + ackResponseTimer = nil; + +} + +/** + * Returns the number of incoming stanzas that have been handled on our side, + * but which we haven't yet sent an ack to the server. +**/ +- (NSUInteger)numIncomingStanzasThatCanBeAcked +{ + // What is unackedByClient_lastAckOffset ? + // + // In the method maybeUpdateStoredLastHandledByClient, + // we remove items from the unackedByClient array, and increase the lastHandledByClient value. + // But we do NOT actually send an ack to the server at this point. + // + // Thus unackedByClient_lastAckOffset represents the number of items we're removed from the unackedByClient array, + // and for which we still need to send an ack to the server. + + NSUInteger count = unackedByClient_lastAckOffset; + + for (XMPPStreamManagementIncomingStanza *stanza in unackedByClient) + { + if (stanza.isHandled) + count++; + else + break; + } + + return count; +} + +/** + * Returns the number of incoming stanzas that cannot yet be acked because + * - the stanza hasn't been marked as handled yet + * - or a preceeding stanza has hasn't been marked as handled yet +**/ +- (NSUInteger)numIncomingStanzasThatCannnotBeAcked +{ + BOOL foundUnhandledStanza = NO; + NSUInteger count = 0; + + for (XMPPStreamManagementIncomingStanza *stanza in unackedByClient) + { + if (foundUnhandledStanza) + { + count++; + } + else if (!stanza.isHandled) + { + foundUnhandledStanza = YES; + count++; + } + } + + return count; +} + +/** + * Sends an ack if needed (if pending meets/exceeds autoAck_stanzaCount). +**/ +- (BOOL)maybeSendAck +{ + XMPPLogTrace(); + + if (!isStarted) + { + // cannot send acks if we're not started + return NO; + } + if ((autoAck_stanzaCount == 0) && (autoAck_timeout == 0.0)) + { + // auto ack disabled + return NO; + } + + NSUInteger pending = [self numIncomingStanzasThatCanBeAcked]; + if (pending == 0) + { + // nothing new to ack + return NO; + } + + // Send ack according to autoAck configuration + + if ((autoAck_stanzaCount > 0) && (pending >= autoAck_stanzaCount)) + { + [self _sendAck]; + return YES; + } + else if ((autoAck_timeout > 0.0) && (autoAckTimer == nil)) + { + __weak id weakSelf = self; + autoAckTimer = [[XMPPTimer alloc] initWithQueue:moduleQueue eventHandler:^{ @autoreleasepool{ + + [weakSelf sendAck]; + }}]; + + [autoAckTimer startWithTimeout:autoAck_timeout interval:0]; + } + + return NO; +} + +- (void)markHandledStanzaId:(id)stanzaId +{ + XMPPLogTrace(); + + if (stanzaId == nil) return; + + dispatch_block_t block = ^{ @autoreleasepool { + + // It's theoretically possible that the delegate(s) returned the same stanzaId for multiple elements. + // Although this is strongly discouraged, we should try to do our best to handle such a situation logically. + // + // In light of this edge case, here are the rules: + // + // Find the first stanza in the queue that is + // - not already marked as handled + // - has a matching stanzaId + // + // Mark this as handled, and then break. + // + // We also check to see if marking this stanza as handled has increased the pending count. + // For example (using the following queue): + // + // 0) + // 1) // <-- marking as handled increases pendingCount from 1 to 2 + // 2) // <-- marking as handled doesn't change pendingCount (still 1) + + BOOL found = NO; + + for (XMPPStreamManagementIncomingStanza *stanza in unackedByClient) + { + if (stanza.isHandled) + { + // continue + } + else if ([stanza.stanzaId isEqual:stanzaId]) + { + stanza.isHandled = YES; + found = YES; + break; + } + } + + if (found) + { + if (![self maybeSendAck]) + { + [self maybeUpdateStoredLastHandledByClient]; + } + } + else + { + // Edge case: + // + // The stanzaId was marked as handled before we finished figuring out what the stanzaId is. + // + // In order to get the stanzaId for a received element, we go through an asynchronous process. + // It's possible (but unlikely) that this process ends up taking longer than it does for the app + // to actually "handle" the element. So we have this odd edge case, + // which we handle by queuing up the stanzaId for later processing. + + if (outstandingStanzaIds > 0) + { + if (pendingHandledStanzaIds == nil) + pendingHandledStanzaIds = [[NSMutableArray alloc] init]; + + [pendingHandledStanzaIds addObject:stanzaId]; + } + } + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)processReceivedElement:(XMPPElement *)element +{ + XMPPLogTrace(); + + NSAssert(isStarted, @"State machine exception"); + + SEL selector = @selector(xmppStreamManagement:getIsHandled:stanzaId:forReceivedElement:); + + if (![multicastDelegate hasDelegateThatRespondsToSelector:selector]) + { + // None of the delegates implement the method. + // Use a shortcut. + + XMPPStreamManagementIncomingStanza *stanza = + [[XMPPStreamManagementIncomingStanza alloc] initWithStanzaId:nil isHandled:YES]; + [unackedByClient addObject:stanza]; + + // Since we know the element is 'handled' we can immediately check to see if we need to send an ack + + if (![self maybeSendAck]) + { + [self maybeUpdateStoredLastHandledByClient]; + } + } + else + { + // We need to query the delegate(s) to see if the stanza can be marked as handled. + // This is an asynchronous process, so we put a placeholder in the array for now. + // + // Note: stanza.isHandled == NO + + XMPPStreamManagementIncomingStanza *stanza = + [[XMPPStreamManagementIncomingStanza alloc] initWithStanzaId:nil isHandled:NO]; + [unackedByClient addObject:stanza]; + + // Query the delegate(s). The Rules: + // + // If ANY of the delegates says the element is "not handled", then we can immediately set it as so. + // Otherwise the element will be marked as handled. + + GCDMulticastDelegateEnumerator *enumerator = [multicastDelegate delegateEnumerator]; + outstandingStanzaIds++; + + dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(concurrentQueue, ^{ @autoreleasepool + { + __block BOOL isHandled = YES; + __block id stanzaId = nil; + + id delegate; + dispatch_queue_t dq; + + while (isHandled && [enumerator getNextDelegate:&delegate delegateQueue:&dq forSelector:selector]) + { + dispatch_sync(dq, ^{ @autoreleasepool { + + [delegate xmppStreamManagement:self + getIsHandled:&isHandled + stanzaId:&stanzaId + forReceivedElement:element]; + + NSAssert(isHandled || stanzaId != nil, + @"You MUST return a stanzaId for any elements you mark as not-yet-handled"); + }}); + } + + dispatch_async(moduleQueue, ^{ @autoreleasepool + { + if (isHandled) + { + stanza.isHandled = YES; + } + else + { + stanza.stanzaId = stanzaId; + + // Check for edge case: + // - stanzaId was marked as handled before we figured out what the stanzaId was + if ([pendingHandledStanzaIds count] > 0) + { + NSUInteger i = 0; + for (id pendingStanzaId in pendingHandledStanzaIds) + { + if ([pendingStanzaId isEqual:stanzaId]) + { + [pendingHandledStanzaIds removeObjectAtIndex:i]; + + stanza.isHandled = YES; + break; + } + + i++; + } + } + } + + // Defensive programming. + // Don't let this array grow infinitely big (if markHandledStanzaId is being invoked incorrectly). + if (--outstandingStanzaIds == 0) { + [pendingHandledStanzaIds removeAllObjects]; + } + + if (stanza.isHandled) + { + if (![self maybeSendAck]) + { + [self maybeUpdateStoredLastHandledByClient]; + } + } + }}); + }}); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Storage Helpers +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method is used when the pendingStanzaIds have changed (ivar unackedByServer changed), + * but we weren't able to process an ack, or update the lastHandledByServer. +**/ +- (void)updateStoredPendingOutgoingStanzas +{ + XMPPLogTrace(); + + NSArray *pending = [[NSArray alloc] initWithArray:unackedByServer copyItems:YES]; + + if (isStarted) + { + [storage setLastDisconnect:[NSDate date] + lastHandledByServer:lastHandledByServer + pendingOutgoingStanzas:pending + forStream:xmppStream]; + } + else + { + [storage setLastDisconnect:disconnectDate + lastHandledByClient:lastHandledByClient + lastHandledByServer:lastHandledByServer + pendingOutgoingStanzas:pending + forStream:xmppStream]; + } +} + +/** + * This method is used when we can maybe increment the lastHandledByClient value, + * but the change isn't significant enough to trigger an autoAck (or autoAck_stanzaCount is disabled). + * + * It updates the lastHandledByClient value (if needed), and notified storage. +**/ +- (void)maybeUpdateStoredLastHandledByClient +{ + XMPPLogTrace(); + + // Edge case note: + // + // This method may be invoked shortly after being disconnected. + // How is this handled? + // + // The unackedByClient array is cleared when we send or . + // And it cannot be appended to unless isStarted is YES. + // Thus this method works properly shortly after a disconnect, and can increment lastHandledByClient. + // And properly handles the edge case of being called in the middle of resuming a session. + + NSUInteger pending = 0; + for (XMPPStreamManagementIncomingStanza *stanza in unackedByClient) + { + if (stanza.isHandled) + pending++; + else + break; + } + + if (pending > 0) + { + [unackedByClient removeObjectsInRange:NSMakeRange(0, pending)]; + unackedByClient_lastAckOffset += pending; + lastHandledByClient += pending; + + XMPPLogVerbose(@"%@: sendAck: lastHandledByClient(%u) inc(%lu) totalPending(%lu)", THIS_FILE, + lastHandledByClient, + (unsigned long)pending, + (unsigned long)unackedByClient_lastAckOffset); + + if (isStarted) + { + [storage setLastDisconnect:[NSDate date] + lastHandledByClient:lastHandledByClient + forStream:xmppStream]; + } + else // edge case + { + // An incoming stanza got markedAsHandled post-disconnect + + NSArray *pending = [[NSArray alloc] initWithArray:unackedByServer copyItems:YES]; + + [storage setLastDisconnect:disconnectDate + lastHandledByClient:lastHandledByClient + lastHandledByServer:lastHandledByServer + pendingOutgoingStanzas:pending + forStream:xmppStream]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Binding a JID resource is a standard part of the authentication process, + * and occurs after SASL authentication completes (which generally authenticates the JID username). + * + * This delegate method allows for a custom binding procedure to be used. + * For example: + * - a custom SASL authentication scheme might combine auth with binding + * - stream management (xep-0198) replaces binding if it can resume a previous session + * + * Return nil (or don't implement this method) if you wish to use the standard binding procedure. +**/ +- (id )xmppStreamWillBind:(XMPPStream *)sender +{ + if (autoResume) + { + // We will check canResume in start: method (part of XMPPCustomBinding protocol) + return self; + } + else + { + return nil; + } +} + +- (void)xmppStream:(XMPPStream *)sender didSendIQ:(XMPPIQ *)iq +{ + XMPPLogTrace(); + + if (isStarted || enableSent) + { + [self processSentElement:iq]; + } +} + +- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message +{ + XMPPLogTrace(); + + if (isStarted || enableSent) + { + [self processSentElement:message]; + } +} + +- (void)xmppStream:(XMPPStream *)sender didSendPresence:(XMPPPresence *)presence +{ + XMPPLogTrace(); + + if (isStarted || enableSent) + { + [self processSentElement:presence]; + } +} + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + XMPPLogTrace(); + + if (isStarted) + { + [self processReceivedElement:iq]; + } + + return NO; +} + +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message +{ + XMPPLogTrace(); + + if (isStarted) + { + [self processReceivedElement:message]; + } +} + +- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence +{ + XMPPLogTrace(); + + if (isStarted) + { + [self processReceivedElement:presence]; + } +} + +/** + * This method is called if any of the xmppStream:willReceiveX: methods filter the incoming stanza. + * + * It may be useful for some extensions to know that something was received, + * even if it was filtered for some reason. +**/ +- (void)xmppStreamDidFilterStanza:(XMPPStream *)sender +{ + XMPPLogTrace(); + + if (isStarted) + { + // The element was filtered/consumed by something in the stack. + // So it is implicitly 'handled'. + + XMPPStreamManagementIncomingStanza *stanza = + [[XMPPStreamManagementIncomingStanza alloc] initWithStanzaId:nil isHandled:YES]; + [unackedByClient addObject:stanza]; + + XMPPLogVerbose(@"%@: xmppStreamDidFilterStanza: lastHandledByClient(%u) pendingToAck(%lu), pendingHandled(%lu)", + THIS_FILE, lastHandledByClient, + (unsigned long)[self numIncomingStanzasThatCanBeAcked], + (unsigned long)[self numIncomingStanzasThatCannnotBeAcked]); + + if (![self maybeSendAck]) + { + [self maybeUpdateStoredLastHandledByClient]; + } + } +} + +- (void)xmppStream:(XMPPStream *)sender didSendCustomElement:(NSXMLElement *)element +{ + XMPPLogTrace(); + + if (enableQueued) + { + if ([[element name] isEqualToString:@"enable"]) + { + enableQueued = NO; + enableSent = YES; + } + } + else if (isStarted) + { + if ([[element name] isEqualToString:@"r"]) + { + [multicastDelegate xmppStreamManagementDidRequestAck:self]; + } + } +} + +- (void)xmppStream:(XMPPStream *)sender didReceiveCustomElement:(NSXMLElement *)element +{ + XMPPLogTrace(); + + NSString *elementName = [element name]; + + if ([elementName isEqualToString:@"r"]) + { + // We received a request from the server. + + if (ackResponseDelay <= 0.0) + { + // Immediately respond to the request, + // as recommended in the XEP. + + [self _sendAck]; + } + else if (ackResponseTimer == nil) + { + // Use client-configured delay before responding to the request. + + __weak id weakSelf = self; + ackResponseTimer = [[XMPPTimer alloc] initWithQueue:moduleQueue eventHandler:^{ @autoreleasepool{ + + [weakSelf _sendAck]; + }}]; + + [ackResponseTimer startWithTimeout:ackResponseDelay interval:0]; + } + } + else if ([elementName isEqualToString:@"a"]) + { + // Try to process the ack. + // If we can't yet, then we'll put it into the pendingAcks array. + + if (![self processReceivedAck:element]) + { + if (unprocessedReceivedAcks == nil) + unprocessedReceivedAcks = [[NSMutableArray alloc] initWithCapacity:1]; + + [unprocessedReceivedAcks addObject:element]; + } + } + else if ([elementName isEqualToString:@"enabled"]) + { + if (enableSent) + { + // + + NSString *resumptionId = nil; + uint32_t max = 0; + + BOOL canResume = [element attributeBoolValueForName:@"resume" withDefaultValue:NO]; + if (canResume) + { + resumptionId = [element attributeStringValueForName:@"id"]; + max = [element attributeUInt32ValueForName:@"max" withDefaultValue:requestedMax]; + } + + [storage setResumptionId:resumptionId + timeout:max + lastDisconnect:[NSDate date] + forStream:xmppStream]; + + [multicastDelegate xmppStreamManagement:self wasEnabled:element]; + + isStarted = YES; + enableSent = NO; + + lastHandledByClient = 0; + lastHandledByServer = 0; + + unprocessedReceivedAcks = nil; + } + else + { + XMPPLogWarn(@"Received unrequested stanza"); + } + } + else if ([elementName isEqualToString:@"failed"]) + { + if (enableSent) + { + [storage removeAllForStream:xmppStream]; + + [multicastDelegate xmppStreamManagement:self wasNotEnabled:element]; + + isStarted = NO; + enableSent = NO; + + [autoRequestTimer cancel]; + autoRequestTimer = nil; + } + } +} + +- (void)xmppStreamDidSendClosingStreamStanza:(XMPPStream *)sender +{ + XMPPLogTrace(); + + wasCleanDisconnect = YES; +} + +- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error +{ + XMPPLogTrace(); + + if (wasCleanDisconnect) + { + disconnectDate = nil; + [storage removeAllForStream:xmppStream]; + } + else + { + disconnectDate = [NSDate date]; + NSArray *pending = [[NSArray alloc] initWithArray:unackedByServer copyItems:YES]; + + [storage setLastDisconnect:disconnectDate + lastHandledByClient:lastHandledByClient + lastHandledByServer:lastHandledByServer + pendingOutgoingStanzas:pending + forStream:xmppStream]; + } + + // Reset temporary state variables + + isStarted = NO; + enableQueued = NO; + enableSent = NO; + + wasCleanDisconnect = NO; + + didAttemptResume = NO; + didResume = NO; + + prev_unackedByServer = nil; + + resume_response = nil; + resume_stanzaIds = nil; + + // Cancel timers + + [autoRequestTimer cancel]; + autoRequestTimer = nil; + + [autoAckTimer cancel]; + autoAckTimer = nil; + + [ackResponseTimer cancel]; + ackResponseTimer = nil; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPStream (XMPPStreamManagement) + +- (BOOL)supportsStreamManagement +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ @autoreleasepool { + + // The root element can be properly queried anytime after the + // stream:features are received, and TLS has been setup (if required). + + if (self.state >= STATE_XMPP_POST_NEGOTIATION) + { + NSXMLElement *features = [self.rootElement elementForName:@"stream:features"]; + NSXMLElement *sm = [features elementForName:@"sm" xmlns:XMLNS_STREAM_MANAGEMENT]; + + result = (sm != nil); + } + }}; + + if (dispatch_get_specific(self.xmppQueueTag)) + block(); + else + dispatch_sync(self.xmppQueue, block); + + return result; +} + +@end diff --git a/Extensions/XEP-0199/XMPPAutoPing.h b/Extensions/XEP-0199/XMPPAutoPing.h new file mode 100644 index 0000000..a224f7f --- /dev/null +++ b/Extensions/XEP-0199/XMPPAutoPing.h @@ -0,0 +1,102 @@ +#import +#import "XMPPModule.h" +#import "XMPPPing.h" + +#define _XMPP_AUTO_PING_H + +@class XMPPJID; + + +/** + * The XMPPAutoPing module sends pings on a designated interval to the target. + * The target may simply be the server, or a specific resource. + * + * The module only sends pings as needed. + * If the xmpp stream is receiving data from the target, there's no need to send a ping. + * Only when no data has been received from the target is a ping sent. +**/ +@interface XMPPAutoPing : XMPPModule { +@private + NSTimeInterval pingInterval; + NSTimeInterval pingTimeout; + XMPPJID *targetJID; + NSString *targetJIDStr; + + NSTimeInterval lastReceiveTime; + dispatch_source_t pingIntervalTimer; + + BOOL awaitingPingResponse; + XMPPPing *xmppPing; +} + +/** + * How often to send a ping. + * + * The internal timer fires every (pingInterval / 4) seconds. + * Upon firing it checks when data was last received from the target, + * and sends a ping if the elapsed time has exceeded the pingInterval. + * Thus the effective resolution of the timer is based on the configured interval. + * + * To temporarily disable auto-ping, set the interval to zero. + * + * The default pingInterval is 60 seconds. +**/ +@property (readwrite) NSTimeInterval pingInterval; + +/** + * How long to wait after sending a ping before timing out. + * + * The timeout is decoupled from the pingInterval to allow for longer pingIntervals, + * which avoids flooding the network, and to allow more precise control overall. + * + * After a ping is sent, if a reply is not received by this timeout, + * the delegate method is invoked. + * + * The default pingTimeout is 10 seconds. +**/ +@property (readwrite) NSTimeInterval pingTimeout; + +/** + * The target to send pings to. + * + * If the targetJID is nil, this implies the target is the xmpp server we're connected to. + * In this case, receiving any data means we've received data from the target. + * + * If the targetJID is non-nil, it must be a full JID (user@domain.tld/rsrc). + * In this case, the module will monitor the stream for data from the given JID. + * + * The default targetJID is nil. +**/ +@property (readwrite, strong) XMPPJID *targetJID; + +/** + * Corresponds to the last time data was received from the target. + * The NSTimeInterval value comes from [NSDate timeIntervalSinceReferenceDate] +**/ +@property (readonly) NSTimeInterval lastReceiveTime; + +/** + * XMPPAutoPing is used to automatically send pings on a regular interval. + * Sometimes the target is also sending pings to us as well. + * If so, you may optionally set respondsToQueries to YES to allow the module to respond to incoming pings. + * + * If you create multiple instances of XMPPAutoPing or XMPPPing, + * then only one instance should respond to queries. + * + * The default value is NO. +**/ +@property (readwrite) BOOL respondsToQueries; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPAutoPingDelegate +@optional + +- (void)xmppAutoPingDidSendPing:(XMPPAutoPing *)sender; +- (void)xmppAutoPingDidReceivePong:(XMPPAutoPing *)sender; + +- (void)xmppAutoPingDidTimeout:(XMPPAutoPing *)sender; + +@end diff --git a/Extensions/XEP-0199/XMPPAutoPing.m b/Extensions/XEP-0199/XMPPAutoPing.m new file mode 100644 index 0000000..5936158 --- /dev/null +++ b/Extensions/XEP-0199/XMPPAutoPing.m @@ -0,0 +1,416 @@ +#import "XMPPAutoPing.h" +#import "XMPPPing.h" +#import "XMPP.h" +#import "XMPPLogging.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +@interface XMPPAutoPing () +- (void)updatePingIntervalTimer; +- (void)startPingIntervalTimer; +- (void)stopPingIntervalTimer; +@end + +#pragma mark - + +@implementation XMPPAutoPing + +- (id)init +{ + return [self initWithDispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super initWithDispatchQueue:queue])) + { + pingInterval = 60; + pingTimeout = 10; + + lastReceiveTime = 0; + + xmppPing = [[XMPPPing alloc] initWithDispatchQueue:queue]; + xmppPing.respondsToQueries = NO; + + [xmppPing addDelegate:self delegateQueue:moduleQueue]; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + if ([super activate:aXmppStream]) + { + [xmppPing activate:aXmppStream]; + + return YES; + } + + return NO; +} + +- (void)deactivate +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [self stopPingIntervalTimer]; + + lastReceiveTime = 0; + awaitingPingResponse = NO; + + [xmppPing deactivate]; + [super deactivate]; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); +} + +- (void)dealloc +{ + + [self stopPingIntervalTimer]; + + [xmppPing removeDelegate:self]; + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSTimeInterval)pingInterval +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return pingInterval; + } + else + { + __block NSTimeInterval result; + + dispatch_sync(moduleQueue, ^{ + result = pingInterval; + }); + return result; + } +} + +- (void)setPingInterval:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ + + if (pingInterval != interval) + { + pingInterval = interval; + + // Update the pingTimer. + // + // Depending on new value and current state of the pingTimer, + // this may mean starting, stoping, or simply updating the timer. + + if (pingInterval > 0) + { + // Remember: Only start the pinger after the xmpp stream is up and authenticated + if ([xmppStream isAuthenticated]) + [self startPingIntervalTimer]; + } + else + { + [self stopPingIntervalTimer]; + } + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (NSTimeInterval)pingTimeout +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return pingTimeout; + } + else + { + __block NSTimeInterval result; + + dispatch_sync(moduleQueue, ^{ + result = pingTimeout; + }); + return result; + } +} + +- (void)setPingTimeout:(NSTimeInterval)timeout +{ + dispatch_block_t block = ^{ + + if (pingTimeout != timeout) + { + pingTimeout = timeout; + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (XMPPJID *)targetJID +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return targetJID; + } + else + { + __block XMPPJID *result; + + dispatch_sync(moduleQueue, ^{ + result = targetJID; + }); + return result; + } +} + +- (void)setTargetJID:(XMPPJID *)jid +{ + dispatch_block_t block = ^{ + + if (![targetJID isEqualToJID:jid]) + { + targetJID = jid; + + targetJIDStr = [targetJID full]; + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (NSTimeInterval)lastReceiveTime +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return lastReceiveTime; + } + else + { + __block NSTimeInterval result; + + dispatch_sync(moduleQueue, ^{ + result = lastReceiveTime; + }); + return result; + } +} + +- (BOOL)respondsToQueries +{ + return xmppPing.respondsToQueries; +} + +- (void)setRespondsToQueries:(BOOL)respondsToQueries +{ + xmppPing.respondsToQueries = respondsToQueries; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Ping Interval +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)handlePingIntervalTimerFire +{ + if (awaitingPingResponse) return; + + BOOL sendPing = NO; + + if (lastReceiveTime == 0) + { + sendPing = YES; + } + else + { + NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate]; + NSTimeInterval elapsed = (now - lastReceiveTime); + + XMPPLogTrace2(@"%@: %@ - elapsed(%f)", [self class], THIS_METHOD, elapsed); + + sendPing = ((elapsed < 0) || (elapsed >= pingInterval)); + } + + if (sendPing) + { + awaitingPingResponse = YES; + + if (targetJID) + [xmppPing sendPingToJID:targetJID withTimeout:pingTimeout]; + else + [xmppPing sendPingToServerWithTimeout:pingTimeout]; + + [multicastDelegate xmppAutoPingDidSendPing:self]; + } +} + +- (void)updatePingIntervalTimer +{ + XMPPLogTrace(); + + NSAssert(pingIntervalTimer != NULL, @"Broken logic (1)"); + NSAssert(pingInterval > 0, @"Broken logic (2)"); + + // The timer fires every (pingInterval / 4) seconds. + // Upon firing it checks when data was last received from the target, + // and sends a ping if the elapsed time has exceeded the pingInterval. + // Thus the effective resolution of the timer is based on the configured pingInterval. + + uint64_t interval = ((pingInterval / 4.0) * NSEC_PER_SEC); + + // The timer's first fire should occur 'interval' after lastReceiveTime. + // If there is no lastReceiveTime, then the timer's first fire should occur 'interval' after now. + + NSTimeInterval diff; + if (lastReceiveTime == 0) + diff = 0.0; + else + diff = lastReceiveTime - [NSDate timeIntervalSinceReferenceDate];; + + dispatch_time_t bt = dispatch_time(DISPATCH_TIME_NOW, (diff * NSEC_PER_SEC)); + dispatch_time_t tt = dispatch_time(bt, interval); + + dispatch_source_set_timer(pingIntervalTimer, tt, interval, 0); +} + +- (void)startPingIntervalTimer +{ + XMPPLogTrace(); + + if (pingInterval <= 0) + { + // Pinger is disabled + return; + } + + BOOL newTimer = NO; + + if (pingIntervalTimer == NULL) + { + newTimer = YES; + pingIntervalTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue); + + dispatch_source_set_event_handler(pingIntervalTimer, ^{ @autoreleasepool { + + [self handlePingIntervalTimerFire]; + + }}); + } + + [self updatePingIntervalTimer]; + + if (newTimer) + { + dispatch_resume(pingIntervalTimer); + } +} + +- (void)stopPingIntervalTimer +{ + XMPPLogTrace(); + + if (pingIntervalTimer) + { + #if !OS_OBJECT_USE_OBJC + dispatch_release(pingIntervalTimer); + #endif + pingIntervalTimer = NULL; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPPing Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppPing:(XMPPPing *)sender didReceivePong:(XMPPIQ *)pong withRTT:(NSTimeInterval)rtt +{ + XMPPLogTrace(); + + awaitingPingResponse = NO; + [multicastDelegate xmppAutoPingDidReceivePong:self]; +} + +- (void)xmppPing:(XMPPPing *)sender didNotReceivePong:(NSString *)pingID dueToTimeout:(NSTimeInterval)timeout +{ + XMPPLogTrace(); + + awaitingPingResponse = NO; + [multicastDelegate xmppAutoPingDidTimeout:self]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender +{ + lastReceiveTime = [NSDate timeIntervalSinceReferenceDate]; + awaitingPingResponse = NO; + + [self startPingIntervalTimer]; +} + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + if (targetJID == nil || [targetJIDStr isEqualToString:[iq fromStr]]) + { + lastReceiveTime = [NSDate timeIntervalSinceReferenceDate]; + } + + return NO; +} + +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message +{ + if (targetJID == nil || [targetJIDStr isEqualToString:[message fromStr]]) + { + lastReceiveTime = [NSDate timeIntervalSinceReferenceDate]; + } +} + +- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence +{ + if (targetJID == nil || [targetJIDStr isEqualToString:[presence fromStr]]) + { + lastReceiveTime = [NSDate timeIntervalSinceReferenceDate]; + } +} + +- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error +{ + [self stopPingIntervalTimer]; + + lastReceiveTime = 0; + awaitingPingResponse = NO; +} + +@end diff --git a/Extensions/XEP-0199/XMPPPing.h b/Extensions/XEP-0199/XMPPPing.h new file mode 100644 index 0000000..6cc145b --- /dev/null +++ b/Extensions/XEP-0199/XMPPPing.h @@ -0,0 +1,51 @@ +#import +#import "XMPP.h" + +#define _XMPP_PING_H + +@class XMPPIDTracker; + + +@interface XMPPPing : XMPPModule +{ + BOOL respondsToQueries; + XMPPIDTracker *pingTracker; +} + +/** + * Whether or not the module should respond to incoming ping queries. + * It you create multiple instances of this module, only one instance should respond to queries. + * + * It is recommended you set this (if needed) before you activate the module. + * The default value is YES. +**/ +@property (readwrite) BOOL respondsToQueries; + +/** + * Send pings to the server or a specific JID. + * The disco module may be used to detect if the target supports ping. + * + * The returned string is the pingID (the elementID of the query that was sent). + * In other words: + * + * SEND: + * RECV: + * + * This may be helpful if you are sending multiple simultaneous pings to the same target. +**/ +- (NSString *)sendPingToServer; +- (NSString *)sendPingToServerWithTimeout:(NSTimeInterval)timeout; +- (NSString *)sendPingToJID:(XMPPJID *)jid; +- (NSString *)sendPingToJID:(XMPPJID *)jid withTimeout:(NSTimeInterval)timeout; + +@end + +@protocol XMPPPingDelegate +@optional + +- (void)xmppPing:(XMPPPing *)sender didReceivePong:(XMPPIQ *)pong withRTT:(NSTimeInterval)rtt; +- (void)xmppPing:(XMPPPing *)sender didNotReceivePong:(NSString *)pingID dueToTimeout:(NSTimeInterval)timeout; + +// Note: If the xmpp stream is disconnected, no delegate methods will be called, and outstanding pings are forgotten. + +@end diff --git a/Extensions/XEP-0199/XMPPPing.m b/Extensions/XEP-0199/XMPPPing.m new file mode 100644 index 0000000..e733ab7 --- /dev/null +++ b/Extensions/XEP-0199/XMPPPing.m @@ -0,0 +1,314 @@ +#import "XMPPPing.h" +#import "XMPPIDTracker.h" +#import "XMPPFramework.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#define DEFAULT_TIMEOUT 30.0 // seconds + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPPingInfo : XMPPBasicTrackingInfo +{ + NSDate *timeSent; +} + +@property (nonatomic, readonly) NSDate *timeSent; + +- (NSTimeInterval)rtt; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPPing + +- (id)init +{ + return [self initWithDispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super initWithDispatchQueue:queue])) + { + respondsToQueries = YES; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + if ([super activate:aXmppStream]) + { + #ifdef _XMPP_CAPABILITIES_H + [xmppStream autoAddDelegate:self delegateQueue:moduleQueue toModulesOfClass:[XMPPCapabilities class]]; + #endif + + pingTracker = [[XMPPIDTracker alloc] initWithDispatchQueue:moduleQueue]; + + return YES; + } + + return NO; +} + +- (void)deactivate +{ +#ifdef _XMPP_CAPABILITIES_H + [xmppStream removeAutoDelegate:self delegateQueue:moduleQueue fromModulesOfClass:[XMPPCapabilities class]]; +#endif + + dispatch_block_t block = ^{ @autoreleasepool { + + [pingTracker removeAllIDs]; + pingTracker = nil; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + [super deactivate]; +} + + +- (BOOL)respondsToQueries +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return respondsToQueries; + } + else + { + __block BOOL result; + + dispatch_sync(moduleQueue, ^{ + result = respondsToQueries; + }); + return result; + } +} + +- (void)setRespondsToQueries:(BOOL)flag +{ + dispatch_block_t block = ^{ + + if (respondsToQueries != flag) + { + respondsToQueries = flag; + + #ifdef _XMPP_CAPABILITIES_H + @autoreleasepool { + // Capabilities may have changed, need to notify others. + [xmppStream resendMyPresence]; + } + #endif + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (NSString *)generatePingIDWithTimeout:(NSTimeInterval)timeout +{ + // This method may be invoked on any thread/queue. + + // Generate unique ID for Ping packet + // It's important the ID be unique as the ID is the only thing that distinguishes a pong packet + + NSString *pingID = [xmppStream generateUUID]; + + dispatch_async(moduleQueue, ^{ @autoreleasepool { + + XMPPPingInfo *pingInfo = [[XMPPPingInfo alloc] initWithTarget:self + selector:@selector(handlePong:withInfo:) + timeout:timeout]; + + [pingTracker addID:pingID trackingInfo:pingInfo]; + + }}); + + return pingID; +} + +- (NSString *)sendPingToServer +{ + // This is a public method. + // It may be invoked on any thread/queue. + + return [self sendPingToServerWithTimeout:DEFAULT_TIMEOUT]; +} + +- (NSString *)sendPingToServerWithTimeout:(NSTimeInterval)timeout +{ + // This is a public method. + // It may be invoked on any thread/queue. + + NSString *pingID = [self generatePingIDWithTimeout:timeout]; + + // Send ping packet + // + // + // + // + + NSXMLElement *ping = [NSXMLElement elementWithName:@"ping" xmlns:@"urn:xmpp:ping"]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:nil elementID:pingID child:ping]; + + [xmppStream sendElement:iq]; + + return pingID; +} + +- (NSString *)sendPingToJID:(XMPPJID *)jid +{ + // This is a public method. + // It may be invoked on any thread/queue. + + return [self sendPingToJID:jid withTimeout:DEFAULT_TIMEOUT]; +} + +- (NSString *)sendPingToJID:(XMPPJID *)jid withTimeout:(NSTimeInterval)timeout +{ + // This is a public method. + // It may be invoked on any thread/queue. + + NSString *pingID = [self generatePingIDWithTimeout:timeout]; + + // Send ping element + // + // + // + // + + NSXMLElement *ping = [NSXMLElement elementWithName:@"ping" xmlns:@"urn:xmpp:ping"]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:jid elementID:pingID child:ping]; + + [xmppStream sendElement:iq]; + + return pingID; +} + +- (void)handlePong:(XMPPIQ *)pongIQ withInfo:(XMPPPingInfo *)pingInfo +{ + if (pongIQ) + { + [multicastDelegate xmppPing:self didReceivePong:pongIQ withRTT:[pingInfo rtt]]; + } + else + { + // Timeout + + [multicastDelegate xmppPing:self didNotReceivePong:[pingInfo elementID] dueToTimeout:[pingInfo timeout]]; + } +} + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + // This method is invoked on the moduleQueue. + + NSString *type = [iq type]; + + if ([type isEqualToString:@"result"] || [type isEqualToString:@"error"]) + { + // Example: + // + // + + // If this is a response to a ping that we've sent, + // then the pingTracker will invoke our handlePong:withInfo: method and return YES. + + return [pingTracker invokeForID:[iq elementID] withObject:iq]; + } + else if (respondsToQueries && [type isEqualToString:@"get"]) + { + // Example: + // + // + // + // + + NSXMLElement *ping = [iq elementForName:@"ping" xmlns:@"urn:xmpp:ping"]; + if (ping) + { + XMPPIQ *pong = [XMPPIQ iqWithType:@"result" to:[iq from] elementID:[iq elementID]]; + + [sender sendElement:pong]; + + return YES; + } + } + + return NO; +} + +- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error +{ + [pingTracker removeAllIDs]; +} + +#ifdef _XMPP_CAPABILITIES_H +/** + * If an XMPPCapabilites instance is used we want to advertise our support for ping. +**/ +- (void)xmppCapabilities:(XMPPCapabilities *)sender collectingMyCapabilities:(NSXMLElement *)query +{ + // This method is invoked on the moduleQueue. + + if (respondsToQueries) + { + // + // ... + // + // ... + // + + NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"]; + [feature addAttributeWithName:@"var" stringValue:@"urn:xmpp:ping"]; + + [query addChild:feature]; + } +} +#endif + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPPingInfo + +@synthesize timeSent; + +- (id)initWithTarget:(id)aTarget selector:(SEL)aSelector timeout:(NSTimeInterval)aTimeout +{ + if ((self = [super initWithTarget:aTarget selector:aSelector timeout:aTimeout])) + { + timeSent = [[NSDate alloc] init]; + } + return self; +} + +- (NSTimeInterval)rtt +{ + return [timeSent timeIntervalSinceNow] * -1.0; +} + + +@end diff --git a/Extensions/XEP-0202/XMPPAutoTime.h b/Extensions/XEP-0202/XMPPAutoTime.h new file mode 100644 index 0000000..7bd7c72 --- /dev/null +++ b/Extensions/XEP-0202/XMPPAutoTime.h @@ -0,0 +1,124 @@ +#import "XMPP.h" +#import "XMPPTime.h" + +#define _XMPP_AUTO_TIME_H + +@class XMPPJID; + +/** + * The XMPPAutoTime module monitors the time difference between our machine and the target. + * The target may simply be the server, or a specific resource. + * + * The module works by sending time queries to the target, and tracking the responses. + * The module will automatically send multiple queuries, and take into account the average RTT. + * It will also automatically update itself on a customizable interval, and whenever the machine's clock changes. + * + * This module is helpful when you are using timestamps from the target. + * For example, you may be receiving offline messages from your server. + * However, all these offline messages are timestamped from the server's clock. + * And the current machine's clock may vary considerably from the server's clock. + * Timezone differences don't matter as UTC is always used in XMPP, but clocks can easily differ. + * This may cause the user some confusion as server timestamps may reflect a time in the future, + * or much longer ago than in reality. +**/ +@interface XMPPAutoTime : XMPPModule +{ + NSTimeInterval recalibrationInterval; + XMPPJID *targetJID; + NSTimeInterval timeDifference; + + dispatch_time_t lastCalibrationTime; + dispatch_source_t recalibrationTimer; + + BOOL awaitingQueryResponse; + XMPPTime *xmppTime; + + NSData *lastServerAddress; + + NSDate *systemUptimeChecked; + NSTimeInterval systemUptime; +} + +/** + * How often to recalibrate the time difference. + * + * The module will automatically calculate the time difference when it is activated, + * or when it first sees the xmppStream become authenticated (whichever occurs first). + * After that first calculation, it will update itself according to this interval. + * + * To temporarily disable recalibration, set the interval to zero. + * + * The default recalibrationInterval is 24 hours. +**/ +@property (readwrite) NSTimeInterval recalibrationInterval; + +/** + * The target to query. + * + * If the targetJID is nil, this implies the target is the xmpp server we're connected to. + * If the targetJID is non-nil, it must be a full JID (user@domain.tld/rsrc). + * + * The default targetJID is nil. +**/ +@property (readwrite, strong) XMPPJID *targetJID; + +/** + * Returns the calculated time difference between our machine and the target. + * + * This is NOT a reference to the difference in time zones. + * Time zone differences generally shouldn't matter as xmpp standards mandate the use of UTC. + * + * Rather this is the difference between our UTC time, and the remote party's UTC time. + * If the two clocks are not synchronized, then the result represents the approximate difference. + * + * If our clock is earlier than the remote clock, then the value will be negative. + * If our clock is ahead of the remote clock, then the value will be positive. + * + * If you later receive a timestamp from the remote party, you can simply add the diff. + * For example: + * + * myTime = [givenTimeFromRemoteParty dateByAddingTimeInterval:diff]; +**/ +@property (readonly) NSTimeInterval timeDifference; + +/** + * Returns the date of the target based on the time difference. +**/ +@property (readonly) NSDate *date; + +/** + * The last time we've completed a calibration. +**/ +@property (readonly) dispatch_time_t lastCalibrationTime; + +/** + * XMPPAutoTime is used to automatically query a target for its time (and calculate the difference). + * Sometimes the target is also sending time requests to us as well. + * If so, you may optionally set respondsToQueries to YES to allow the module to respond to incoming time queries. + * + * If you create multiple instances of XMPPAutoTime or XMPPTime, + * then only one instance should respond to queries. + * + * The default value is NO. +**/ +@property (readwrite) BOOL respondsToQueries; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPAutoTimeDelegate +@optional + +- (void)xmppAutoTime:(XMPPAutoTime *)sender didUpdateTimeDifference:(NSTimeInterval)timeDifference; + +@end + +@interface XMPPStream (XMPPAutoTime) + +- (NSTimeInterval)xmppAutoTime_timeDifferenceForTargetJID:(XMPPJID *)targetJID; +- (NSDate *)xmppAutoTime_dateForTargetJID:(XMPPJID *)targetJID; + +@end diff --git a/Extensions/XEP-0202/XMPPAutoTime.m b/Extensions/XEP-0202/XMPPAutoTime.m new file mode 100644 index 0000000..86037d6 --- /dev/null +++ b/Extensions/XEP-0202/XMPPAutoTime.m @@ -0,0 +1,488 @@ +#import "XMPPAutoTime.h" +#import "XMPP.h" +#import "XMPPLogging.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +@interface XMPPAutoTime () + +@property (nonatomic, strong) NSData *lastServerAddress; +@property (nonatomic, strong) NSDate *systemUptimeChecked; + +- (void)updateRecalibrationTimer; +- (void)startRecalibrationTimer; +- (void)stopRecalibrationTimer; +@end + +#pragma mark - + +@implementation XMPPAutoTime + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super initWithDispatchQueue:queue])) + { + recalibrationInterval = (60 * 60 * 24); + + lastCalibrationTime = DISPATCH_TIME_FOREVER; + + xmppTime = [[XMPPTime alloc] initWithDispatchQueue:queue]; + xmppTime.respondsToQueries = NO; + + [xmppTime addDelegate:self delegateQueue:moduleQueue]; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + if ([super activate:aXmppStream]) + { + [xmppTime activate:aXmppStream]; + + self.systemUptimeChecked = [NSDate date]; + systemUptime = [[NSProcessInfo processInfo] systemUptime]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(systemClockDidChange:) + name:NSSystemClockDidChangeNotification + object:nil]; + + return YES; + } + + return NO; +} + +- (void)deactivate +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [self stopRecalibrationTimer]; + + [xmppTime deactivate]; + awaitingQueryResponse = NO; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + [super deactivate]; + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); +} + +- (void)dealloc +{ + // recalibrationTimer released in [self deactivate] + + [xmppTime removeDelegate:self]; + xmppTime = nil; // Might be referenced via [super dealloc] -> [self deactivate] +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@synthesize lastServerAddress; +@synthesize systemUptimeChecked; + +- (NSTimeInterval)recalibrationInterval +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return recalibrationInterval; + } + else + { + __block NSTimeInterval result; + + dispatch_sync(moduleQueue, ^{ + result = recalibrationInterval; + }); + return result; + } +} + +- (void)setRecalibrationInterval:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ + + if (recalibrationInterval != interval) + { + recalibrationInterval = interval; + + // Update the recalibrationTimer. + // + // Depending on new value and current state of the recalibrationTimer, + // this may mean starting, stoping, or simply updating the timer. + + if (recalibrationInterval > 0) + { + // Remember: Only start the timer after the xmpp stream is up and authenticated + if ([xmppStream isAuthenticated]) + [self startRecalibrationTimer]; + } + else + { + [self stopRecalibrationTimer]; + } + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (XMPPJID *)targetJID +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return targetJID; + } + else + { + __block XMPPJID *result; + + dispatch_sync(moduleQueue, ^{ + result = targetJID; + }); + return result; + } +} + +- (void)setTargetJID:(XMPPJID *)jid +{ + dispatch_block_t block = ^{ + + if (![targetJID isEqualToJID:jid]) + { + targetJID = jid; + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (NSTimeInterval)timeDifference +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return timeDifference; + } + else + { + __block NSTimeInterval result; + + dispatch_sync(moduleQueue, ^{ + result = timeDifference; + }); + + return result; + } +} + +- (NSDate *)date +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return [[NSDate date] dateByAddingTimeInterval:-timeDifference]; + } + else + { + __block NSDate *result; + + dispatch_sync(moduleQueue, ^{ + result = [[NSDate date] dateByAddingTimeInterval:-timeDifference]; + }); + + return result; + } +} + +- (dispatch_time_t)lastCalibrationTime +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return lastCalibrationTime; + } + else + { + __block dispatch_time_t result; + + dispatch_sync(moduleQueue, ^{ + result = lastCalibrationTime; + }); + + return result; + } +} + +- (BOOL)respondsToQueries +{ + return xmppTime.respondsToQueries; +} + +- (void)setRespondsToQueries:(BOOL)respondsToQueries +{ + xmppTime.respondsToQueries = respondsToQueries; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Notifications +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)systemClockDidChange:(NSNotification *)notification +{ + XMPPLogTrace(); + XMPPLogVerbose(@"NSSystemClockDidChangeNotification: %@", notification); + + if (lastCalibrationTime == DISPATCH_TIME_FOREVER) + { + // Doesn't matter, we haven't done a calibration yet. + return; + } + + // When the system clock changes, this affects our timeDifference. + // However, the notification doesn't tell us by how much the system clock has changed. + // So here's how we figure it out: + // + // The systemUptime isn't affected by the system clock. + // We previously recorded the system uptime, and simultaneously recoded the system clock time. + // We can now grab the current system uptime and current system clock time. + // Using the four data points we can calculate how much the system clock has changed. + + NSDate *now = [NSDate date]; + NSTimeInterval sysUptime = [[NSProcessInfo processInfo] systemUptime]; + + dispatch_async(moduleQueue, ^{ @autoreleasepool { + + // Calculate system clock change + + NSDate *oldSysTime = systemUptimeChecked; + NSDate *newSysTime = now; + + NSTimeInterval oldSysUptime = systemUptime; + NSTimeInterval newSysUptime = sysUptime; + + NSTimeInterval sysTimeDiff = [newSysTime timeIntervalSinceDate:oldSysTime]; + NSTimeInterval sysUptimeDiff = newSysUptime - oldSysUptime; + + NSTimeInterval sysClockChange = sysTimeDiff - sysUptimeDiff; + + // Modify timeDifference & notify delegate + + timeDifference += sysClockChange; + [multicastDelegate xmppAutoTime:self didUpdateTimeDifference:timeDifference]; + + // Dont forget to update our variables + + self.systemUptimeChecked = now; + systemUptime = sysUptime; + + }}); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Recalibration Timer +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)handleRecalibrationTimerFire +{ + XMPPLogTrace(); + + if (awaitingQueryResponse) return; + + awaitingQueryResponse = YES; + + if (targetJID) + [xmppTime sendQueryToJID:targetJID]; + else + [xmppTime sendQueryToServer]; +} + +- (void)updateRecalibrationTimer +{ + XMPPLogTrace(); + + NSAssert(recalibrationTimer != NULL, @"Broken logic (1)"); + NSAssert(recalibrationInterval > 0, @"Broken logic (2)"); + + + uint64_t interval = (recalibrationInterval * NSEC_PER_SEC); + dispatch_time_t tt; + + if (lastCalibrationTime == DISPATCH_TIME_FOREVER) + tt = dispatch_time(DISPATCH_TIME_NOW, 0); // First timer fire at (NOW) + else + tt = dispatch_time(lastCalibrationTime, interval); // First timer fire at (lastCalibrationTime + interval) + + dispatch_source_set_timer(recalibrationTimer, tt, interval, 0); +} + +- (void)startRecalibrationTimer +{ + XMPPLogTrace(); + + if (recalibrationInterval <= 0) + { + // Timer is disabled + return; + } + + BOOL newTimer = NO; + + if (recalibrationTimer == NULL) + { + newTimer = YES; + recalibrationTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue); + + dispatch_source_set_event_handler(recalibrationTimer, ^{ @autoreleasepool { + + [self handleRecalibrationTimerFire]; + + }}); + } + + [self updateRecalibrationTimer]; + + if (newTimer) + { + dispatch_resume(recalibrationTimer); + } +} + +- (void)stopRecalibrationTimer +{ + XMPPLogTrace(); + + if (recalibrationTimer) + { + #if !OS_OBJECT_USE_OBJC + dispatch_release(recalibrationTimer); + #endif + recalibrationTimer = NULL; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPTime Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppTime:(XMPPTime *)sender didReceiveResponse:(XMPPIQ *)iq withRTT:(NSTimeInterval)rtt +{ + XMPPLogTrace(); + + awaitingQueryResponse = NO; + + lastCalibrationTime = dispatch_time(DISPATCH_TIME_NOW, 0); + timeDifference = [XMPPTime approximateTimeDifferenceFromResponse:iq andRTT:rtt]; + + [multicastDelegate xmppAutoTime:self didUpdateTimeDifference:timeDifference]; +} + +- (void)xmppTime:(XMPPTime *)sender didNotReceiveResponse:(NSString *)queryID dueToTimeout:(NSTimeInterval)timeout +{ + XMPPLogTrace(); + + awaitingQueryResponse = NO; + + // Nothing to do here really. Most likely the server doesn't support XEP-0202. +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppStream:(XMPPStream *)sender socketDidConnect:(GCDAsyncSocket *)socket +{ + NSData *currentServerAddress = [socket connectedAddress]; + + if (lastServerAddress == nil) + { + self.lastServerAddress = currentServerAddress; + } + else if (![lastServerAddress isEqualToData:currentServerAddress]) + { + XMPPLogInfo(@"%@: Connected to a different server. Resetting calibration info.", [self class]); + + lastCalibrationTime = DISPATCH_TIME_FOREVER; + timeDifference = 0.0; + + self.lastServerAddress = currentServerAddress; + } +} + +- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender +{ + [self startRecalibrationTimer]; +} + +- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error +{ + [self stopRecalibrationTimer]; + + awaitingQueryResponse = NO; + + // We do NOT reset the lastCalibrationTime here. + // If we reconnect to the same server, the lastCalibrationTime remains valid. +} + +@end + +@implementation XMPPStream (XMPPAutoTime) + +- (NSTimeInterval)xmppAutoTime_timeDifferenceForTargetJID:(XMPPJID *)targetJID +{ + __block NSTimeInterval timeDifference = 0.0; + + [self enumerateModulesOfClass:[XMPPAutoTime class] withBlock:^(XMPPModule *module, NSUInteger idx, BOOL *stop) { + + XMPPAutoTime *autoTime = (XMPPAutoTime *)module; + + if([targetJID isEqualToJID:autoTime.targetJID] || (!targetJID && !autoTime.targetJID)) + { + timeDifference = autoTime.timeDifference; + *stop = YES; + } + }]; + + return timeDifference; +} + +- (NSDate *)xmppAutoTime_dateForTargetJID:(XMPPJID *)targetJID +{ + __block NSDate *date = [NSDate date]; + + [self enumerateModulesOfClass:[XMPPAutoTime class] withBlock:^(XMPPModule *module, NSUInteger idx, BOOL *stop) { + + XMPPAutoTime *autoTime = (XMPPAutoTime *)module; + + if([targetJID isEqualToJID:autoTime.targetJID] || (!targetJID && !autoTime.targetJID)) + { + date = autoTime.date; + *stop = YES; + } + }]; + + return date; +} + +@end diff --git a/Extensions/XEP-0202/XMPPTime.h b/Extensions/XEP-0202/XMPPTime.h new file mode 100644 index 0000000..4d8799b --- /dev/null +++ b/Extensions/XEP-0202/XMPPTime.h @@ -0,0 +1,90 @@ +#import +#import "XMPP.h" + +#define _XMPP_TIME_H + +@class XMPPIDTracker; +@protocol XMPPTimeDelegate; + + +@interface XMPPTime : XMPPModule +{ + BOOL respondsToQueries; + XMPPIDTracker *queryTracker; +} + +/** + * Whether or not the module should respond to incoming time queries. + * It you create multiple instances of this module, only one instance should respond to queries. + * + * It is recommended you set this (if needed) before you activate the module. + * The default value is YES. +**/ +@property (readwrite) BOOL respondsToQueries; + +/** + * Send query to the server or a specific JID. + * The disco module may be used to detect if the target supports this XEP. + * + * The returned string is the queryID (the elementID of the query that was sent). + * In other words: + * + * SEND: + * RECV: + * + * This may be helpful if you are sending multiple simultaneous queries to the same target. +**/ +- (NSString *)sendQueryToServer; +- (NSString *)sendQueryToServerWithTimeout:(NSTimeInterval)timeout; +- (NSString *)sendQueryToJID:(XMPPJID *)jid; +- (NSString *)sendQueryToJID:(XMPPJID *)jid withTimeout:(NSTimeInterval)timeout; + +/** + * Extracts the utc date from the given response/time element, + * and returns an NSDate representation of the time in the local time zone. + * Since the returned date is in the local time zone, it is suitable for presentation. +**/ ++ (NSDate *)dateFromResponse:(XMPPIQ *)iq; + +/** + * Extracts the time zone offset from the given response/time element. +**/ ++ (NSTimeZone *)timeZoneOffsetFromResponse:(XMPPIQ *)iq; + +/** + * Given the returned time response from a remote party, and the approximate round trip time, + * calculates the difference between our clock and the remote party's clock. + * + * This is NOT a reference to the difference in time zones. + * Time zone differences generally shouldn't matter as xmpp standards mandate the use of UTC. + * + * Rather this is the difference between our UTC time, and the remote party's UTC time. + * If the two clocks are not synchronized, then the result represents the approximate difference. + * + * If our clock is earlier than the remote clock, then the result will be negative. + * If our clock is ahead of the remote clock, then the result will be positive. + * + * If you later receive a timestamp from the remote party, you could add the diff. + * For example: + * + * myTime = [givenTimeFromRemoteParty dateByAddingTimeInterval:diff]; +**/ ++ (NSTimeInterval)approximateTimeDifferenceFromResponse:(XMPPIQ *)iq andRTT:(NSTimeInterval)rtt; + +/** + * Creates and returns a time element. +**/ ++ (NSXMLElement *)timeElement; ++ (NSXMLElement *)timeElementFromDate:(NSDate *)date; + +@end + +@protocol XMPPTimeDelegate +@optional + +- (void)xmppTime:(XMPPTime *)sender didReceiveResponse:(XMPPIQ *)iq withRTT:(NSTimeInterval)rtt; +- (void)xmppTime:(XMPPTime *)sender didNotReceiveResponse:(NSString *)queryID dueToTimeout:(NSTimeInterval)timeout; + +// Note: If the xmpp stream is disconnected, no delegate methods will be called, and outstanding queries are forgotten. + +@end diff --git a/Extensions/XEP-0202/XMPPTime.m b/Extensions/XEP-0202/XMPPTime.m new file mode 100644 index 0000000..3e09cd5 --- /dev/null +++ b/Extensions/XEP-0202/XMPPTime.m @@ -0,0 +1,505 @@ +#import "XMPPTime.h" +#import "XMPPIDTracker.h" +#import "XMPPDateTimeProfiles.h" +#import "XMPPFramework.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#define DEFAULT_TIMEOUT 30.0 // seconds + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPTimeQueryInfo : XMPPBasicTrackingInfo +{ + NSDate *timeSent; +} + +@property (nonatomic, readonly) NSDate *timeSent; + +- (NSTimeInterval)rtt; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPTime + +- (id)init +{ + return [self initWithDispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super initWithDispatchQueue:queue])) + { + respondsToQueries = YES; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + if ([super activate:aXmppStream]) + { + #ifdef _XMPP_CAPABILITIES_H + [xmppStream autoAddDelegate:self delegateQueue:moduleQueue toModulesOfClass:[XMPPCapabilities class]]; + #endif + + queryTracker = [[XMPPIDTracker alloc] initWithDispatchQueue:moduleQueue]; + + return YES; + } + + return NO; +} + +- (void)deactivate +{ +#ifdef _XMPP_CAPABILITIES_H + [xmppStream removeAutoDelegate:self delegateQueue:moduleQueue fromModulesOfClass:[XMPPCapabilities class]]; +#endif + + dispatch_block_t block = ^{ @autoreleasepool { + + [queryTracker removeAllIDs]; + queryTracker = nil; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + [super deactivate]; +} + + +- (BOOL)respondsToQueries +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return respondsToQueries; + } + else + { + __block BOOL result; + + dispatch_sync(moduleQueue, ^{ + result = respondsToQueries; + }); + return result; + } +} + +- (void)setRespondsToQueries:(BOOL)flag +{ + dispatch_block_t block = ^{ + + if (respondsToQueries != flag) + { + respondsToQueries = flag; + + #ifdef _XMPP_CAPABILITIES_H + @autoreleasepool { + // Capabilities may have changed, need to notify others. + [xmppStream resendMyPresence]; + } + #endif + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (NSString *)generateQueryIDWithTimeout:(NSTimeInterval)timeout +{ + // This method may be invoked on any thread/queue. + + // Generate unique ID for query. + // It's important the ID be unique as the ID is the + // only thing that distinguishes multiple queries from each other. + + NSString *queryID = [xmppStream generateUUID]; + + dispatch_async(moduleQueue, ^{ @autoreleasepool { + + XMPPTimeQueryInfo *queryInfo = [[XMPPTimeQueryInfo alloc] initWithTarget:self + selector:@selector(handleResponse:withInfo:) + timeout:timeout]; + + [queryTracker addID:queryID trackingInfo:queryInfo]; + + }}); + + return queryID; +} + +- (NSString *)sendQueryToServer +{ + // This is a public method. + // It may be invoked on any thread/queue. + + return [self sendQueryToServerWithTimeout:DEFAULT_TIMEOUT]; +} + +- (NSString *)sendQueryToServerWithTimeout:(NSTimeInterval)timeout +{ + // This is a public method. + // It may be invoked on any thread/queue. + + NSString *queryID = [self generateQueryIDWithTimeout:timeout]; + + // Send ping packet + // + // + // + // + // Note: Sometimes the to attribute is required. (ejabberd) + + NSXMLElement *time = [NSXMLElement elementWithName:@"time" xmlns:@"urn:xmpp:time"]; + XMPPJID *domainJID = [[xmppStream myJID] domainJID]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:domainJID elementID:queryID child:time]; + + [xmppStream sendElement:iq]; + + return queryID; +} + +- (NSString *)sendQueryToJID:(XMPPJID *)jid +{ + // This is a public method. + // It may be invoked on any thread/queue. + + return [self sendQueryToJID:jid withTimeout:DEFAULT_TIMEOUT]; +} + +- (NSString *)sendQueryToJID:(XMPPJID *)jid withTimeout:(NSTimeInterval)timeout +{ + // This is a public method. + // It may be invoked on any thread/queue. + + NSString *queryID = [self generateQueryIDWithTimeout:timeout]; + + // Send ping element + // + // + // + + NSXMLElement *time = [NSXMLElement elementWithName:@"time" xmlns:@"urn:xmpp:time"]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"get" to:jid elementID:queryID child:time]; + + [xmppStream sendElement:iq]; + + return queryID; +} + +- (void)handleResponse:(XMPPIQ *)iq withInfo:(XMPPTimeQueryInfo *)queryInfo +{ + if (iq) + { + if ([[iq type] isEqualToString:@"result"]) + [multicastDelegate xmppTime:self didReceiveResponse:iq withRTT:[queryInfo rtt]]; + else + [multicastDelegate xmppTime:self didNotReceiveResponse:[queryInfo elementID] dueToTimeout:-1.0]; + } + else + { + [multicastDelegate xmppTime:self didNotReceiveResponse:[queryInfo elementID] dueToTimeout:[queryInfo timeout]]; + } +} + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + // This method is invoked on the moduleQueue. + + NSString *type = [iq type]; + + if ([type isEqualToString:@"result"] || [type isEqualToString:@"error"]) + { + // Examples: + // + // + // + // + // + // + // + + return [queryTracker invokeForID:[iq elementID] withObject:iq]; + } + else if (respondsToQueries && [type isEqualToString:@"get"]) + { + // Example: + // + // + // + + NSXMLElement *time = [iq elementForName:@"time" xmlns:@"urn:xmpp:time"]; + if (time) + { + NSXMLElement *currentTime = [[self class] timeElement]; + + XMPPIQ *response = [XMPPIQ iqWithType:@"result" to:[iq from] elementID:[iq elementID]]; + [response addChild:currentTime]; + + [sender sendElement:response]; + + return YES; + } + } + + return NO; +} + +- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error +{ + [queryTracker removeAllIDs]; +} + +#ifdef _XMPP_CAPABILITIES_H +/** + * If an XMPPCapabilites instance is used we want to advertise our support for XEP-0202. +**/ +- (void)xmppCapabilities:(XMPPCapabilities *)sender collectingMyCapabilities:(NSXMLElement *)query +{ + // This method is invoked on the moduleQueue. + + if (respondsToQueries) + { + // + // ... + // + // ... + // + + NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"]; + [feature addAttributeWithName:@"var" stringValue:@"urn:xmpp:time"]; + + [query addChild:feature]; + } +} +#endif + ++ (NSDate *)dateFromResponse:(XMPPIQ *)iq +{ + // + // + // + + NSXMLElement *time = [iq elementForName:@"time" xmlns:@"urn:xmpp:time"]; + if (time == nil) return nil; + + NSString *utc = [[time elementForName:@"utc"] stringValue]; + if (utc == nil) return nil; + + // Note: + // + // NSDate is a very simple class, but can be confusing at times. + // NSDate simply stores an NSTimeInterval internally, + // which is just a double representing the number of seconds since the reference date. + // Since it's a double, it can yield sub-millisecond precision. + // + // In addition to this, it stores the values in UTC. + // However, if you print the value using NSLog via "%@", + // it will automatically print the date in the local timezone: + // + // NSDate *refDate = [NSDate dateWithTimeIntervalSinceReferenceDate:0.0]; + // + // NSLog(@"%f", [refDate timeIntervalSinceReferenceDate]); // Prints: 0.0 + // NSLog(@"%@", refDate); // Prints: 2000-12-31 19:00:00 -05:00 + // NSLog(@"%@", [utcDateFormatter stringFromDate:refDate]); // Prints: 2001-01-01 00:00:00 +00:00 + // + // Now the value we've received from XMPPDateTimeProfiles is correct. + // If we print it out using a utcDateFormatter we would see it is correct. + // If we printed it out generically using NSLog, then we would see it converted into our local time zone. + + return [XMPPDateTimeProfiles parseDateTime:utc]; +} + ++ (NSTimeZone *)timeZoneOffsetFromResponse:(XMPPIQ *)iq +{ + // + // + // + + NSXMLElement *time = [iq elementForName:@"time" xmlns:@"urn:xmpp:time"]; + if (time == nil) return 0; + + NSString *tzo = [[time elementForName:@"tzo"] stringValue]; + if (tzo == nil) return 0; + + return [XMPPDateTimeProfiles parseTimeZoneOffset:tzo]; +} + ++ (NSTimeInterval)approximateTimeDifferenceFromResponse:(XMPPIQ *)iq andRTT:(NSTimeInterval)rtt +{ + // First things first, get the current date and time + + NSDate *localDate = [NSDate date]; + + // Then worry about the calculations + + NSXMLElement *time = [iq elementForName:@"time" xmlns:@"urn:xmpp:time"]; + if (time == nil) return 0.0; + + NSString *utc = [[time elementForName:@"utc"] stringValue]; + if (utc == nil) return 0.0; + + NSDate *remoteDate = [XMPPDateTimeProfiles parseDateTime:utc]; + if (remoteDate == nil) return 0.0; + + NSTimeInterval localTI = [localDate timeIntervalSinceReferenceDate]; + NSTimeInterval remoteTI = [remoteDate timeIntervalSinceReferenceDate] - (rtt / 2.0); + + // Did the response contain millisecond precision? + // This is an important consideration. + // Imagine if both computers are perfectly synced, + // but the remote response doesn't contain milliseconds. + // This could possibly cause us to think the difference is close to a full second. + // + // DateTime examples (from XMPPDateTimeProfiles documentation): + // + // 1969-07-21T02:56:15 + // 1969-07-21T02:56:15Z + // 1969-07-20T21:56:15-05:00 + // 1969-07-21T02:56:15.123 + // 1969-07-21T02:56:15.123Z + // 1969-07-20T21:56:15.123-05:00 + + BOOL hasMilliseconds = ([utc length] > 19) && ([utc characterAtIndex:19] == '.'); + + if (hasMilliseconds) + { + return remoteTI - localTI; + } + else + { + // No milliseconds. What to do? + // + // We could simply truncate the milliseconds from our time... + // But this could make things much worse. + // For example: + // + // local = 14:22:36.750 + // remote = 14:22:37 + // + // If we truncate the result now we calculate a diff of 1.000 (a full second). + // Considering the remote's milliseconds could have been anything from 000 to 999, + // this means our calculations are: + // + // perfect : 0.1% chance + // diff too big : 75.0% chance + // diff too small : 24.9% chance + // + // Perhaps a better solution would give us a more even spread. + // We can do this by calculating the range: + // + // 37.000 - 36.750 = 0.25 + // 37.999 - 36.750 = 1.249 + // + // So a better guess of the diff is 0.750 (3/4 of a second): + // + // perfect : 0.1% chance + // diff too big : 50.0% chance + // diff too small : 49.9% chance + + NSTimeInterval diff1 = localTI - (remoteTI + 0.000); + NSTimeInterval diff2 = localTI - (remoteTI + 0.999); + + return ((diff1 + diff2) / 2.0); + } +} + ++ (NSXMLElement *)timeElement +{ + return [self timeElementFromDate:[NSDate date]]; +} + ++ (NSXMLElement *)timeElementFromDate:(NSDate *)date +{ + // + + NSDateFormatter *df = [[NSDateFormatter alloc] init]; + [df setFormatterBehavior:NSDateFormatterBehavior10_4]; // Use unicode patterns (as opposed to 10_3) + [df setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]; + [df setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"]; + [df setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + + NSString *utcValue = [df stringFromDate:date]; + + + NSInteger tzoInSeconds = [[NSTimeZone systemTimeZone] secondsFromGMTForDate:date]; + + NSInteger tzoH = tzoInSeconds / (60 * 60); + NSInteger tzoS = tzoInSeconds % (60 * 60); + + NSString *tzoValue = [NSString stringWithFormat:@"%+03li:%02li", (long)tzoH, (long)tzoS]; + + NSXMLElement *tzo = [NSXMLElement elementWithName:@"tzo" stringValue:tzoValue]; + NSXMLElement *utc = [NSXMLElement elementWithName:@"utc" stringValue:utcValue]; + + NSXMLElement *time = [NSXMLElement elementWithName:@"time" xmlns:@"urn:xmpp:time"]; + [time addChild:tzo]; + [time addChild:utc]; + + return time; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPTimeQueryInfo + +@synthesize timeSent; + +- (id)initWithTarget:(id)aTarget selector:(SEL)aSelector timeout:(NSTimeInterval)aTimeout +{ + if ((self = [super initWithTarget:aTarget selector:aSelector timeout:aTimeout])) + { + timeSent = [[NSDate alloc] init]; + } + return self; +} + +- (NSTimeInterval)rtt +{ + return [timeSent timeIntervalSinceNow] * -1.0; +} + + +@end diff --git a/Extensions/XEP-0203/NSXMLElement+XEP_0203.h b/Extensions/XEP-0203/NSXMLElement+XEP_0203.h new file mode 100644 index 0000000..27db923 --- /dev/null +++ b/Extensions/XEP-0203/NSXMLElement+XEP_0203.h @@ -0,0 +1,12 @@ +#import +#if TARGET_OS_IPHONE +#import "DDXML.h" +#endif + + +@interface NSXMLElement (XEP_0203) + +- (BOOL)wasDelayed; +- (NSDate *)delayedDeliveryDate; + +@end diff --git a/Extensions/XEP-0203/NSXMLElement+XEP_0203.m b/Extensions/XEP-0203/NSXMLElement+XEP_0203.m new file mode 100644 index 0000000..ee404d3 --- /dev/null +++ b/Extensions/XEP-0203/NSXMLElement+XEP_0203.m @@ -0,0 +1,84 @@ +#import "NSXMLElement+XEP_0203.h" +#import "XMPPDateTimeProfiles.h" +#import "NSXMLElement+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +@implementation NSXMLElement (XEP_0203) + +- (BOOL)wasDelayed +{ + NSXMLElement *delay; + + delay = [self elementForName:@"delay" xmlns:@"urn:xmpp:delay"]; + if (delay) + { + return YES; + } + + delay = [self elementForName:@"x" xmlns:@"jabber:x:delay"]; + if (delay) + { + return YES; + } + + return NO; +} + +- (NSDate *)delayedDeliveryDate +{ + NSXMLElement *delay; + + // From XEP-0203 (Delayed Delivery) + // + // + // + // The format [of the stamp attribute] MUST adhere to the dateTime format + // specified in XEP-0082 and MUST be expressed in UTC. + + delay = [self elementForName:@"delay" xmlns:@"urn:xmpp:delay"]; + if (delay) + { + NSString *stampValue = [delay attributeStringValueForName:@"stamp"]; + + // There are other considerations concerning XEP-0082. + // For example, it may optionally contain milliseconds. + // And it may possibly express UTC as "+00:00" instead of "Z". + // + // Thankfully there is already an implementation that takes into account all these possibilities. + + return [XMPPDateTimeProfiles parseDateTime:stampValue]; + } + + // From XEP-0091 (Legacy Delayed Delivery) + // + // + + delay = [self elementForName:@"x" xmlns:@"jabber:x:delay"]; + if (delay) + { + NSDate *stamp; + + NSString *stampValue = [delay attributeStringValueForName:@"stamp"]; + + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; + [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]; + [dateFormatter setDateFormat:@"yyyyMMdd'T'HH:mm:ss"]; + [dateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"GMT"]]; + + stamp = [dateFormatter dateFromString:stampValue]; + + return stamp; + } + + return nil; +} + +@end diff --git a/Extensions/XEP-0223/XEP_0223.h b/Extensions/XEP-0223/XEP_0223.h new file mode 100644 index 0000000..aa0670f --- /dev/null +++ b/Extensions/XEP-0223/XEP_0223.h @@ -0,0 +1,21 @@ +/** + * XEP-0223 : Persistent Storage of Private Data via PubSub + * + * This specification defines best practices for using the XMPP publish-subscribe extension to + * persistently store private information such as bookmarks and client configuration options. + * + * http://xmpp.org/extensions/xep-0223.html +**/ + +#import + + +@interface XEP_0223 : NSObject + +/** + * This method returns the recommended configuration options to configure a pubsub node for storing private data. + * It may be passed directly to the publishToNoe:::: method of XMPPPubSub. +**/ ++ (NSDictionary *)privateStoragePubSubOptions; + +@end diff --git a/Extensions/XEP-0223/XEP_0223.m b/Extensions/XEP-0223/XEP_0223.m new file mode 100644 index 0000000..129ce35 --- /dev/null +++ b/Extensions/XEP-0223/XEP_0223.m @@ -0,0 +1,20 @@ +/** + * XEP-0223 : Persistent Storage of Private Data via PubSub + * + * This specification defines best practices for using the XMPP publish-subscribe extension to + * persistently store private information such as bookmarks and client configuration options. + * + * http://xmpp.org/extensions/xep-0223.html +**/ + +#import "XEP_0223.h" + +@implementation XEP_0223 + ++ (NSDictionary *)privateStoragePubSubOptions +{ + return @{ @"pubsub#persist_items" : @(YES), + @"pubsub#access_model" : @"whitelist" }; +} + +@end diff --git a/Extensions/XEP-0224/XMPPAttentionModule.h b/Extensions/XEP-0224/XMPPAttentionModule.h new file mode 100644 index 0000000..c003ee7 --- /dev/null +++ b/Extensions/XEP-0224/XMPPAttentionModule.h @@ -0,0 +1,24 @@ +#import "XMPPModule.h" +#import "XMPPMessage+XEP_0224.h" + +#define _XMPP_ATTENTION_MODULE_H + +@interface XMPPAttentionModule : XMPPModule { + BOOL respondsToQueries; +} + +/** + * Whether or not the module should respond to incoming attention request queries. + * It you create multiple instances of this module, only one instance should respond to queries. + * + * It is recommended you set this (if needed) before you activate the module. + * The default value is YES. + **/ +@property (readwrite) BOOL respondsToQueries; + +@end + +@protocol XMPPAttentionDelegate +@optional +- (void)xmppAttention:(XMPPAttentionModule *)sender didReceiveAttentionHeadlineMessage:(XMPPMessage *)attentionRequest; +@end diff --git a/Extensions/XEP-0224/XMPPAttentionModule.m b/Extensions/XEP-0224/XMPPAttentionModule.m new file mode 100644 index 0000000..439152e --- /dev/null +++ b/Extensions/XEP-0224/XMPPAttentionModule.m @@ -0,0 +1,127 @@ +#import "XMPPAttentionModule.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +@implementation XMPPAttentionModule + +- (id)init +{ + return [self initWithDispatchQueue:NULL]; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if ((self = [super initWithDispatchQueue:queue])) + { + respondsToQueries = YES; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + if ([super activate:aXmppStream]) + { +#ifdef _XMPP_CAPABILITIES_H + [xmppStream autoAddDelegate:self delegateQueue:moduleQueue toModulesOfClass:[XMPPCapabilities class]]; +#endif + return YES; + } + + return NO; +} + +- (void)deactivate +{ +#ifdef _XMPP_CAPABILITIES_H + [xmppStream removeAutoDelegate:self delegateQueue:moduleQueue fromModulesOfClass:[XMPPCapabilities class]]; +#endif + + [super deactivate]; +} + +- (BOOL)respondsToQueries +{ + if (dispatch_get_specific(moduleQueueTag)) + { + return respondsToQueries; + } + else + { + __block BOOL result; + + dispatch_sync(moduleQueue, ^{ + result = respondsToQueries; + }); + return result; + } +} + +- (void)setRespondsToQueries:(BOOL)flag +{ + dispatch_block_t block = ^{ + + if (respondsToQueries != flag) + { + respondsToQueries = flag; + +#ifdef _XMPP_CAPABILITIES_H + @autoreleasepool { + // Capabilities may have changed, need to notify others. + [xmppStream resendMyPresence]; + } +#endif + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + + +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message +{ + // This method is invoked on the moduleQueue. + + // Format of an attention message. Body is optional and not used by clients like Pidgin + // + // + // Why don't you answer, Herbie? + // + + if ([message isAttentionMessage]) + { + [multicastDelegate xmppAttention:self didReceiveAttentionHeadlineMessage:message]; + } +} + + +#ifdef _XMPP_CAPABILITIES_H +/** + * If an XMPPCapabilites instance is used we want to advertise our support for attention requests. + **/ +- (void)xmppCapabilities:(XMPPCapabilities *)sender collectingMyCapabilities:(NSXMLElement *)query +{ + // This method is invoked on the moduleQueue. + + if (respondsToQueries) + { + // + // ... + // + // ... + // + + NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"]; + [feature addAttributeWithName:@"var" stringValue:XMLNS_ATTENTION]; + + [query addChild:feature]; + } +} +#endif + +@end diff --git a/Extensions/XEP-0224/XMPPMessage+XEP_0224.h b/Extensions/XEP-0224/XMPPMessage+XEP_0224.h new file mode 100644 index 0000000..45e81a2 --- /dev/null +++ b/Extensions/XEP-0224/XMPPMessage+XEP_0224.h @@ -0,0 +1,8 @@ +#import "XMPPMessage.h" +#define XMLNS_ATTENTION @"urn:xmpp:attention:0" + +@interface XMPPMessage (XEP_0224) +- (BOOL)isHeadLineMessage; +- (BOOL)isAttentionMessage; +- (BOOL)isAttentionMessageWithBody; +@end diff --git a/Extensions/XEP-0224/XMPPMessage+XEP_0224.m b/Extensions/XEP-0224/XMPPMessage+XEP_0224.m new file mode 100644 index 0000000..91bc941 --- /dev/null +++ b/Extensions/XEP-0224/XMPPMessage+XEP_0224.m @@ -0,0 +1,24 @@ +#import "XMPPMessage+XEP_0224.h" +#import "NSXMLElement+XMPP.h" + +@implementation XMPPMessage (XEP_0224) + +- (BOOL)isHeadLineMessage { + return [[[self attributeForName:@"type"] stringValue] isEqualToString:@"headline"]; +} + +- (BOOL)isAttentionMessage +{ + return [self isHeadLineMessage] && [self elementForName:@"attention" xmlns:XMLNS_ATTENTION]; +} + +- (BOOL)isAttentionMessageWithBody +{ + if([self isAttentionMessage]) + { + return [self isMessageWithBody]; + } + return NO; +} + +@end diff --git a/Extensions/XEP-0280/XMPPMessage+XEP_0280.h b/Extensions/XEP-0280/XMPPMessage+XEP_0280.h new file mode 100644 index 0000000..7f76965 --- /dev/null +++ b/Extensions/XEP-0280/XMPPMessage+XEP_0280.h @@ -0,0 +1,19 @@ +#import "XMPPMessage.h" +@class XMPPJID; + +@interface XMPPMessage (XEP_0280) + +- (NSXMLElement *)receivedMessageCarbon; +- (NSXMLElement *)sentMessageCarbon; + +- (BOOL)isMessageCarbon; +- (BOOL)isReceivedMessageCarbon; +- (BOOL)isSentMessageCarbon; +- (BOOL)isTrustedMessageCarbon; +- (BOOL)isTrustedMessageCarbonForMyJID:(XMPPJID *)jid; + +- (XMPPMessage *)messageCarbonForwardedMessage; + +- (void)addPrivateMessageCarbons; + +@end diff --git a/Extensions/XEP-0280/XMPPMessage+XEP_0280.m b/Extensions/XEP-0280/XMPPMessage+XEP_0280.m new file mode 100644 index 0000000..4229e3f --- /dev/null +++ b/Extensions/XEP-0280/XMPPMessage+XEP_0280.m @@ -0,0 +1,118 @@ +#import "XMPPMessage+XEP_0280.h" +#import "XMPPJID.h" +#import "NSXMLElement+XMPP.h" +#import "NSXMLElement+XEP_0297.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#define XMLNS_XMPP_MESSAGE_CARBONS @"urn:xmpp:carbons:2" + +@implementation XMPPMessage (XEP_0280) + +- (NSXMLElement *)receivedMessageCarbon +{ + return [self elementForName:@"received" xmlns:XMLNS_XMPP_MESSAGE_CARBONS]; +} + +- (NSXMLElement *)sentMessageCarbon +{ + return [self elementForName:@"sent" xmlns:XMLNS_XMPP_MESSAGE_CARBONS]; +} + + +- (BOOL)isMessageCarbon +{ + if([self isReceivedMessageCarbon] || [self isSentMessageCarbon]) + { + return YES; + } + else + { + return NO; + } +} + +- (BOOL)isReceivedMessageCarbon +{ + if([self receivedMessageCarbon]) + { + return YES; + } + else + { + return NO; + } +} + +- (BOOL)isSentMessageCarbon +{ + if([self sentMessageCarbon]) + { + return YES; + } + else + { + return NO; + } +} + +- (BOOL)isTrustedMessageCarbon +{ + BOOL isTrustedMessageCarbon = NO; + XMPPMessage *messageCarbonForwardedMessage = [self messageCarbonForwardedMessage]; + + if([self isSentMessageCarbon]) + { + if([[self from] isEqualToJID:[messageCarbonForwardedMessage from] options:XMPPJIDCompareBare]) + { + isTrustedMessageCarbon = YES; + } + + } + else if([self isReceivedMessageCarbon]) + { + if([[self from] isEqualToJID:[messageCarbonForwardedMessage to] options:XMPPJIDCompareBare]) + { + isTrustedMessageCarbon = YES; + } + } + + return isTrustedMessageCarbon; +} + +- (BOOL)isTrustedMessageCarbonForMyJID:(XMPPJID *)jid +{ + if([self isTrustedMessageCarbon] && [[jid bareJID] isEqualToJID:self.from]) + { + return YES; + } + else + { + return NO; + } +} + +- (XMPPMessage *)messageCarbonForwardedMessage +{ + NSXMLElement *carbon = nil; + + if([self receivedMessageCarbon]) + { + carbon = [self receivedMessageCarbon]; + } + else if([self sentMessageCarbon]) + { + carbon = [self sentMessageCarbon]; + } + + return [carbon forwardedMessage]; +} + +- (void)addPrivateMessageCarbons +{ + [self addChild:[NSXMLElement elementWithName:@"private" xmlns:XMLNS_XMPP_MESSAGE_CARBONS]]; +} + +@end diff --git a/Extensions/XEP-0280/XMPPMessageCarbons.h b/Extensions/XEP-0280/XMPPMessageCarbons.h new file mode 100644 index 0000000..6696085 --- /dev/null +++ b/Extensions/XEP-0280/XMPPMessageCarbons.h @@ -0,0 +1,65 @@ +#import "XMPPModule.h" +@class XMPPMessage; +@class XMPPIDTracker; + +#define _XMPP_MESSAGE_CARBONS_H + +@interface XMPPMessageCarbons : XMPPModule +{ + BOOL autoEnableMessageCarbons; + BOOL allowsUntrustedMessageCarbons; + BOOL messageCarbonsEnabled; + + XMPPIDTracker *xmppIDTracker; +} + +/** + * Wether or not to automatically enable Message Carbons. + * + * Default YES +**/ +@property (assign) BOOL autoEnableMessageCarbons; + +/** + * Wether Message Carbons is currently enabled or not. + * + * @see enableMessageCarbons + * @see disableMessageCarbons +**/ +@property (assign, getter = isMessageCarbonsEnabled,readonly) BOOL messageCarbonsEnabled; + +/** + * Whether Message Carbons are validated before calling the delegate methods. + * + * @see xmppMessageCarbons:willReceiveMessage:outgoing: + * @see xmppMessageCarbons:didReceiveMessage:outgoing: + * + * A Message Carbon is Trusted if: + * + * - It is from the Stream's Bare JID + * - Sent Forward Messages are from the Stream's JID + * - Received Forward Messages are to the Stream's JID + * + * Default is NO +**/ +@property (assign) BOOL allowsUntrustedMessageCarbons; + +/** + * Enable Message Carbons +**/ +- (void)enableMessageCarbons; + +/** + * Disable Message Carbons +**/ +- (void)disableMessageCarbons; + +@end + +@protocol XMPPMessageCarbonsDelegate + +- (void)xmppMessageCarbons:(XMPPMessageCarbons *)xmppMessageCarbons willReceiveMessage:(XMPPMessage *)message outgoing:(BOOL)isOutgoing; + +- (void)xmppMessageCarbons:(XMPPMessageCarbons *)xmppMessageCarbons didReceiveMessage:(XMPPMessage *)message outgoing:(BOOL)isOutgoing; + +@end \ No newline at end of file diff --git a/Extensions/XEP-0280/XMPPMessageCarbons.m b/Extensions/XEP-0280/XMPPMessageCarbons.m new file mode 100644 index 0000000..6111bfa --- /dev/null +++ b/Extensions/XEP-0280/XMPPMessageCarbons.m @@ -0,0 +1,293 @@ +#import "XMPPMessageCarbons.h" +#import "XMPP.h" +#import "XMPPFramework.h" +#import "XMPPLogging.h" +#import "XMPPIDTracker.h" +#import "NSXMLElement+XEP_0297.h" +#import "XMPPMessage+XEP_0280.h" +#import "XMPPInternal.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG +static const int xmppLogLevel = XMPP_LOG_LEVEL_VERBOSE; // | XMPP_LOG_FLAG_TRACE; +#else +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + + +#define XMLNS_XMPP_MESSAGE_CARBONS @"urn:xmpp:carbons:2" + +@implementation XMPPMessageCarbons + +- (id)initWithDispatchQueue:(dispatch_queue_t)queue +{ + if((self = [super initWithDispatchQueue:queue])) + { + autoEnableMessageCarbons = YES; + } + return self; +} + +- (BOOL)activate:(XMPPStream *)aXmppStream +{ + XMPPLogTrace(); + + if ([super activate:aXmppStream]) + { + XMPPLogVerbose(@"%@: Activated", THIS_FILE); + + xmppIDTracker = [[XMPPIDTracker alloc] initWithDispatchQueue:moduleQueue]; + + return YES; + } + + return NO; +} + +- (void)deactivate +{ + XMPPLogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool { + + [xmppIDTracker removeAllIDs]; + xmppIDTracker = nil; + + }}; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + [super deactivate]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)autoEnableMessageCarbons +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = autoEnableMessageCarbons; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setAutoEnableMessageCarbons:(BOOL)flag +{ + dispatch_block_t block = ^{ + autoEnableMessageCarbons = flag; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (BOOL)isMessageCarbonsEnabled +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = messageCarbonsEnabled; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (BOOL)allowsUntrustedMessageCarbons +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = allowsUntrustedMessageCarbons; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); + + return result; +} + +- (void)setAllowsUntrustedMessageCarbons:(BOOL)flag +{ + dispatch_block_t block = ^{ + allowsUntrustedMessageCarbons = flag; + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_async(moduleQueue, block); +} + +- (void)enableMessageCarbons +{ + dispatch_block_t block = ^{ + + if(!messageCarbonsEnabled && [xmppIDTracker numberOfIDs] == 0) + { + NSString *elementID = [XMPPStream generateUUID]; + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" elementID:elementID]; + [iq setXmlns:@"jabber:client"]; + + NSXMLElement *enable = [NSXMLElement elementWithName:@"enable" xmlns:XMLNS_XMPP_MESSAGE_CARBONS]; + [iq addChild:enable]; + + [xmppIDTracker addElement:iq + target:self + selector:@selector(enableMessageCarbonsIQ:withInfo:) + timeout:XMPPIDTrackerTimeoutNone]; + + [xmppStream sendElement:iq]; + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); +} + +- (void)disableMessageCarbons +{ + dispatch_block_t block = ^{ + + if(messageCarbonsEnabled && [xmppIDTracker numberOfIDs] == 0) + { + NSString *elementID = [XMPPStream generateUUID]; + + XMPPIQ *iq = [XMPPIQ iqWithType:@"set" elementID:elementID]; + [iq setXmlns:@"jabber:client"]; + + NSXMLElement *enable = [NSXMLElement elementWithName:@"disable" xmlns:XMLNS_XMPP_MESSAGE_CARBONS]; + [iq addChild:enable]; + + [xmppIDTracker addElement:iq + target:self + selector:@selector(disableMessageCarbonsIQ:withInfo:) + timeout:XMPPIDTrackerTimeoutNone]; + + [xmppStream sendElement:iq]; + } + }; + + if (dispatch_get_specific(moduleQueueTag)) + block(); + else + dispatch_sync(moduleQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender +{ + XMPPLogTrace(); + + messageCarbonsEnabled = NO; + + if(self.autoEnableMessageCarbons) + { + [self enableMessageCarbons]; + } +} + +- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error +{ + messageCarbonsEnabled = NO; +} + +- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq +{ + [xmppIDTracker invokeForID:[iq elementID] withObject:iq]; + + return NO; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark XMPPStream Delegate +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)enableMessageCarbonsIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)trackerInfo +{ + XMPPLogTrace(); + + if([iq isResultIQ]) + { + messageCarbonsEnabled = YES; + } +} + +- (void)disableMessageCarbonsIQ:(XMPPIQ *)iq withInfo:(XMPPBasicTrackingInfo *)trackerInfo +{ + XMPPLogTrace(); + + if([iq isResultIQ]) + { + messageCarbonsEnabled = NO; + } +} + +- (XMPPMessage *)xmppStream:(XMPPStream *)sender willReceiveMessage:(XMPPMessage *)message +{ + XMPPLogTrace(); + + if([message isTrustedMessageCarbonForMyJID:sender.myJID] || + ([message isMessageCarbon] && allowsUntrustedMessageCarbons)) + { + BOOL outgoing = [message isSentMessageCarbon]; + + XMPPMessage *messageCarbonForwardedMessage = [message messageCarbonForwardedMessage]; + + [multicastDelegate xmppMessageCarbons:self + willReceiveMessage:messageCarbonForwardedMessage + outgoing:outgoing]; + } + + return message; +} + +- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message +{ + XMPPLogTrace(); + + if([message isTrustedMessageCarbonForMyJID:sender.myJID] || + ([message isMessageCarbon] && allowsUntrustedMessageCarbons)) + { + BOOL outgoing = [message isSentMessageCarbon]; + + XMPPMessage *messageCarbonForwardedMessage = [message messageCarbonForwardedMessage]; + + [multicastDelegate xmppMessageCarbons:self + didReceiveMessage:messageCarbonForwardedMessage + outgoing:outgoing]; + } +} + + +@end diff --git a/Extensions/XEP-0297/NSXMLElement+XEP_0297.h b/Extensions/XEP-0297/NSXMLElement+XEP_0297.h new file mode 100644 index 0000000..cebec07 --- /dev/null +++ b/Extensions/XEP-0297/NSXMLElement+XEP_0297.h @@ -0,0 +1,39 @@ +#import + +#if TARGET_OS_IPHONE + #import "DDXML.h" +#endif + +@class XMPPIQ; +@class XMPPMessage; +@class XMPPPresence; + +@interface NSXMLElement (XEP_0297) + +#pragma mark Forwarded Stanza + +- (NSXMLElement *)forwardedStanza; + +- (BOOL)hasForwardedStanza; + +- (BOOL)isForwardedStanza; + +#pragma mark Delayed Delivery Date + +- (NSDate *)forwardedStanzaDelayedDeliveryDate; + +#pragma mark XMPPElement + +- (XMPPIQ *)forwardedIQ; + +- (BOOL)hasForwardedIQ; + +- (XMPPMessage *)forwardedMessage; + +- (BOOL)hasForwardedMessage; + +- (XMPPPresence *)forwardedPresence; + +- (BOOL)hasForwardedPresence; + +@end diff --git a/Extensions/XEP-0297/NSXMLElement+XEP_0297.m b/Extensions/XEP-0297/NSXMLElement+XEP_0297.m new file mode 100644 index 0000000..bda7682 --- /dev/null +++ b/Extensions/XEP-0297/NSXMLElement+XEP_0297.m @@ -0,0 +1,138 @@ +#import "NSXMLElement+XEP_0297.h" +#import "NSXMLElement+XMPP.h" +#import "NSXMLElement+XEP_0203.h" +#import "XMPPIQ.h" +#import "XMPPMessage.h" +#import "XMPPPresence.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#define NAME_XMPP_STANZA_FORWARDING @"forwarded" +#define XMLNS_XMPP_STANZA_FORWARDING @"urn:xmpp:forward:0" + +@implementation NSXMLElement (XEP_0297) + +#pragma mark Forwarded Stanza + +- (NSXMLElement *)forwardedStanza +{ + return [self elementForName:NAME_XMPP_STANZA_FORWARDING xmlns:XMLNS_XMPP_STANZA_FORWARDING]; +} + +- (BOOL)hasForwardedStanza +{ + if([self forwardedStanza]) + { + return YES; + } + else + { + return NO; + } +} + +- (BOOL)isForwardedStanza +{ + if([[self name] isEqualToString:NAME_XMPP_STANZA_FORWARDING] && [[self xmlns] isEqualToString:XMLNS_XMPP_STANZA_FORWARDING]) + { + return YES; + } + else + { + return NO; + } +} + +#pragma mark Delayed Delivery Date + +- (NSDate *)forwardedStanzaDelayedDeliveryDate +{ + if([self isForwardedStanza]) + { + return [self delayedDeliveryDate]; + } + else + { + return [[self forwardedStanza] delayedDeliveryDate]; + } +} + + +#pragma mark XMPPElement + +- (XMPPIQ *)forwardedIQ +{ + if([self isForwardedStanza]) + { + return [XMPPIQ iqFromElement:[self elementForName:@"iq"]]; + } + else + { + return [XMPPIQ iqFromElement:[[self forwardedStanza] elementForName:@"iq"]]; + } +} + +- (BOOL)hasForwardedIQ +{ + if([self forwardedIQ]) + { + return YES; + } + else + { + return NO; + } +} + +- (XMPPMessage *)forwardedMessage +{ + if([self isForwardedStanza]) + { + return [XMPPMessage messageFromElement:[self elementForName:@"message"]]; + } + else + { + return [XMPPMessage messageFromElement:[[self forwardedStanza] elementForName:@"message"]]; + } +} + +- (BOOL)hasForwardedMessage +{ + if([self forwardedMessage]) + { + return YES; + } + else + { + return NO; + } +} + + +- (XMPPPresence *)forwardedPresence +{ + if([self isForwardedStanza]) + { + return [XMPPPresence presenceFromElement:[self elementForName:@"presence"]]; + } + else + { + return [XMPPPresence presenceFromElement:[[self forwardedStanza] elementForName:@"presence"]]; + } +} + +- (BOOL)hasForwardedPresence +{ + if([self forwardedPresence]) + { + return YES; + } + else + { + return NO; + } +} + +@end diff --git a/Extensions/XEP-0308/XMPPMessage+XEP_0308.h b/Extensions/XEP-0308/XMPPMessage+XEP_0308.h new file mode 100644 index 0000000..eec8267 --- /dev/null +++ b/Extensions/XEP-0308/XMPPMessage+XEP_0308.h @@ -0,0 +1,14 @@ +#import "XMPPMessage.h" + +@interface XMPPMessage (XEP_0308) + +- (BOOL)isMessageCorrection; + +- (NSString *)correctedMessageID; + +- (void)addMessageCorrectionWithID:(NSString *)messageCorrectionID; + +- (XMPPMessage *)generateCorrectionMessageWithID:(NSString *)elementID; +- (XMPPMessage *)generateCorrectionMessageWithID:(NSString *)elementID body:(NSString *)body; + +@end diff --git a/Extensions/XEP-0308/XMPPMessage+XEP_0308.m b/Extensions/XEP-0308/XMPPMessage+XEP_0308.m new file mode 100644 index 0000000..d34c8db --- /dev/null +++ b/Extensions/XEP-0308/XMPPMessage+XEP_0308.m @@ -0,0 +1,88 @@ +#import "XMPPMessage+XEP_0308.h" +#import "NSXMLElement+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#define NAME_XMPP_MESSAGE_CORRECT @"replace" +#define XMLNS_XMPP_MESSAGE_CORRECT @"urn:xmpp:message-correct:0" + +@implementation XMPPMessage (XEP_0308) + +- (BOOL)isMessageCorrection +{ + if([[self correctedMessageID] length]) + { + return YES; + } + else + { + return NO; + } +} + +- (NSString *)correctedMessageID +{ + return [[self elementForName:NAME_XMPP_MESSAGE_CORRECT xmlns:XMLNS_XMPP_MESSAGE_CORRECT] attributeStringValueForName:@"id"]; +} + +- (void)addMessageCorrectionWithID:(NSString *)messageCorrectionID +{ + NSXMLElement *replace = [NSXMLElement elementWithName:NAME_XMPP_MESSAGE_CORRECT stringValue:XMLNS_XMPP_MESSAGE_CORRECT]; + [replace addAttributeWithName:@"id" stringValue:messageCorrectionID]; + [self addChild:replace]; +} + +- (XMPPMessage *)generateCorrectionMessageWithID:(NSString *)elementID +{ + XMPPMessage *correctionMessage = nil; + + if([[self elementID] length] && ![self isMessageCorrection]) + { + correctionMessage = [self copy]; + + [correctionMessage removeAttributeForName:@"id"]; + + if([elementID length]) + { + [correctionMessage addAttributeWithName:@"id" stringValue:elementID]; + } + + [correctionMessage addMessageCorrectionWithID:[self elementID]]; + } + + return correctionMessage; +} + +- (XMPPMessage *)generateCorrectionMessageWithID:(NSString *)elementID body:(NSString *)body +{ + XMPPMessage *correctionMessage = nil; + + if([[self elementID] length] && ![self isMessageCorrection]) + { + correctionMessage = [self copy]; + + [correctionMessage removeAttributeForName:@"id"]; + + if([elementID length]) + { + [correctionMessage addAttributeWithName:@"id" stringValue:elementID]; + } + + NSXMLElement *bodyElement = [correctionMessage elementForName:@"body"]; + + if(bodyElement) + { + [correctionMessage removeChildAtIndex:[[correctionMessage children] indexOfObject:bodyElement]]; + } + + [self addBody:body]; + + [correctionMessage addMessageCorrectionWithID:[self elementID]]; + } + + return correctionMessage; +} + +@end diff --git a/Extensions/XEP-0333/XMPPMessage+XEP_0333.h b/Extensions/XEP-0333/XMPPMessage+XEP_0333.h new file mode 100644 index 0000000..e7e70e6 --- /dev/null +++ b/Extensions/XEP-0333/XMPPMessage+XEP_0333.h @@ -0,0 +1,28 @@ +#import "XMPPMessage.h" + +@interface XMPPMessage (XEP_0333) + +- (BOOL)hasChatMarker; + +- (BOOL)hasMarkableChatMarker; +- (BOOL)hasReceivedChatMarker; +- (BOOL)hasDisplayedChatMarker; +- (BOOL)hasAcknowledgedChatMarker; + +- (NSString *)chatMarker; +- (NSString *)chatMarkerID; + +- (void)addMarkableChatMarker; +- (void)addReceivedChatMarkerWithID:(NSString *)elementID; +- (void)addDisplayedChatMarkerWithID:(NSString *)elementID; +- (void)addAcknowledgedChatMarkerWithID:(NSString *)elementID; + +- (XMPPMessage *)generateReceivedChatMarker; +- (XMPPMessage *)generateDisplayedChatMarker; +- (XMPPMessage *)generateAcknowledgedChatMarker; + +- (XMPPMessage *)generateReceivedChatMarkerIncludingThread:(BOOL)includingThread; +- (XMPPMessage *)generateDisplayedChatMarkerIncludingThread:(BOOL)includingThread; +- (XMPPMessage *)generateAcknowledgedChatMarkerIncludingThread:(BOOL)includingThread; + +@end diff --git a/Extensions/XEP-0333/XMPPMessage+XEP_0333.m b/Extensions/XEP-0333/XMPPMessage+XEP_0333.m new file mode 100644 index 0000000..6159142 --- /dev/null +++ b/Extensions/XEP-0333/XMPPMessage+XEP_0333.m @@ -0,0 +1,142 @@ +#import "XMPPMessage+XEP_0333.h" +#import "NSXMLElement+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#define XMLNS_CHAT_MARKERS @"urn:xmpp:chat-markers:0" + +#define MARKABLE_NAME @"markable" +#define RECEIVED_NAME @"received" +#define DISPLAYED_NAME @"displayed" +#define ACKNOWLEDGED_NAME @"acknowledged" + +@implementation XMPPMessage (XEP_0333) + +- (BOOL)hasChatMarker +{ + return ([[self elementsForXmlns:XMLNS_CHAT_MARKERS] count] > 0); +} + +- (BOOL)hasMarkableChatMarker +{ + return ([self elementForName:MARKABLE_NAME xmlns:XMLNS_CHAT_MARKERS] != nil); +} + +- (BOOL)hasReceivedChatMarker +{ + return ([self elementForName:RECEIVED_NAME xmlns:XMLNS_CHAT_MARKERS] != nil); +} + +- (BOOL)hasDisplayedChatMarker +{ + return ([self elementForName:DISPLAYED_NAME xmlns:XMLNS_CHAT_MARKERS] != nil); +} + +- (BOOL)hasAcknowledgedChatMarker +{ + return ([self elementForName:ACKNOWLEDGED_NAME xmlns:XMLNS_CHAT_MARKERS] != nil); +} + +- (NSString *)chatMarker +{ + return [[[self elementsForXmlns:XMLNS_CHAT_MARKERS] lastObject] name]; +} + +- (NSString *)chatMarkerID +{ + return [[[self elementsForXmlns:XMLNS_CHAT_MARKERS] lastObject] attributeStringValueForName:@"id"]; +} + +- (NSString *)chatMarkerThread +{ + return [[[self elementsForXmlns:XMLNS_CHAT_MARKERS] lastObject] attributeStringValueForName:@"thread"]; +} + +- (void)addMarkableChatMarker +{ + NSXMLElement *markableDisplayedMarker = [[NSXMLElement alloc] initWithName:MARKABLE_NAME xmlns:XMLNS_CHAT_MARKERS]; + [self addChild:markableDisplayedMarker]; +} + +- (void)addReceivedChatMarkerWithID:(NSString *)elementID +{ + NSXMLElement *receivedChatMarker = [[NSXMLElement alloc] initWithName:RECEIVED_NAME xmlns:XMLNS_CHAT_MARKERS]; + [receivedChatMarker addAttributeWithName:@"id" stringValue:elementID]; + + [self addChild:receivedChatMarker]; +} + +- (void)addDisplayedChatMarkerWithID:(NSString *)elementID +{ + NSXMLElement *readDisplayedMarker = [[NSXMLElement alloc] initWithName:DISPLAYED_NAME xmlns:XMLNS_CHAT_MARKERS]; + [readDisplayedMarker addAttributeWithName:@"id" stringValue:elementID]; + + [self addChild:readDisplayedMarker]; +} + +- (void)addAcknowledgedChatMarkerWithID:(NSString *)elementID +{ + NSXMLElement *acknowledgedChatMarker = [[NSXMLElement alloc] initWithName:ACKNOWLEDGED_NAME xmlns:XMLNS_CHAT_MARKERS]; + [acknowledgedChatMarker addAttributeWithName:@"id" stringValue:elementID]; + + [self addChild:acknowledgedChatMarker]; +} + +- (XMPPMessage *)generateReceivedChatMarker +{ + return [self generateReceivedChatMarkerIncludingThread:NO]; +} + +- (XMPPMessage *)generateDisplayedChatMarker +{ + return [self generateDisplayedChatMarkerIncludingThread:NO]; +} + +- (XMPPMessage *)generateAcknowledgedChatMarker +{ + return [self generateAcknowledgedChatMarkerIncludingThread:NO]; +} + +- (XMPPMessage *)generateReceivedChatMarkerIncludingThread:(BOOL)includingThread +{ + XMPPMessage *message = [XMPPMessage message]; + [message addAttributeWithName:@"to" stringValue:[self fromStr]]; + + if(includingThread && [self thread]) + { + [message addThread:[self thread]]; + } + + [message addReceivedChatMarkerWithID:[self elementID]]; + + return message; +} +- (XMPPMessage *)generateDisplayedChatMarkerIncludingThread:(BOOL)includingThread +{ + XMPPMessage *message = [XMPPMessage message]; + [message addAttributeWithName:@"to" stringValue:[self fromStr]]; + + if(includingThread && [self thread]) + { + [message addThread:[self thread]]; + } + + [message addDisplayedChatMarkerWithID:[self elementID]]; + return message; +} +- (XMPPMessage *)generateAcknowledgedChatMarkerIncludingThread:(BOOL)includingThread +{ + XMPPMessage *message = [XMPPMessage message]; + [message addAttributeWithName:@"to" stringValue:[self fromStr]]; + + if(includingThread && [self thread]) + { + [message addThread:[self thread]]; + } + + [message addAcknowledgedChatMarkerWithID:[self elementID]]; + return message; +} +@end diff --git a/Extensions/XEP-0335/NSXMLElement+XEP_0335.h b/Extensions/XEP-0335/NSXMLElement+XEP_0335.h new file mode 100644 index 0000000..334e511 --- /dev/null +++ b/Extensions/XEP-0335/NSXMLElement+XEP_0335.h @@ -0,0 +1,21 @@ +#import +#if TARGET_OS_IPHONE +#import "DDXML.h" +#endif + +@interface NSXMLElement (XEP_0335) + +- (NSXMLElement *)JSONContainer; + +- (BOOL)isJSONContainer; +- (BOOL)hasJSONContainer; + +- (NSString *)JSONContainerString; +- (NSData *)JSONContainerData; +- (id)JSONContainerObject; + +- (void)addJSONContainerWithString:(NSString *)JSONContainerString; +- (void)addJSONContainerWithData:(NSData *)JSONContainerData; +- (void)addJSONContainerWithObject:(id)JSONContainerObject; + +@end diff --git a/Extensions/XEP-0335/NSXMLElement+XEP_0335.m b/Extensions/XEP-0335/NSXMLElement+XEP_0335.m new file mode 100644 index 0000000..7c4a534 --- /dev/null +++ b/Extensions/XEP-0335/NSXMLElement+XEP_0335.m @@ -0,0 +1,87 @@ +#import "NSXMLElement+XEP_0335.h" +#import "NSXMLElement+XMPP.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#define XEP_0335_NAME @"json" +#define XEP_0335_XMLNS @"urn:xmpp:json:0" + +@implementation NSXMLElement (XEP_0335) + +- (NSXMLElement *)JSONContainer +{ + if([self isJSONContainer]) + { + return self; + } + else + { + return [self elementForName:XEP_0335_NAME xmlns:XEP_0335_XMLNS]; + } +} + +- (BOOL)isJSONContainer +{ + if([[self name] isEqualToString:XEP_0335_NAME] && [[self xmlns] isEqualToString:XEP_0335_XMLNS]) + { + return YES; + } + else + { + return NO; + } +} + +- (BOOL)hasJSONContainer +{ + return [self elementForName:XEP_0335_NAME xmlns:XEP_0335_XMLNS] != nil; +} + +- (NSString *)JSONContainerString +{ + return [[self JSONContainer] stringValue]; +} + +- (NSData *)JSONContainerData +{ + NSString *JSONContainerString = [self JSONContainerString]; + return [JSONContainerString dataUsingEncoding:NSUTF8StringEncoding]; +} + +- (id)JSONContainerObject +{ + NSData *JSONData = [self JSONContainerData]; + return [NSJSONSerialization JSONObjectWithData:JSONData options:0 error:nil]; +} + +- (void)addJSONContainerWithString:(NSString *)JSONContainerString +{ + if([JSONContainerString length]) + { + NSXMLElement *container = [NSXMLElement elementWithName:XEP_0335_NAME xmlns:XEP_0335_XMLNS]; + [container setStringValue:JSONContainerString]; + [self addChild:container]; + } +} + +- (void)addJSONContainerWithData:(NSData *)JSONContainerData +{ + if([JSONContainerData length]) + { + NSString *JSONContainerString = [[NSString alloc] initWithData:JSONContainerData encoding:NSUTF8StringEncoding]; + [self addJSONContainerWithString:JSONContainerString]; + } +} + +- (void)addJSONContainerWithObject:(id)JSONContainerObject +{ + if([NSJSONSerialization isValidJSONObject:JSONContainerObject]) + { + NSData *JSONContainerData = [NSJSONSerialization dataWithJSONObject:JSONContainerObject options:0 error:nil]; + [self addJSONContainerWithData:JSONContainerData]; + } +} + +@end diff --git a/Extensions/XEP-0352/NSXMLElement+XEP_0352.h b/Extensions/XEP-0352/NSXMLElement+XEP_0352.h new file mode 100644 index 0000000..7c360fc --- /dev/null +++ b/Extensions/XEP-0352/NSXMLElement+XEP_0352.h @@ -0,0 +1,9 @@ + +#import "NSXMLElement+XMPP.h" + +@interface NSXMLElement (XEP0352) + ++ (instancetype)indicateInactiveElement; ++ (instancetype)indicateActiveElement; + +@end diff --git a/Extensions/XEP-0352/NSXMLElement+XEP_0352.m b/Extensions/XEP-0352/NSXMLElement+XEP_0352.m new file mode 100644 index 0000000..1e02012 --- /dev/null +++ b/Extensions/XEP-0352/NSXMLElement+XEP_0352.m @@ -0,0 +1,18 @@ + +#import "NSXMLElement+XEP_0352.h" + +#define XMLNS_XMPP_CLIENT_STATE_INDICATION @"urn:xmpp:csi:0" + +@implementation NSXMLElement (XEP0352) + ++ (instancetype)indicateActiveElement +{ + return [NSXMLElement elementWithName:@"active" xmlns:XMLNS_XMPP_CLIENT_STATE_INDICATION]; +} + ++ (instancetype)indicateInactiveElement +{ + return [NSXMLElement elementWithName:@"inactive" xmlns:XMLNS_XMPP_CLIENT_STATE_INDICATION]; +} + +@end diff --git a/PNXMPPFramework.podspec b/PNXMPPFramework.podspec index 3ec68f5..60404f9 100644 --- a/PNXMPPFramework.podspec +++ b/PNXMPPFramework.podspec @@ -18,17 +18,18 @@ Pod::Spec.new do |s| # * Finally, don't worry about the indent, CocoaPods strips it! #s.description = <<-DESC DESC - s.homepage = "https://github.com//PNXMPPFramework" + s.homepage = "https://github.com/giuseppenucifora/PNXMPPFramework" # s.screenshots = "www.example.com/screenshots_1", "www.example.com/screenshots_2" s.license = 'MIT' s.author = { "Giuseppe Nucifora" => "me@giuseppenucifora.com" } - s.source = { :git => "https://github.com//PNXMPPFramework.git", :tag => s.version.to_s } + s.source = { :git => "https://github.com/giuseppenucifora/PNXMPPFramework.git", :tag => s.version.to_s } # s.social_media_url = 'https://twitter.com/' s.platform = :ios, '7.0' s.requires_arc = true s.source_files = 'Pod/Classes/**/*' + s.resources = [ '**/*.{xcdatamodel,xcdatamodeld}'] s.resource_bundles = { 'PNXMPPFramework' => ['Pod/Assets/*.png'] } @@ -36,7 +37,273 @@ Pod::Spec.new do |s| # s.public_header_files = 'Pod/Classes/**/*.h' # s.frameworks = 'UIKit', 'MapKit' # s.dependency 'AFNetworking', '~> 2.3' - s.dependency 'CocoaLumberjack' - s.dependency 'CocoaAsyncSocket' - s.ios.dependency 'KissXML' + + +s.preserve_path = 'PNXMPPFramework/module/module.modulemap' +#s.module_map = 'module/module.modulemap' + +s.subspec 'Core' do |core| +core.source_files = ['XMPPFramework.h', 'Core/**/*.{h,m}', 'Vendor/libidn/*.h', 'Authentication/**/*.{h,m}', 'Categories/**/*.{h,m}', 'Utilities/**/*.{h,m}'] +core.vendored_libraries = 'Vendor/libidn/libidn.a' +core.libraries = 'xml2', 'resolv' +core.xcconfig = { 'HEADER_SEARCH_PATHS' => '$(inherited) $(SDKROOT)/usr/include/libxml2 $(PODS_ROOT)/XMPPFramework/module $(SDKROOT)/usr/include/libresolv', 'LIBRARY_SEARCH_PATHS' => '"$(PODS_ROOT)/Vendor/libidn"', 'CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES' => 'YES', 'OTHER_LDFLAGS' => '"-lxml2"', 'ENABLE_BITCODE' => 'NO' +} +core.dependency 'CocoaLumberjack','~>1.9' +core.dependency 'CocoaAsyncSocket' +core.dependency 'KissXML' +end + +s.subspec 'Authentication' do |ss| +ss.dependency 'PNXMPPFramework/Core' +end + +s.subspec 'Categories' do |ss| +ss.dependency 'PNXMPPFramework/Core' +end + +s.subspec 'Utilities' do |ss| +ss.dependency 'PNXMPPFramework/Core' +end + +s.subspec 'BandwidthMonitor' do |ss| +ss.source_files = 'Extensions/BandwidthMonitor/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'CoreDataStorage' do |ss| +ss.source_files = ['Extensions/CoreDataStorage/**/*.{h,m}', 'Extensions/XEP-0203/NSXMLElement+XEP_0203.h'] +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +ss.framework = 'CoreData' +end + +s.subspec 'GoogleSharedStatus' do |ss| +ss.source_files = 'Extensions/GoogleSharedStatus/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'ProcessOne' do |ss| +ss.source_files = 'Extensions/ProcessOne/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'Reconnect' do |ss| +ss.source_files = 'Extensions/Reconnect/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +ss.framework = 'SystemConfiguration' +end + +s.subspec 'Roster' do |ss| +ss.source_files = 'Extensions/Roster/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.dependency 'PNXMPPFramework/CoreDataStorage' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'SystemInputActivityMonitor' do |ss| +ss.source_files = ['Extensions/SystemInputActivityMonitor/**/*.{h,m}', 'Utilities/GCDMulticastDelegate.h'] +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0009' do |ss| +ss.source_files = 'Extensions/XEP-0009/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0012' do |ss| +ss.source_files = 'Extensions/XEP-0012/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0016' do |ss| +ss.source_files = 'Extensions/XEP-0016/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0045' do |ss| +ss.source_files = 'Extensions/XEP-0045/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.dependency 'PNXMPPFramework/CoreDataStorage' +ss.dependency 'PNXMPPFramework/XEP-0203' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0054' do |ss| +ss.source_files = ['Extensions/XEP-0054/**/*.{h,m}', 'Extensions/XEP-0153/XMPPvCardAvatarModule.h', 'Extensions/XEP-0082/XMPPDateTimeProfiles.h', 'Extensions/XEP-0082/NSDate+XMPPDateTimeProfiles.h'] +ss.dependency 'PNXMPPFramework/Core' +ss.dependency 'PNXMPPFramework/Roster' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +ss.framework = 'CoreLocation' +end + +s.subspec 'XEP-0059' do |ss| +ss.source_files = 'Extensions/XEP-0059/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0060' do |ss| +ss.source_files = 'Extensions/XEP-0060/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0065' do |ss| +ss.source_files = 'Extensions/XEP-0065/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0066' do |ss| +ss.source_files = 'Extensions/XEP-0066/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0082' do |ss| +ss.source_files = 'Extensions/XEP-0082/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0085' do |ss| +ss.source_files = 'Extensions/XEP-0085/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0092' do |ss| +ss.source_files = 'Extensions/XEP-0092/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0100' do |ss| +ss.source_files = 'Extensions/XEP-0100/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0106' do |ss| +ss.source_files = 'Extensions/XEP-0106/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0115' do |ss| +ss.source_files = 'Extensions/XEP-0115/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.dependency 'PNXMPPFramework/CoreDataStorage' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0136' do |ss| +ss.source_files = 'Extensions/XEP-0136/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/CoreDataStorage' +ss.dependency 'PNXMPPFramework/XEP-0203' +ss.dependency 'PNXMPPFramework/XEP-0085' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0153' do |ss| +ss.source_files = ['Extensions/XEP-0153/**/*.{h,m}', 'Extensions/XEP-0082/NSDate+XMPPDateTimeProfiles.h'] +ss.dependency 'PNXMPPFramework/Core' +ss.dependency 'PNXMPPFramework/XEP-0054' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0172' do |ss| +ss.source_files = 'Extensions/XEP-0172/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0184' do |ss| +ss.source_files = 'Extensions/XEP-0184/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0191' do |ss| +ss.source_files = 'Extensions/XEP-0191/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0198' do |ss| +ss.source_files = 'Extensions/XEP-0198/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0199' do |ss| +ss.source_files = 'Extensions/XEP-0199/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0202' do |ss| +ss.source_files = 'Extensions/XEP-0202/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.dependency 'PNXMPPFramework/XEP-0082' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0203' do |ss| +ss.source_files = 'Extensions/XEP-0203/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.dependency 'PNXMPPFramework/XEP-0082' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0223' do |ss| +ss.source_files = 'Extensions/XEP-0223/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0224' do |ss| +ss.source_files = 'Extensions/XEP-0224/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0280' do |ss| +ss.source_files = ['Extensions/XEP-0280/**/*.{h,m}', 'Extensions/XEP-0297/NSXMLElement+XEP_0297.h'] +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0297' do |ss| +ss.source_files = ['Extensions/XEP-0297/**/*.{h,m}', 'Extensions/XEP-0203/**/*.h'] +ss.dependency 'PNXMPPFramework/Core' +ss.dependency 'PNXMPPFramework/XEP-0203' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0308' do |ss| +ss.source_files = 'Extensions/XEP-0308/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0333' do |ss| +ss.source_files = 'Extensions/XEP-0333/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end + +s.subspec 'XEP-0335' do |ss| +ss.source_files = 'Extensions/XEP-0335/**/*.{h,m}' +ss.dependency 'PNXMPPFramework/Core' +ss.prefix_header_contents = "#define HAVE_XMPP_SUBSPEC_#{name.upcase.sub('-', '_')}" +end end diff --git a/Utilities/DDList.h b/Utilities/DDList.h new file mode 100644 index 0000000..0f741fc --- /dev/null +++ b/Utilities/DDList.h @@ -0,0 +1,61 @@ +#import + +@class DDListEnumerator; + +struct DDListNode { + void * element; + struct DDListNode * prev; + struct DDListNode * next; +}; +typedef struct DDListNode DDListNode; + + +/** + * The DDList class is designed as a simple list class. + * It can store objective-c objects as well as non-objective-c pointers. + * It does not retain objective-c objects as it treats all elements as simple pointers. + * + * Example usages: + * - Storing a list of delegates, where there is a desire to not retain the individual delegates. + * - Storing a list of dispatch timers, which are not NSObjects, and cannot be stored in NSCollection classes. + * + * This class is NOT thread-safe. + * It is designed to be used within a thread-safe context (e.g. within a single dispatch_queue). +**/ +@interface DDList : NSObject +{ + DDListNode *listHead; + DDListNode *listTail; +} + +- (void)add:(void *)element; +- (void)remove:(void *)element; +- (void)removeAll:(void *)element; +- (void)removeAllElements; + +- (BOOL)contains:(void *)element; + +- (NSUInteger)count; + +/** + * The enumerators return a snapshot of the list that can be enumerated. + * The list can later be altered (elements added/removed) without affecting enumerator snapshots. +**/ +- (DDListEnumerator *)listEnumerator; +- (DDListEnumerator *)reverseListEnumerator; + +@end + +@interface DDListEnumerator : NSObject +{ + NSUInteger numElements; + NSUInteger currentElementIndex; + void **elements; +} + +- (NSUInteger)numTotal; +- (NSUInteger)numLeft; + +- (void *)nextElement; + +@end diff --git a/Utilities/DDList.m b/Utilities/DDList.m new file mode 100644 index 0000000..0ddd4ae --- /dev/null +++ b/Utilities/DDList.m @@ -0,0 +1,297 @@ +#import "DDList.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@interface DDListEnumerator (PrivateAPI) + +- (id)initWithList:(DDListNode *)list reverse:(BOOL)reverse; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation DDList + +- (id)init +{ + if ((self = [super init])) + { + listHead = NULL; + listTail = NULL; + } + return self; +} + +- (void)add:(void *)element +{ + if(element == NULL) return; + + DDListNode *node = malloc(sizeof(DDListNode)); + + node->element = element; + + // Remember: The list is a linked list of DDListNode objects. + // Each node object is allocated and placed in the list. + // It is not deallocated until it is later removed from the linked list. + + if (listTail == NULL) + { + node->next = NULL; + node->prev = NULL; + } + else + { + node->next = NULL; + node->prev = listTail; + node->prev->next = node; + } + + listTail = node; + + if (listHead == NULL) + listHead = node; +} + +- (void)remove:(void *)element allInstances:(BOOL)allInstances +{ + if(element == NULL) return; + + DDListNode *node = listHead; + while (node != NULL) + { + DDListNode *nextNode = node->next; + if (element == node->element) + { + // Remove the node from the list. + // This is done by editing the pointers of the node's neighbors to skip it. + // + // In other words: + // node->prev->next = node->next + // node->next->prev = node->prev + // + // We also want to properly update our list pointer, + // which always points to the "first" element in the list. (Most recently added.) + + if (node->prev != NULL) + node->prev->next = node->next; + else + listHead = node->next; + + if (node->next != NULL) + node->next->prev = node->prev; + else + listTail = node->prev; + + free(node); + + if (!allInstances) break; + } + node = nextNode; + } +} + +- (void)remove:(void *)element +{ + [self remove:element allInstances:NO]; +} + +- (void)removeAll:(void *)element +{ + [self remove:element allInstances:YES]; +} + +- (void)removeAllElements +{ + DDListNode *node = listHead; + while (node != NULL) + { + DDListNode *next = node->next; + + free(node); + node = next; + } + + listHead = NULL; + listTail = NULL; +} + +- (BOOL)contains:(void *)element +{ + DDListNode *node; + for (node = listHead; node != NULL; node = node->next) + { + if (node->element == element) + { + return YES; + } + } + + return NO; +} + +- (NSUInteger)count +{ + NSUInteger count = 0; + + DDListNode *node; + for (node = listHead; node != NULL; node = node->next) + { + count++; + } + + return count; +} + +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state + objects:(id __unsafe_unretained [])buffer + count:(NSUInteger)len +{ + DDListNode *currentNode; + + if (state->extra[0] == 1) + return 0; + + if (state->state == 0) + currentNode = listHead; + else + currentNode = (DDListNode *)state->state; + + NSUInteger batchCount = 0; + while (currentNode != NULL && batchCount < len) + { + buffer[batchCount] = (__bridge id)currentNode->element; + currentNode = currentNode->next; + batchCount++; + } + + state->state = (unsigned long)currentNode; + state->itemsPtr = buffer; + state->mutationsPtr = (__bridge void *)self; + + if (currentNode == NULL) + state->extra[0] = 1; + else + state->extra[0] = 0; + + return batchCount; +} + +- (DDListEnumerator *)listEnumerator +{ + return [[DDListEnumerator alloc] initWithList:listHead reverse:NO]; +} + +- (DDListEnumerator *)reverseListEnumerator +{ + return [[DDListEnumerator alloc] initWithList:listTail reverse:YES]; +} + +- (void)dealloc +{ + [self removeAllElements]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation DDListEnumerator + +- (id)initWithList:(DDListNode *)list reverse:(BOOL)reverse +{ + if ((self = [super init])) + { + numElements = 0; + currentElementIndex = 0; + + // First get a count of the number of elements in the given list. + + if (reverse) + { + for (DDListNode *node = list; node != NULL; node = node->prev) + { + numElements++; + } + } + else + { + for (DDListNode *node = list; node != NULL; node = node->next) + { + numElements++; + } + + } + + // Now copy the list into a C array. + + if (numElements > 0) + { + elements = malloc(numElements * sizeof(void *)); + + DDListNode *node = list; + + if (reverse) + { + for (NSUInteger i = 0; i < numElements; i++) + { + elements[i] = node->element; + node = node->prev; + } + } + else + { + for (NSUInteger i = 0; i < numElements; i++) + { + elements[i] = node->element; + node = node->next; + } + } + } + } + return self; +} + +- (NSUInteger)numTotal +{ + return numElements; +} + +- (NSUInteger)numLeft +{ + if (currentElementIndex < numElements) + return numElements - currentElementIndex; + else + return 0; +} + +- (void *)nextElement +{ + if (currentElementIndex < numElements) + { + void *element = elements[currentElementIndex]; + currentElementIndex++; + + return element; + } + else + { + return NULL; + } +} + +- (void)dealloc +{ + if (elements) + { + free(elements); + } +} + +@end diff --git a/Utilities/GCDMulticastDelegate.h b/Utilities/GCDMulticastDelegate.h new file mode 100644 index 0000000..81976f6 --- /dev/null +++ b/Utilities/GCDMulticastDelegate.h @@ -0,0 +1,56 @@ +#import + +@class GCDMulticastDelegateEnumerator; + +/** + * This class provides multicast delegate functionality. That is: + * - it provides a means for managing a list of delegates + * - any method invocations to an instance of this class are automatically forwarded to all delegates + * + * For example: + * + * // Make this method call on every added delegate (there may be several) + * [multicastDelegate cog:self didFindThing:thing]; + * + * This allows multiple delegates to be added to an xmpp stream or any xmpp module, + * which in turn makes development easier as there can be proper separation of logically different code sections. + * + * In addition, this makes module development easier, + * as multiple delegates can usually be handled in a manner similar to the traditional single delegate paradigm. + * + * This class also provides proper support for GCD queues. + * So each delegate specifies which queue they would like their delegate invocations to be dispatched onto. + * + * All delegate dispatching is done asynchronously (which is a critically important architectural design). +**/ + +@interface GCDMulticastDelegate : NSObject + +- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; +- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; +- (void)removeDelegate:(id)delegate; + +- (void)removeAllDelegates; + +- (NSUInteger)count; +- (NSUInteger)countOfClass:(Class)aClass; +- (NSUInteger)countForSelector:(SEL)aSelector; + +- (BOOL)hasDelegateThatRespondsToSelector:(SEL)aSelector; + +- (GCDMulticastDelegateEnumerator *)delegateEnumerator; + +@end + + +@interface GCDMulticastDelegateEnumerator : NSObject + +- (NSUInteger)count; +- (NSUInteger)countOfClass:(Class)aClass; +- (NSUInteger)countForSelector:(SEL)aSelector; + +- (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr; +- (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr ofClass:(Class)aClass; +- (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr forSelector:(SEL)aSelector; + +@end diff --git a/Utilities/GCDMulticastDelegate.m b/Utilities/GCDMulticastDelegate.m new file mode 100644 index 0000000..1785c27 --- /dev/null +++ b/Utilities/GCDMulticastDelegate.m @@ -0,0 +1,654 @@ +#import "GCDMulticastDelegate.h" +#import + +#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE +#import +#endif + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +/** + * How does this class work? + * + * In theory, this class is very straight-forward. + * It provides a way for multiple delegates to be called, each on its own delegate queue. + * + * In other words, any delegate method call to this class + * will get forwarded (dispatch_async'd) to each added delegate. + * + * Important note concerning thread-safety: + * + * This class is designed to be used from within a single dispatch queue. + * In other words, it is NOT thread-safe, and should only be used from within the external dedicated dispatch_queue. +**/ + +@interface GCDMulticastDelegateNode : NSObject { +@private + + #if __has_feature(objc_arc_weak) + __weak id delegate; + #if !TARGET_OS_IPHONE + __unsafe_unretained id unsafeDelegate; // Some classes don't support weak references yet (e.g. NSWindowController) + #endif + #else + __unsafe_unretained id delegate; + #endif + + dispatch_queue_t delegateQueue; +} + +- (id)initWithDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; + +#if __has_feature(objc_arc_weak) +@property (/* atomic */ readwrite, weak) id delegate; +#if !TARGET_OS_IPHONE +@property (/* atomic */ readwrite, unsafe_unretained) id unsafeDelegate; +#endif +#else +@property (/* atomic */ readwrite, unsafe_unretained) id delegate; +#endif + +@property (nonatomic, readonly) dispatch_queue_t delegateQueue; + +@end + + +@interface GCDMulticastDelegate () +{ + NSMutableArray *delegateNodes; +} + +- (NSInvocation *)duplicateInvocation:(NSInvocation *)origInvocation; + +@end + + +@interface GCDMulticastDelegateEnumerator () +{ + NSUInteger numNodes; + NSUInteger currentNodeIndex; + NSArray *delegateNodes; +} + +- (id)initFromDelegateNodes:(NSMutableArray *)inDelegateNodes; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation GCDMulticastDelegate + +- (id)init +{ + if ((self = [super init])) + { + delegateNodes = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue +{ + if (delegate == nil) return; + if (delegateQueue == NULL) return; + + GCDMulticastDelegateNode *node = + [[GCDMulticastDelegateNode alloc] initWithDelegate:delegate delegateQueue:delegateQueue]; + + [delegateNodes addObject:node]; +} + +- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue +{ + if (delegate == nil) return; + + NSUInteger i; + for (i = [delegateNodes count]; i > 0; i--) + { + GCDMulticastDelegateNode *node = delegateNodes[i - 1]; + + id nodeDelegate = node.delegate; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if (delegate == nodeDelegate) + { + if ((delegateQueue == NULL) || (delegateQueue == node.delegateQueue)) + { + // Recall that this node may be retained by a GCDMulticastDelegateEnumerator. + // The enumerator is a thread-safe snapshot of the delegate list at the moment it was created. + // To properly remove this node from list, and from the list(s) of any enumerators, + // we nullify the delegate via the atomic property. + // + // However, the delegateQueue is not modified. + // The thread-safety is hinged on the atomic delegate property. + // The delegateQueue is expected to properly exist until the node is deallocated. + + node.delegate = nil; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + node.unsafeDelegate = nil; + #endif + + [delegateNodes removeObjectAtIndex:(i-1)]; + } + } + } +} + +- (void)removeDelegate:(id)delegate +{ + [self removeDelegate:delegate delegateQueue:NULL]; +} + +- (void)removeAllDelegates +{ + for (GCDMulticastDelegateNode *node in delegateNodes) + { + node.delegate = nil; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + node.unsafeDelegate = nil; + #endif + } + + [delegateNodes removeAllObjects]; +} + +- (NSUInteger)count +{ + return [delegateNodes count]; +} + +- (NSUInteger)countOfClass:(Class)aClass +{ + NSUInteger count = 0; + + for (GCDMulticastDelegateNode *node in delegateNodes) + { + id nodeDelegate = node.delegate; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if ([nodeDelegate isKindOfClass:aClass]) + { + count++; + } + } + + return count; +} + +- (NSUInteger)countForSelector:(SEL)aSelector +{ + NSUInteger count = 0; + + for (GCDMulticastDelegateNode *node in delegateNodes) + { + id nodeDelegate = node.delegate; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if ([nodeDelegate respondsToSelector:aSelector]) + { + count++; + } + } + + return count; +} + +- (BOOL)hasDelegateThatRespondsToSelector:(SEL)aSelector +{ + for (GCDMulticastDelegateNode *node in delegateNodes) + { + id nodeDelegate = node.delegate; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if ([nodeDelegate respondsToSelector:aSelector]) + { + return YES; + } + } + + return NO; +} + +- (GCDMulticastDelegateEnumerator *)delegateEnumerator +{ + return [[GCDMulticastDelegateEnumerator alloc] initFromDelegateNodes:delegateNodes]; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector +{ + for (GCDMulticastDelegateNode *node in delegateNodes) + { + id nodeDelegate = node.delegate; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + NSMethodSignature *result = [nodeDelegate methodSignatureForSelector:aSelector]; + + if (result != nil) + { + return result; + } + } + + // This causes a crash... + // return [super methodSignatureForSelector:aSelector]; + + // This also causes a crash... + // return nil; + + return [[self class] instanceMethodSignatureForSelector:@selector(doNothing)]; +} + +- (void)forwardInvocation:(NSInvocation *)origInvocation +{ + SEL selector = [origInvocation selector]; + BOOL foundNilDelegate = NO; + + for (GCDMulticastDelegateNode *node in delegateNodes) + { + id nodeDelegate = node.delegate; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if ([nodeDelegate respondsToSelector:selector]) + { + // All delegates MUST be invoked ASYNCHRONOUSLY. + + NSInvocation *dupInvocation = [self duplicateInvocation:origInvocation]; + + dispatch_async(node.delegateQueue, ^{ @autoreleasepool { + + [dupInvocation invokeWithTarget:nodeDelegate]; + + }}); + } + else if (nodeDelegate == nil) + { + foundNilDelegate = YES; + } + } + + if (foundNilDelegate) + { + // At lease one weak delegate reference disappeared. + // Remove nil delegate nodes from the list. + // + // This is expected to happen very infrequently. + // This is why we handle it separately (as it requires allocating an indexSet). + + NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] init]; + + NSUInteger i = 0; + for (GCDMulticastDelegateNode *node in delegateNodes) + { + id nodeDelegate = node.delegate; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if (nodeDelegate == nil) + { + [indexSet addIndex:i]; + } + i++; + } + + [delegateNodes removeObjectsAtIndexes:indexSet]; + } +} + +- (void)doesNotRecognizeSelector:(SEL)aSelector +{ + // Prevent NSInvalidArgumentException +} + +- (void)doNothing {} + +- (void)dealloc +{ + [self removeAllDelegates]; +} + +- (NSInvocation *)duplicateInvocation:(NSInvocation *)origInvocation +{ + NSMethodSignature *methodSignature = [origInvocation methodSignature]; + + NSInvocation *dupInvocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [dupInvocation setSelector:[origInvocation selector]]; + + NSUInteger i, count = [methodSignature numberOfArguments]; + for (i = 2; i < count; i++) + { + const char *type = [methodSignature getArgumentTypeAtIndex:i]; + + if (*type == *@encode(BOOL)) + { + BOOL value; + [origInvocation getArgument:&value atIndex:i]; + [dupInvocation setArgument:&value atIndex:i]; + } + else if (*type == *@encode(char) || *type == *@encode(unsigned char)) + { + char value; + [origInvocation getArgument:&value atIndex:i]; + [dupInvocation setArgument:&value atIndex:i]; + } + else if (*type == *@encode(short) || *type == *@encode(unsigned short)) + { + short value; + [origInvocation getArgument:&value atIndex:i]; + [dupInvocation setArgument:&value atIndex:i]; + } + else if (*type == *@encode(int) || *type == *@encode(unsigned int)) + { + int value; + [origInvocation getArgument:&value atIndex:i]; + [dupInvocation setArgument:&value atIndex:i]; + } + else if (*type == *@encode(long) || *type == *@encode(unsigned long)) + { + long value; + [origInvocation getArgument:&value atIndex:i]; + [dupInvocation setArgument:&value atIndex:i]; + } + else if (*type == *@encode(long long) || *type == *@encode(unsigned long long)) + { + long long value; + [origInvocation getArgument:&value atIndex:i]; + [dupInvocation setArgument:&value atIndex:i]; + } + else if (*type == *@encode(double)) + { + double value; + [origInvocation getArgument:&value atIndex:i]; + [dupInvocation setArgument:&value atIndex:i]; + } + else if (*type == *@encode(float)) + { + float value; + [origInvocation getArgument:&value atIndex:i]; + [dupInvocation setArgument:&value atIndex:i]; + } + else if (*type == '@') + { + void *value; + [origInvocation getArgument:&value atIndex:i]; + [dupInvocation setArgument:&value atIndex:i]; + } + else if (*type == '^') + { + void *block; + [origInvocation getArgument:&block atIndex:i]; + [dupInvocation setArgument:&block atIndex:i]; + } + else + { + NSString *selectorStr = NSStringFromSelector([origInvocation selector]); + + NSString *format = @"Argument %lu to method %@ - Type(%c) not supported"; + NSString *reason = [NSString stringWithFormat:format, (unsigned long)(i - 2), selectorStr, *type]; + + [[NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil] raise]; + } + } + + [dupInvocation retainArguments]; + + return dupInvocation; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation GCDMulticastDelegateNode + +@synthesize delegate; // atomic +#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE +@synthesize unsafeDelegate; // atomic +#endif +@synthesize delegateQueue; // non-atomic + +#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE +static BOOL SupportsWeakReferences(id delegate) +{ + // From Apple's documentation: + // + // > Which classes don’t support weak references? + // > + // > You cannot currently create weak references to instances of the following classes: + // > + // > NSATSTypesetter, NSColorSpace, NSFont, NSFontManager, NSFontPanel, NSImage, NSMenuView, + // > NSParagraphStyle, NSSimpleHorizontalTypesetter, NSTableCellView, NSTextView, NSViewController, + // > NSWindow, and NSWindowController. + // > + // > In addition, in OS X no classes in the AV Foundation framework support weak references. + // + // NSMenuView is deprecated (and not available to 64-bit applications). + // NSSimpleHorizontalTypesetter is an internal class. + + if ([delegate isKindOfClass:[NSATSTypesetter class]]) return NO; + if ([delegate isKindOfClass:[NSColorSpace class]]) return NO; + if ([delegate isKindOfClass:[NSFont class]]) return NO; + if ([delegate isKindOfClass:[NSFontManager class]]) return NO; + if ([delegate isKindOfClass:[NSFontPanel class]]) return NO; + if ([delegate isKindOfClass:[NSImage class]]) return NO; + if ([delegate isKindOfClass:[NSParagraphStyle class]]) return NO; + if ([delegate isKindOfClass:[NSTableCellView class]]) return NO; + if ([delegate isKindOfClass:[NSTextView class]]) return NO; + if ([delegate isKindOfClass:[NSViewController class]]) return NO; + if ([delegate isKindOfClass:[NSWindow class]]) return NO; + if ([delegate isKindOfClass:[NSWindowController class]]) return NO; + + return YES; +} +#endif + +- (id)initWithDelegate:(id)inDelegate delegateQueue:(dispatch_queue_t)inDelegateQueue +{ + if ((self = [super init])) + { + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + { + if (SupportsWeakReferences(inDelegate)) + { + delegate = inDelegate; + delegateQueue = inDelegateQueue; + } + else + { + delegate = [NSNull null]; + + unsafeDelegate = inDelegate; + delegateQueue = inDelegateQueue; + } + } + #else + { + delegate = inDelegate; + delegateQueue = inDelegateQueue; + } + #endif + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) + dispatch_retain(delegateQueue); + #endif + } + return self; +} + +- (void)dealloc +{ + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) + dispatch_release(delegateQueue); + #endif +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation GCDMulticastDelegateEnumerator + +- (id)initFromDelegateNodes:(NSMutableArray *)inDelegateNodes +{ + if ((self = [super init])) + { + delegateNodes = [inDelegateNodes copy]; + + numNodes = [delegateNodes count]; + currentNodeIndex = 0; + } + return self; +} + +- (NSUInteger)count +{ + return numNodes; +} + +- (NSUInteger)countOfClass:(Class)aClass +{ + NSUInteger count = 0; + + for (GCDMulticastDelegateNode *node in delegateNodes) + { + id nodeDelegate = node.delegate; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if ([nodeDelegate isKindOfClass:aClass]) + { + count++; + } + } + + return count; +} + +- (NSUInteger)countForSelector:(SEL)aSelector +{ + NSUInteger count = 0; + + for (GCDMulticastDelegateNode *node in delegateNodes) + { + id nodeDelegate = node.delegate; + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if ([nodeDelegate respondsToSelector:aSelector]) + { + count++; + } + } + + return count; +} + +- (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr +{ + while (currentNodeIndex < numNodes) + { + GCDMulticastDelegateNode *node = delegateNodes[currentNodeIndex]; + currentNodeIndex++; + + id nodeDelegate = node.delegate; // snapshot atomic property + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if (nodeDelegate) + { + if (delPtr) *delPtr = nodeDelegate; + if (dqPtr) *dqPtr = node.delegateQueue; + + return YES; + } + } + + return NO; +} + +- (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr ofClass:(Class)aClass +{ + while (currentNodeIndex < numNodes) + { + GCDMulticastDelegateNode *node = delegateNodes[currentNodeIndex]; + currentNodeIndex++; + + id nodeDelegate = node.delegate; // snapshot atomic property + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if ([nodeDelegate isKindOfClass:aClass]) + { + if (delPtr) *delPtr = nodeDelegate; + if (dqPtr) *dqPtr = node.delegateQueue; + + return YES; + } + } + + return NO; +} + +- (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr forSelector:(SEL)aSelector +{ + while (currentNodeIndex < numNodes) + { + GCDMulticastDelegateNode *node = delegateNodes[currentNodeIndex]; + currentNodeIndex++; + + id nodeDelegate = node.delegate; // snapshot atomic property + #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE + if (nodeDelegate == [NSNull null]) + nodeDelegate = node.unsafeDelegate; + #endif + + if ([nodeDelegate respondsToSelector:aSelector]) + { + if (delPtr) *delPtr = nodeDelegate; + if (dqPtr) *dqPtr = node.delegateQueue; + + return YES; + } + } + + return NO; +} + +@end diff --git a/Utilities/RFImageToDataTransformer.h b/Utilities/RFImageToDataTransformer.h new file mode 100644 index 0000000..579ab4b --- /dev/null +++ b/Utilities/RFImageToDataTransformer.h @@ -0,0 +1,19 @@ + +/* + File: RFImageToDataTransformer.h + Abstract: A value transformer, which transforms a UIImage or NSImage object into an NSData object. + + Based on Apple's UIImageToDataTransformer + + Copyright (C) 2010 Apple Inc. All Rights Reserved. + Copyright (C) 2011 RF.com All Rights Reserved. + */ + +#import + + +@interface RFImageToDataTransformer : NSValueTransformer { + +} + +@end diff --git a/Utilities/RFImageToDataTransformer.m b/Utilities/RFImageToDataTransformer.m new file mode 100644 index 0000000..a7df718 --- /dev/null +++ b/Utilities/RFImageToDataTransformer.m @@ -0,0 +1,48 @@ + +/* + File: RFImageToDataTransformer.m + Abstract: A value transformer, which transforms a UIImage or NSImage object into an NSData object. + + Based on Apple's UIImageToDataTransformer + + Copyright (C) 2010 Apple Inc. All Rights Reserved. + Copyright (C) 2011 RF.com All Rights Reserved. + */ + +#import "RFImageToDataTransformer.h" + +#if TARGET_OS_IPHONE +#import +#else + +#endif + + +@implementation RFImageToDataTransformer + ++ (BOOL)allowsReverseTransformation { + return YES; +} + ++ (Class)transformedValueClass { + return [NSData class]; +} + +- (id)transformedValue:(id)value { +#if TARGET_OS_IPHONE + return UIImagePNGRepresentation(value); +#else + return [(NSImage *)value TIFFRepresentation]; +#endif +} + +- (id)reverseTransformedValue:(id)value { +#if TARGET_OS_IPHONE + return [[UIImage alloc] initWithData:value]; +#else + return [[NSImage alloc] initWithData:value]; +#endif +} + +@end + diff --git a/Utilities/XMPPIDTracker.h b/Utilities/XMPPIDTracker.h new file mode 100644 index 0000000..ea5b58f --- /dev/null +++ b/Utilities/XMPPIDTracker.h @@ -0,0 +1,210 @@ +#import + +@protocol XMPPTrackingInfo; + +@class XMPPElement; + +@class XMPPStream; + +extern const NSTimeInterval XMPPIDTrackerTimeoutNone; + +/** + * A common operation in XMPP is to send some kind of request with a unique id, + * and wait for the response to come back. + * The most common example is sending an IQ of type='get' with a unique id, and then awaiting the response. + * + * In order to properly handle the response, the id must be stored. + * If there are multiple queries going out and/or different kinds of queries, + * then information about the appropriate handling of the response must also be stored. + * This may be accomplished by storing the appropriate selector, or perhaps a block handler. + * Additionally one may need to setup timeouts and handle those properly as well. + * + * This class provides the scaffolding to simplify the tasks associated with this common operation. + * Essentially, it provides the following: + * - a dictionary where the unique id is the key, and the needed tracking info is the object + * - an optional timer to fire upon a timeout + * + * The class is designed to be flexible. + * You can provide a target/selector or a block handler to be invoked. + * Additionally, you can use the basic tracking info, or you can extend it to suit your needs. + * + * It is best illustrated with a few examples. + * + * ---- EXAMPLE 1 - SIMPLE TRACKING WITH TARGET / SELECTOR ---- + * + * XMPPIQ *iq = ... + * [iqTracker addID:[iq elementID] target:self selector:@selector(processBookQuery:withInfo:) timeout:15.0]; + * + * - (void)processBookQueury:(XMPPIQ *)iq withInfo:(id ) = ^(XMPPIQ *iq, id info) { + * ... + * }; + * [iqTracker addID:[iq elementID] block:blockHandler timeout:15.0]; + * + * // Same xmppStream:didReceiveIQ: as example 1 + * + * ---- EXAMPLE 3 - ADVANCED TRACKING ---- + * + * @interface PingTrackingInfo : XMPPBasicTrackingInfo + * ... + * @end + * + * XMPPIQ *ping = ... + * PingTrackingInfo *pingInfo = ... + * + * [iqTracker addID:[ping elementID] trackingInfo:pingInfo]; + * + * - (void)handlePong:(XMPPIQ *)iq withInfo:(PingTrackingInfo *)info { + * ... + * } + * + * // Same xmppStream:didReceiveIQ: as example 1 + * + * + * ---- Validating Responses ---- + * + * XMPPIDTracker can also be used to validate that the response was from the expected jid. + * To do this you need to initalize XMPPIDTracker with the stream where the request/response is going to be tracked. + * + * xmppIDTracker = [[XMPPIDTracker alloc] initWithStream:stream dispatchQueue:queue]; + * + * You also need to supply the element (not just the ID) to the add an invoke methods. + * + * ---- EXAMPLE 1 - SIMPLE TRACKING WITH TARGET / SELECTOR AND VALIDATION ---- + * + * XMPPIQ *iq = ... + * [iqTracker addElement:iq target:self selector:@selector(processBookQuery:withInfo:) timeout:15.0]; + * + * - (void)processBookQueury:(XMPPIQ *)iq withInfo:(id info))block + timeout:(NSTimeInterval)timeout; + +- (void)addElement:(XMPPElement *)element + block:(void (^)(id obj, id info))block + timeout:(NSTimeInterval)timeout; + +- (void)addID:(NSString *)elementID trackingInfo:(id )trackingInfo; + +- (void)addElement:(XMPPElement *)element trackingInfo:(id )trackingInfo; + +- (BOOL)invokeForID:(NSString *)elementID withObject:(id)obj; + +- (BOOL)invokeForElement:(XMPPElement *)element withObject:(id)obj; + +- (NSUInteger)numberOfIDs; + +- (void)removeID:(NSString *)elementID; +- (void)removeAllIDs; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPTrackingInfo + +@property (nonatomic, readonly) NSTimeInterval timeout; + +@property (nonatomic, readwrite, copy) NSString *elementID; + +@property (nonatomic, readwrite, copy) XMPPElement *element; + +- (void)createTimerWithDispatchQueue:(dispatch_queue_t)queue; +- (void)cancelTimer; + +- (void)invokeWithObject:(id)obj; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPBasicTrackingInfo : NSObject +{ + id target; + SEL selector; + + void (^block)(id obj, id info); + + NSTimeInterval timeout; + + NSString *elementID; + XMPPElement *element; + dispatch_source_t timer; +} + +- (id)initWithTarget:(id)target selector:(SEL)selector timeout:(NSTimeInterval)timeout; +- (id)initWithBlock:(void (^)(id obj, id info))block timeout:(NSTimeInterval)timeout; + +@property (nonatomic, readonly) NSTimeInterval timeout; + +@property (nonatomic, readwrite, copy) NSString *elementID; + +@property (nonatomic, readwrite, copy) XMPPElement *element; + +- (void)createTimerWithDispatchQueue:(dispatch_queue_t)queue; +- (void)cancelTimer; + +- (void)invokeWithObject:(id)obj; + +@end diff --git a/Utilities/XMPPIDTracker.m b/Utilities/XMPPIDTracker.m new file mode 100644 index 0000000..2bd62db --- /dev/null +++ b/Utilities/XMPPIDTracker.m @@ -0,0 +1,347 @@ +#import "XMPPIDTracker.h" +#import "XMPP.h" +#import "XMPPLogging.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG +static const int xmppLogLevel = XMPP_LOG_LEVEL_VERBOSE; // | XMPP_LOG_FLAG_TRACE; +#else +static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +#define AssertProperQueue() NSAssert(dispatch_get_specific(queueTag), @"Invoked on incorrect queue") + +const NSTimeInterval XMPPIDTrackerTimeoutNone = -1; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPIDTracker () +{ + void *queueTag; +} + +@end + +@implementation XMPPIDTracker + +- (id)init +{ + // You must use initWithDispatchQueue or initWithStream:dispatchQueue: + return nil; +} + +- (id)initWithDispatchQueue:(dispatch_queue_t)aQueue +{ + return [self initWithStream:nil dispatchQueue:aQueue]; +} + +- (id)initWithStream:(XMPPStream *)stream dispatchQueue:(dispatch_queue_t)aQueue +{ + NSParameterAssert(aQueue != NULL); + + if ((self = [super init])) + { + xmppStream = stream; + + queue = aQueue; + + queueTag = &queueTag; + dispatch_queue_set_specific(queue, queueTag, queueTag, NULL); + +#if !OS_OBJECT_USE_OBJC + dispatch_retain(queue); +#endif + + dict = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (void)dealloc +{ + // We don't call [self removeAllIDs] because dealloc might not be invoked on queue + + for (id info in [dict objectEnumerator]) + { + [info cancelTimer]; + } + [dict removeAllObjects]; + + #if !OS_OBJECT_USE_OBJC + dispatch_release(queue); + #endif +} + +- (void)addID:(NSString *)elementID target:(id)target selector:(SEL)selector timeout:(NSTimeInterval)timeout +{ + AssertProperQueue(); + + XMPPBasicTrackingInfo *trackingInfo; + trackingInfo = [[XMPPBasicTrackingInfo alloc] initWithTarget:target selector:selector timeout:timeout]; + + [self addID:elementID trackingInfo:trackingInfo]; +} + +- (void)addElement:(XMPPElement *)element target:(id)target selector:(SEL)selector timeout:(NSTimeInterval)timeout +{ + AssertProperQueue(); + + XMPPBasicTrackingInfo *trackingInfo; + trackingInfo = [[XMPPBasicTrackingInfo alloc] initWithTarget:target selector:selector timeout:timeout]; + + [self addElement:element trackingInfo:trackingInfo]; +} + +- (void)addID:(NSString *)elementID + block:(void (^)(id obj, id info))block + timeout:(NSTimeInterval)timeout +{ + AssertProperQueue(); + + XMPPBasicTrackingInfo *trackingInfo; + trackingInfo = [[XMPPBasicTrackingInfo alloc] initWithBlock:block timeout:timeout]; + + [self addID:elementID trackingInfo:trackingInfo]; +} + + +- (void)addElement:(XMPPElement *)element + block:(void (^)(id obj, id info))block + timeout:(NSTimeInterval)timeout +{ + AssertProperQueue(); + + XMPPBasicTrackingInfo *trackingInfo; + trackingInfo = [[XMPPBasicTrackingInfo alloc] initWithBlock:block timeout:timeout]; + + [self addElement:element trackingInfo:trackingInfo]; +} + +- (void)addID:(NSString *)elementID trackingInfo:(id )trackingInfo +{ + AssertProperQueue(); + + dict[elementID] = trackingInfo; + + [trackingInfo setElementID:elementID]; + [trackingInfo createTimerWithDispatchQueue:queue]; +} + +- (void)addElement:(XMPPElement *)element trackingInfo:(id )trackingInfo +{ + AssertProperQueue(); + + if([[element elementID] length] == 0) return; + + dict[[element elementID]] = trackingInfo; + + [trackingInfo setElementID:[element elementID]]; + [trackingInfo setElement:element]; + [trackingInfo createTimerWithDispatchQueue:queue]; +} + +- (BOOL)invokeForID:(NSString *)elementID withObject:(id)obj +{ + AssertProperQueue(); + + if([elementID length] == 0) return NO; + + id info = dict[elementID]; + + if (info) + { + [info invokeWithObject:obj]; + [info cancelTimer]; + [dict removeObjectForKey:elementID]; + + return YES; + } + + return NO; +} + +- (BOOL)invokeForElement:(XMPPElement *)element withObject:(id)obj +{ + AssertProperQueue(); + + NSString *elementID = [element elementID]; + + if ([elementID length] == 0) return NO; + + id info = dict[elementID]; + if(info) + { + BOOL valid = YES; + + if(xmppStream && [element isKindOfClass:[XMPPIQ class]] && [[info element] isKindOfClass:[XMPPIQ class]]) + { + XMPPIQ *iq = (XMPPIQ *)element; + + if([iq isResultIQ] || [iq isErrorIQ]) + { + valid = [xmppStream isValidResponseElement:iq forRequestElement:[info element]]; + } + } + + if(!valid) + { + XMPPLogError(@"%s: Element with ID %@ cannot be validated.", __FILE__ , [element elementID]); + } + + if (valid) + { + [info invokeWithObject:obj]; + [info cancelTimer]; + [dict removeObjectForKey:[element elementID]]; + + return YES; + } + } + + return NO; +} + +- (NSUInteger)numberOfIDs +{ + AssertProperQueue(); + + return [[dict allKeys] count]; +} + +- (void)removeID:(NSString *)elementID +{ + AssertProperQueue(); + + id info = dict[elementID]; + if (info) + { + [info cancelTimer]; + [dict removeObjectForKey:elementID]; + } +} + +- (void)removeAllIDs +{ + AssertProperQueue(); + + for (id info in [dict objectEnumerator]) + { + [info cancelTimer]; + } + [dict removeAllObjects]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPBasicTrackingInfo + +@synthesize timeout; +@synthesize elementID; +@synthesize element; + +- (id)init +{ + // Use initWithTarget:selector:timeout: or initWithBlock:timeout: + + return nil; +} + +- (id)initWithTarget:(id)aTarget selector:(SEL)aSelector timeout:(NSTimeInterval)aTimeout +{ + if(target || selector) + { + NSParameterAssert(aTarget); + NSParameterAssert(aSelector); + } + + if ((self = [super init])) + { + target = aTarget; + selector = aSelector; + timeout = aTimeout; + } + return self; +} + +- (id)initWithBlock:(void (^)(id obj, id info))aBlock timeout:(NSTimeInterval)aTimeout +{ + NSParameterAssert(aBlock); + + if ((self = [super init])) + { + block = [aBlock copy]; + timeout = aTimeout; + } + return self; +} + +- (void)dealloc +{ + [self cancelTimer]; + + target = nil; + selector = NULL; +} + +- (void)createTimerWithDispatchQueue:(dispatch_queue_t)queue +{ + NSAssert(queue != NULL, @"Method invoked with NULL queue"); + NSAssert(timer == NULL, @"Method invoked multiple times"); + + if (timeout > 0.0) + { + timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); + + dispatch_source_set_event_handler(timer, ^{ @autoreleasepool { + + [self invokeWithObject:nil]; + [self cancelTimer]; + + }}); + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(timer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(timer); + } +} + +- (void)cancelTimer +{ + if (timer) + { + dispatch_source_cancel(timer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(timer); + #endif + timer = NULL; + } +} + +- (void)invokeWithObject:(id)obj +{ + if (block) + { + block(obj, self); + } + else if(target && selector) + { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [target performSelector:selector withObject:obj withObject:self]; + #pragma clang diagnostic pop + } +} + +@end diff --git a/Utilities/XMPPSRVResolver.h b/Utilities/XMPPSRVResolver.h new file mode 100644 index 0000000..571e696 --- /dev/null +++ b/Utilities/XMPPSRVResolver.h @@ -0,0 +1,86 @@ +// +// XMPPSRVResolver.h +// +// Originally created by Eric Chamberlain on 6/15/10. +// Based on SRVResolver by Apple, Inc. +// + +#import +#include + +extern NSString *const XMPPSRVResolverErrorDomain; + + +@interface XMPPSRVResolver : NSObject +{ + __unsafe_unretained id delegate; + dispatch_queue_t delegateQueue; + + dispatch_queue_t resolverQueue; + void *resolverQueueTag; + + __strong NSString *srvName; + NSTimeInterval timeout; + + BOOL resolveInProgress; + + NSMutableArray *results; + DNSServiceRef sdRef; + + int sdFd; + dispatch_source_t sdReadSource; + dispatch_source_t timeoutTimer; +} + +/** + * The delegate & delegateQueue are mandatory. + * The resolverQueue is optional. If NULL, it will automatically create it's own internal queue. +**/ +- (id)initWithdDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq resolverQueue:(dispatch_queue_t)rq; + +@property (strong, readonly) NSString *srvName; +@property (readonly) NSTimeInterval timeout; + +- (void)startWithSRVName:(NSString *)aSRVName timeout:(NSTimeInterval)aTimeout; +- (void)stop; + ++ (NSString *)srvNameFromXMPPDomain:(NSString *)xmppDomain; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol XMPPSRVResolverDelegate + +- (void)xmppSRVResolver:(XMPPSRVResolver *)sender didResolveRecords:(NSArray *)records; +- (void)xmppSRVResolver:(XMPPSRVResolver *)sender didNotResolveDueToError:(NSError *)error; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPSRVRecord : NSObject +{ + UInt16 priority; + UInt16 weight; + UInt16 port; + NSString *target; + + NSUInteger sum; + NSUInteger srvResultsIndex; +} + ++ (XMPPSRVRecord *)recordWithPriority:(UInt16)priority weight:(UInt16)weight port:(UInt16)port target:(NSString *)target; + +- (id)initWithPriority:(UInt16)priority weight:(UInt16)weight port:(UInt16)port target:(NSString *)target; + +@property (nonatomic, readonly) UInt16 priority; +@property (nonatomic, readonly) UInt16 weight; +@property (nonatomic, readonly) UInt16 port; +@property (nonatomic, readonly) NSString *target; + +@end diff --git a/Utilities/XMPPSRVResolver.m b/Utilities/XMPPSRVResolver.m new file mode 100644 index 0000000..7a4c543 --- /dev/null +++ b/Utilities/XMPPSRVResolver.m @@ -0,0 +1,687 @@ +// +// XMPPSRVResolver.m +// +// Originally created by Eric Chamberlain on 6/15/10. +// Based on SRVResolver by Apple, Inc. +// + +#import "XMPPSRVResolver.h" +#import "XMPPLogging.h" + +//#warning Fix "dns.h" issue without resorting to this ugly hack. +// This is a hack to prevent OnionKit's clobbering of the actual system's +//#include "/usr/include/dns.h" + +#include +#include + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +NSString *const XMPPSRVResolverErrorDomain = @"XMPPSRVResolverErrorDomain"; + +// Log levels: off, error, warn, info, verbose +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface XMPPSRVRecord () + +@property(nonatomic, assign) NSUInteger srvResultsIndex; +@property(nonatomic, assign) NSUInteger sum; + +- (NSComparisonResult)compareByPriority:(XMPPSRVRecord *)aRecord; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPSRVResolver + +- (id)initWithdDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq resolverQueue:(dispatch_queue_t)rq +{ + NSParameterAssert(aDelegate != nil); + NSParameterAssert(dq != NULL); + + if ((self = [super init])) + { + XMPPLogTrace(); + + delegate = aDelegate; + delegateQueue = dq; + + #if !OS_OBJECT_USE_OBJC + dispatch_retain(delegateQueue); + #endif + + if (rq) + { + resolverQueue = rq; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(resolverQueue); + #endif + } + else + { + resolverQueue = dispatch_queue_create("XMPPSRVResolver", NULL); + } + + resolverQueueTag = &resolverQueueTag; + dispatch_queue_set_specific(resolverQueue, resolverQueueTag, resolverQueueTag, NULL); + + results = [[NSMutableArray alloc] initWithCapacity:2]; + } + return self; +} + +- (void)dealloc +{ + XMPPLogTrace(); + + [self stop]; + + #if !OS_OBJECT_USE_OBJC + if (resolverQueue) + dispatch_release(resolverQueue); + #endif +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Properties +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@dynamic srvName; +@dynamic timeout; + +- (NSString *)srvName +{ + __block NSString *result = nil; + + dispatch_block_t block = ^{ + result = [srvName copy]; + }; + + if (dispatch_get_specific(resolverQueueTag)) + block(); + else + dispatch_sync(resolverQueue, block); + + return result; +} + +- (NSTimeInterval)timeout +{ + __block NSTimeInterval result = 0.0; + + dispatch_block_t block = ^{ + result = timeout; + }; + + if (dispatch_get_specific(resolverQueueTag)) + block(); + else + dispatch_sync(resolverQueue, block); + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Private Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)sortResults +{ + NSAssert(dispatch_get_specific(resolverQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + // Sort results + NSMutableArray *sortedResults = [NSMutableArray arrayWithCapacity:[results count]]; + + // Sort the list by priority (lowest number first) + [results sortUsingSelector:@selector(compareByPriority:)]; + + /* From RFC 2782 + * + * For each distinct priority level + * While there are still elements left at this priority level + * + * Select an element as specified above, in the + * description of Weight in "The format of the SRV + * RR" Section, and move it to the tail of the new + * list. + * + * The following algorithm SHOULD be used to order + * the SRV RRs of the same priority: + */ + + NSUInteger srvResultsCount; + + while ([results count] > 0) + { + srvResultsCount = [results count]; + + if (srvResultsCount == 1) + { + XMPPSRVRecord *srvRecord = results[0]; + + [sortedResults addObject:srvRecord]; + [results removeObjectAtIndex:0]; + } + else // (srvResultsCount > 1) + { + // more than two records so we need to sort + + /* To select a target to be contacted next, arrange all SRV RRs + * (that have not been ordered yet) in any order, except that all + * those with weight 0 are placed at the beginning of the list. + * + * Compute the sum of the weights of those RRs, and with each RR + * associate the running sum in the selected order. + */ + + NSUInteger runningSum = 0; + NSMutableArray *samePriorityRecords = [NSMutableArray arrayWithCapacity:srvResultsCount]; + + XMPPSRVRecord *srvRecord = results[0]; + + NSUInteger initialPriority = srvRecord.priority; + NSUInteger index = 0; + + do + { + if (srvRecord.weight == 0) + { + // add to front of array + [samePriorityRecords insertObject:srvRecord atIndex:0]; + + srvRecord.srvResultsIndex = index; + srvRecord.sum = 0; + } + else + { + // add to end of array and update the running sum + [samePriorityRecords addObject:srvRecord]; + + runningSum += srvRecord.weight; + + srvRecord.srvResultsIndex = index; + srvRecord.sum = runningSum; + } + + if (++index < srvResultsCount) + { + srvRecord = results[index]; + } + else + { + srvRecord = nil; + } + + } while(srvRecord && (srvRecord.priority == initialPriority)); + + /* Then choose a uniform random number between 0 and the sum computed + * (inclusive), and select the RR whose running sum value is the + * first in the selected order which is greater than or equal to + * the random number selected. + */ + + NSUInteger randomIndex = arc4random() % (runningSum + 1); + + for (srvRecord in samePriorityRecords) + { + if (srvRecord.sum >= randomIndex) + { + /* The target host specified in the + * selected SRV RR is the next one to be contacted by the client. + * Remove this SRV RR from the set of the unordered SRV RRs and + * apply the described algorithm to the unordered SRV RRs to select + * the next target host. Continue the ordering process until there + * are no unordered SRV RRs. This process is repeated for each + * Priority. + */ + + [sortedResults addObject:srvRecord]; + [results removeObjectAtIndex:srvRecord.srvResultsIndex]; + + break; + } + } + } + } + + results = sortedResults; + + XMPPLogVerbose(@"%@: Sorted results:\n%@", THIS_FILE, results); +} + +- (void)succeed +{ + NSAssert(dispatch_get_specific(resolverQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace(); + + [self sortResults]; + + id theDelegate = delegate; + NSArray *records = [results copy]; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + SEL selector = @selector(xmppSRVResolver:didResolveRecords:); + + if ([theDelegate respondsToSelector:selector]) + { + [theDelegate xmppSRVResolver:self didResolveRecords:records]; + } + else + { + XMPPLogWarn(@"%@: delegate doesn't implement %@", THIS_FILE, NSStringFromSelector(selector)); + } + + }}); + + [self stop]; +} + +- (void)failWithError:(NSError *)error +{ + NSAssert(dispatch_get_specific(resolverQueueTag), @"Invoked on incorrect queue"); + + XMPPLogTrace2(@"%@: %@ %@", THIS_FILE, THIS_METHOD, error); + + id theDelegate = delegate; + + if (delegateQueue != NULL) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + SEL selector = @selector(xmppSRVResolver:didNotResolveDueToError:); + + if ([theDelegate respondsToSelector:selector]) + { + [theDelegate xmppSRVResolver:self didNotResolveDueToError:error]; + } + else + { + XMPPLogWarn(@"%@: delegate doesn't implement %@", THIS_FILE, NSStringFromSelector(selector)); + } + + }}); + } + + [self stop]; +} + +- (void)failWithDNSError:(DNSServiceErrorType)sdErr +{ + XMPPLogTrace2(@"%@: %@ %i", THIS_FILE, THIS_METHOD, (int)sdErr); + + [self failWithError:[NSError errorWithDomain:XMPPSRVResolverErrorDomain code:sdErr userInfo:nil]]; +} + +- (XMPPSRVRecord *)processRecord:(const void *)rdata length:(uint16_t)rdlen +{ + XMPPLogTrace(); + + // Note: This method is almost entirely from Apple's sample code. + // + // Otherwise there would be a lot more comments and explanation... + + if (rdata == NULL) + { + XMPPLogWarn(@"%@: %@ - rdata == NULL", THIS_FILE, THIS_METHOD); + return nil; + } + + // Rather than write a whole bunch of icky parsing code, I just synthesise + // a resource record and use . + + XMPPSRVRecord *result = nil; + + NSMutableData * rrData; + dns_resource_record_t * rr; + uint8_t u8; // 1 byte + uint16_t u16; // 2 bytes + uint32_t u32; // 4 bytes + + rrData = [NSMutableData dataWithCapacity:(1 + 2 + 2 + 4 + 2 + rdlen)]; + + u8 = 0; + [rrData appendBytes:&u8 length:sizeof(u8)]; + u16 = htons(kDNSServiceType_SRV); + [rrData appendBytes:&u16 length:sizeof(u16)]; + u16 = htons(kDNSServiceClass_IN); + [rrData appendBytes:&u16 length:sizeof(u16)]; + u32 = htonl(666); + [rrData appendBytes:&u32 length:sizeof(u32)]; + u16 = htons(rdlen); + [rrData appendBytes:&u16 length:sizeof(u16)]; + [rrData appendBytes:rdata length:rdlen]; + + // Parse the record. + + rr = dns_parse_resource_record([rrData bytes], (uint32_t) [rrData length]); + if (rr != NULL) + { + NSString *target; + + target = [NSString stringWithCString:rr->data.SRV->target encoding:NSASCIIStringEncoding]; + if (target != nil) + { + UInt16 priority = rr->data.SRV->priority; + UInt16 weight = rr->data.SRV->weight; + UInt16 port = rr->data.SRV->port; + + result = [XMPPSRVRecord recordWithPriority:priority weight:weight port:port target:target]; + } + + dns_free_resource_record(rr); + } + + return result; +} + +static void QueryRecordCallback(DNSServiceRef sdRef, + DNSServiceFlags flags, + uint32_t interfaceIndex, + DNSServiceErrorType errorCode, + const char * fullname, + uint16_t rrtype, + uint16_t rrclass, + uint16_t rdlen, + const void * rdata, + uint32_t ttl, + void * context) +{ + // Called when we get a response to our query. + // It does some preliminary work, but the bulk of the interesting stuff + // is done in the processRecord:length: method. + + XMPPSRVResolver *resolver = (__bridge XMPPSRVResolver *)context; + + NSCAssert(dispatch_get_specific(resolver->resolverQueueTag), @"Invoked on incorrect queue"); + + XMPPLogCTrace(); + + if (!(flags & kDNSServiceFlagsAdd)) + { + // If the kDNSServiceFlagsAdd flag is not set, the domain information is not valid. + return; + } + + if (errorCode == kDNSServiceErr_NoError && + rrtype == kDNSServiceType_SRV) + { + XMPPSRVRecord *record = [resolver processRecord:rdata length:rdlen]; + if (record) + { + [resolver->results addObject:record]; + } + + if ( ! (flags & kDNSServiceFlagsMoreComing) ) + { + [resolver succeed]; + } + } + else + { + [resolver failWithDNSError:errorCode]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Public Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)startWithSRVName:(NSString *)aSRVName timeout:(NSTimeInterval)aTimeout +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (resolveInProgress) + { + return; + } + + XMPPLogTrace2(@"%@: startWithSRVName:%@ timeout:%f", THIS_FILE, aSRVName, aTimeout); + + // Save parameters + + srvName = [aSRVName copy]; + + timeout = aTimeout; + + // Check parameters + + const char *srvNameCStr = [srvName cStringUsingEncoding:NSASCIIStringEncoding]; + if (srvNameCStr == NULL) + { + [self failWithDNSError:kDNSServiceErr_BadParam]; + return; + + } + + // Create DNS Service + + DNSServiceErrorType sdErr; + sdErr = DNSServiceQueryRecord(&sdRef, // Pointer to unitialized DNSServiceRef + kDNSServiceFlagsReturnIntermediates, // Flags + kDNSServiceInterfaceIndexAny, // Interface index + srvNameCStr, // Full domain name + kDNSServiceType_SRV, // rrtype + kDNSServiceClass_IN, // rrclass + QueryRecordCallback, // Callback method + (__bridge void *)self); // Context pointer + + if (sdErr != kDNSServiceErr_NoError) + { + [self failWithDNSError:sdErr]; + return; + } + + // Extract unix socket (so we can poll for events) + + sdFd = DNSServiceRefSockFD(sdRef); + if (sdFd < 0) + { + // Todo... + } + + // Create GCD read source for sd file descriptor + + sdReadSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, sdFd, 0, resolverQueue); + + dispatch_source_set_event_handler(sdReadSource, ^{ @autoreleasepool { + + XMPPLogVerbose(@"%@: sdReadSource_eventHandler", THIS_FILE); + + // There is data to be read on the socket (or an error occurred). + // + // Invoking DNSServiceProcessResult will invoke our QueryRecordCallback, + // the callback we set when we created the sdRef. + + DNSServiceErrorType dnsErr = DNSServiceProcessResult(sdRef); + if (dnsErr != kDNSServiceErr_NoError) + { + [self failWithDNSError:dnsErr]; + } + + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theSdReadSource = sdReadSource; + #endif + DNSServiceRef theSdRef = sdRef; + + dispatch_source_set_cancel_handler(sdReadSource, ^{ @autoreleasepool { + + XMPPLogVerbose(@"%@: sdReadSource_cancelHandler", THIS_FILE); + + #if !OS_OBJECT_USE_OBJC + dispatch_release(theSdReadSource); + #endif + DNSServiceRefDeallocate(theSdRef); + + }}); + + dispatch_resume(sdReadSource); + + // Create timer (if requested timeout > 0) + + if (timeout > 0.0) + { + timeoutTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, resolverQueue); + + dispatch_source_set_event_handler(timeoutTimer, ^{ @autoreleasepool { + + NSString *errMsg = @"Operation timed out"; + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + NSError *err = [NSError errorWithDomain:XMPPSRVResolverErrorDomain code:0 userInfo:userInfo]; + + [self failWithError:err]; + + }}); + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(timeoutTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(timeoutTimer); + } + + resolveInProgress = YES; + }}; + + if (dispatch_get_specific(resolverQueueTag)) + block(); + else + dispatch_async(resolverQueue, block); +} + +- (void)stop +{ + dispatch_block_t block = ^{ @autoreleasepool { + + XMPPLogTrace(); + + delegate = nil; + if (delegateQueue) + { + #if !OS_OBJECT_USE_OBJC + dispatch_release(delegateQueue); + #endif + delegateQueue = NULL; + } + + [results removeAllObjects]; + + if (sdReadSource) + { + // Cancel the readSource. + // It will be released from within the cancel handler. + dispatch_source_cancel(sdReadSource); + sdReadSource = NULL; + sdFd = -1; + + // The sdRef will be deallocated from within the cancel handler too. + sdRef = NULL; + } + + if (timeoutTimer) + { + dispatch_source_cancel(timeoutTimer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(timeoutTimer); + #endif + timeoutTimer = NULL; + } + + resolveInProgress = NO; + }}; + + if (dispatch_get_specific(resolverQueueTag)) + block(); + else + dispatch_sync(resolverQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utility Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (NSString *)srvNameFromXMPPDomain:(NSString *)xmppDomain +{ + if (xmppDomain == nil) + return nil; + else + return [NSString stringWithFormat:@"_xmpp-client._tcp.%@", xmppDomain]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation XMPPSRVRecord + +@synthesize priority; +@synthesize weight; +@synthesize port; +@synthesize target; + +@synthesize sum; +@synthesize srvResultsIndex; + + ++ (XMPPSRVRecord *)recordWithPriority:(UInt16)p1 weight:(UInt16)w port:(UInt16)p2 target:(NSString *)t +{ + return [[XMPPSRVRecord alloc] initWithPriority:p1 weight:w port:p2 target:t]; +} + +- (id)initWithPriority:(UInt16)p1 weight:(UInt16)w port:(UInt16)p2 target:(NSString *)t +{ + if ((self = [super init])) + { + priority = p1; + weight = w; + port = p2; + target = [t copy]; + + sum = 0; + srvResultsIndex = 0; + } + return self; +} + + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@:%p target(%@) port(%hu) priority(%hu) weight(%hu)>", + NSStringFromClass([self class]), self, target, port, priority, weight]; +} + +- (NSComparisonResult)compareByPriority:(XMPPSRVRecord *)aRecord +{ + UInt16 mPriority = self.priority; + UInt16 aPriority = aRecord.priority; + + if (mPriority < aPriority) + return NSOrderedAscending; + + if (mPriority > aPriority) + return NSOrderedDescending; + + return NSOrderedSame; +} + +@end diff --git a/Utilities/XMPPStringPrep.h b/Utilities/XMPPStringPrep.h new file mode 100644 index 0000000..c1d41a8 --- /dev/null +++ b/Utilities/XMPPStringPrep.h @@ -0,0 +1,41 @@ +#import + + +@interface XMPPStringPrep : NSObject + +/** + * Preps a node identifier for use in a JID. + * If the given node is invalid, this method returns nil. + * + * See the XMPP RFC (6120) for details. + * + * Note: The prep properly converts the string to lowercase, as per the RFC. +**/ ++ (NSString *)prepNode:(NSString *)node; + +/** + * Preps a domain name for use in a JID. + * If the given domain is invalid, this method returns nil. + * + * See the XMPP RFC (6120) for details. +**/ ++ (NSString *)prepDomain:(NSString *)domain; + +/** + * Preps a resource identifier for use in a JID. + * If the given node is invalid, this method returns nil. + * + * See the XMPP RFC (6120) for details. + **/ ++ (NSString *)prepResource:(NSString *)resource; + +/** + * Preps a password with SASLprep profile. + * If the given string is invalid, this method returns nil. + * + * See the SCRAM RFC (5802) for details. + **/ + ++ (NSString *) prepPassword:(NSString *)password; + +@end diff --git a/Utilities/XMPPStringPrep.m b/Utilities/XMPPStringPrep.m new file mode 100644 index 0000000..5831efd --- /dev/null +++ b/Utilities/XMPPStringPrep.m @@ -0,0 +1,67 @@ +#import "XMPPStringPrep.h" +#import "stringprep.h" + + +@implementation XMPPStringPrep + ++ (NSString *)prepNode:(NSString *)node +{ + if(node == nil) return nil; + + // Each allowable portion of a JID MUST NOT be more than 1023 bytes in length. + // We make the buffer just big enough to hold a null-terminated string of this length. + char buf[1024]; + + strncpy(buf, [node UTF8String], sizeof(buf)); + + if(stringprep_xmpp_nodeprep(buf, sizeof(buf)) != 0) return nil; + + return [NSString stringWithUTF8String:buf]; +} + ++ (NSString *)prepDomain:(NSString *)domain +{ + if(domain == nil) return nil; + + // Each allowable portion of a JID MUST NOT be more than 1023 bytes in length. + // We make the buffer just big enough to hold a null-terminated string of this length. + char buf[1024]; + + strncpy(buf, [domain UTF8String], sizeof(buf)); + + if(stringprep_nameprep(buf, sizeof(buf)) != 0) return nil; + + return [NSString stringWithUTF8String:buf]; +} + ++ (NSString *)prepResource:(NSString *)resource +{ + if(resource == nil) return nil; + + // Each allowable portion of a JID MUST NOT be more than 1023 bytes in length. + // We make the buffer just big enough to hold a null-terminated string of this length. + char buf[1024]; + + strncpy(buf, [resource UTF8String], sizeof(buf)); + + if(stringprep_xmpp_resourceprep(buf, sizeof(buf)) != 0) return nil; + + return [NSString stringWithUTF8String:buf]; +} + ++ (NSString *)prepPassword:(NSString *)password +{ + if(password == nil) return nil; + + // Each allowable portion of a JID MUST NOT be more than 1023 bytes in length. + // We make the buffer just big enough to hold a null-terminated string of this length. + char buf[1024]; + + strncpy(buf, [password UTF8String], sizeof(buf)); + + if(stringprep(buf, sizeof(buf), 0, stringprep_saslprep) != 0) return nil; + + return [NSString stringWithUTF8String:buf]; +} + +@end diff --git a/Utilities/XMPPTimer.h b/Utilities/XMPPTimer.h new file mode 100644 index 0000000..64f15c1 --- /dev/null +++ b/Utilities/XMPPTimer.h @@ -0,0 +1,41 @@ +#import + +/** + * This class is a simple wrapper around dispatch_source_t timers. + * + * The primary motivation for this is to allow timers to be stored in collections. + * But the class also makes it easier to code timers, as it simplifies the API. +**/ +@interface XMPPTimer : NSObject + +/** + * Creates an instance of a timer that will fire on the given queue. + * It will invoke the given event handler block when it fires. +**/ +- (instancetype)initWithQueue:(dispatch_queue_t)queue eventHandler:(dispatch_block_t)block; + +/** + * Starts the timer. + * It will first fire after the timeout. + * After that, it will continue to fire every interval. + * + * The interval is optional. + * If interval is zero (or negative), it will not use an interval (will only fire once after the timeout). + * + * This method can only be called once. +**/ +- (void)startWithTimeout:(NSTimeInterval)timeout interval:(NSTimeInterval)interval; + +/** + * Allows you to update an already started timer. + * + * The new timeout that you pass can be applied to 'now' or to the original start time of the timer. +**/ +- (void)updateTimeout:(NSTimeInterval)timeout fromOriginalStartTime:(BOOL)useOriginalStartTime; + +/** + * Cancels the timer so that it won't fire. +**/ +- (void)cancel; + +@end diff --git a/Utilities/XMPPTimer.m b/Utilities/XMPPTimer.m new file mode 100644 index 0000000..f3b6d0c --- /dev/null +++ b/Utilities/XMPPTimer.m @@ -0,0 +1,86 @@ +#import "XMPPTimer.h" +#import "XMPPLogging.h" + +// Log levels: off, error, warn, info, verbose +// Log flags: trace +#if DEBUG + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#else + static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; +#endif + +@implementation XMPPTimer +{ + BOOL isStarted; + + dispatch_time_t start; + uint64_t timeout; + uint64_t interval; + + dispatch_source_t timer; +} + +- (instancetype)initWithQueue:(dispatch_queue_t)queue eventHandler:(dispatch_block_t)block +{ + if ((self = [super init])) + { + timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); + dispatch_source_set_event_handler(timer, block); + + isStarted = NO; + } + return self; +} + +- (void)dealloc +{ + [self cancel]; +} + +- (void)startWithTimeout:(NSTimeInterval)inTimeout interval:(NSTimeInterval)inInterval +{ + if (isStarted) + { + XMPPLogWarn(@"Unable to start timer - already started"); + return; + } + + start = dispatch_time(DISPATCH_TIME_NOW, 0); + timeout = (inTimeout * NSEC_PER_SEC); + interval = (inInterval > 0.0) ? (inInterval * NSEC_PER_SEC) : DISPATCH_TIME_FOREVER; + + dispatch_source_set_timer(timer, dispatch_time(start, timeout), interval, 0); + dispatch_resume(timer); + + isStarted = YES; +} + +- (void)updateTimeout:(NSTimeInterval)inTimeout fromOriginalStartTime:(BOOL)useOriginalStartTime +{ + if (!isStarted) + { + XMPPLogWarn(@"Unable to update timer - not yet started"); + return; + } + + if (!useOriginalStartTime) { + start = dispatch_time(DISPATCH_TIME_NOW, 0); + } + timeout = (inTimeout * NSEC_PER_SEC); + + dispatch_source_set_timer(timer, dispatch_time(start, timeout), interval, 0); +} + +- (void)cancel +{ + if (timer) + { + dispatch_source_cancel(timer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(timer); + #endif + timer = NULL; + } +} + +@end diff --git a/Vendor/CocoaAsyncSocket/GCDAsyncSocket.h b/Vendor/CocoaAsyncSocket/GCDAsyncSocket.h new file mode 100644 index 0000000..374bcdd --- /dev/null +++ b/Vendor/CocoaAsyncSocket/GCDAsyncSocket.h @@ -0,0 +1,1179 @@ +// +// GCDAsyncSocket.h +// +// This class is in the public domain. +// Originally created by Robbie Hanson in Q3 2010. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import +#import +#import +#import +#import + +#include // AF_INET, AF_INET6 + +@class GCDAsyncReadPacket; +@class GCDAsyncWritePacket; +@class GCDAsyncSocketPreBuffer; + +extern NSString *const GCDAsyncSocketException; +extern NSString *const GCDAsyncSocketErrorDomain; + +extern NSString *const GCDAsyncSocketQueueName; +extern NSString *const GCDAsyncSocketThreadName; + +extern NSString *const GCDAsyncSocketManuallyEvaluateTrust; +#if TARGET_OS_IPHONE +extern NSString *const GCDAsyncSocketUseCFStreamForTLS; +#endif +#define GCDAsyncSocketSSLPeerName (NSString *)kCFStreamSSLPeerName +#define GCDAsyncSocketSSLCertificates (NSString *)kCFStreamSSLCertificates +#define GCDAsyncSocketSSLIsServer (NSString *)kCFStreamSSLIsServer +extern NSString *const GCDAsyncSocketSSLPeerID; +extern NSString *const GCDAsyncSocketSSLProtocolVersionMin; +extern NSString *const GCDAsyncSocketSSLProtocolVersionMax; +extern NSString *const GCDAsyncSocketSSLSessionOptionFalseStart; +extern NSString *const GCDAsyncSocketSSLSessionOptionSendOneByteRecord; +extern NSString *const GCDAsyncSocketSSLCipherSuites; +#if !TARGET_OS_IPHONE +extern NSString *const GCDAsyncSocketSSLDiffieHellmanParameters; +#endif + +#define GCDAsyncSocketLoggingContext 65535 + + +enum GCDAsyncSocketError +{ + GCDAsyncSocketNoError = 0, // Never used + GCDAsyncSocketBadConfigError, // Invalid configuration + GCDAsyncSocketBadParamError, // Invalid parameter was passed + GCDAsyncSocketConnectTimeoutError, // A connect operation timed out + GCDAsyncSocketReadTimeoutError, // A read operation timed out + GCDAsyncSocketWriteTimeoutError, // A write operation timed out + GCDAsyncSocketReadMaxedOutError, // Reached set maxLength without completing + GCDAsyncSocketClosedError, // The remote peer closed the connection + GCDAsyncSocketOtherError, // Description provided in userInfo +}; +typedef enum GCDAsyncSocketError GCDAsyncSocketError; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface GCDAsyncSocket : NSObject + +/** + * GCDAsyncSocket uses the standard delegate paradigm, + * but executes all delegate callbacks on a given delegate dispatch queue. + * This allows for maximum concurrency, while at the same time providing easy thread safety. + * + * You MUST set a delegate AND delegate dispatch queue before attempting to + * use the socket, or you will get an error. + * + * The socket queue is optional. + * If you pass NULL, GCDAsyncSocket will automatically create it's own socket queue. + * If you choose to provide a socket queue, the socket queue must not be a concurrent queue. + * If you choose to provide a socket queue, and the socket queue has a configured target queue, + * then please see the discussion for the method markSocketQueueTargetQueue. + * + * The delegate queue and socket queue can optionally be the same. +**/ +- (id)init; +- (id)initWithSocketQueue:(dispatch_queue_t)sq; +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq; +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq; + +#pragma mark Configuration + +@property (atomic, weak, readwrite) id delegate; +#if OS_OBJECT_USE_OBJC +@property (atomic, strong, readwrite) dispatch_queue_t delegateQueue; +#else +@property (atomic, assign, readwrite) dispatch_queue_t delegateQueue; +#endif + +- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr; +- (void)setDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; + +/** + * If you are setting the delegate to nil within the delegate's dealloc method, + * you may need to use the synchronous versions below. +**/ +- (void)synchronouslySetDelegate:(id)delegate; +- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)delegateQueue; +- (void)synchronouslySetDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; + +/** + * By default, both IPv4 and IPv6 are enabled. + * + * For accepting incoming connections, this means GCDAsyncSocket automatically supports both protocols, + * and can simulataneously accept incoming connections on either protocol. + * + * For outgoing connections, this means GCDAsyncSocket can connect to remote hosts running either protocol. + * If a DNS lookup returns only IPv4 results, GCDAsyncSocket will automatically use IPv4. + * If a DNS lookup returns only IPv6 results, GCDAsyncSocket will automatically use IPv6. + * If a DNS lookup returns both IPv4 and IPv6 results, the preferred protocol will be chosen. + * By default, the preferred protocol is IPv4, but may be configured as desired. +**/ + +@property (atomic, assign, readwrite, getter=isIPv4Enabled) BOOL IPv4Enabled; +@property (atomic, assign, readwrite, getter=isIPv6Enabled) BOOL IPv6Enabled; + +@property (atomic, assign, readwrite, getter=isIPv4PreferredOverIPv6) BOOL IPv4PreferredOverIPv6; + +/** + * User data allows you to associate arbitrary information with the socket. + * This data is not used internally by socket in any way. +**/ +@property (atomic, strong, readwrite) id userData; + +#pragma mark Accepting + +/** + * Tells the socket to begin listening and accepting connections on the given port. + * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it, + * and the socket:didAcceptNewSocket: delegate method will be invoked. + * + * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) +**/ +- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * This method is the same as acceptOnPort:error: with the + * additional option of specifying which interface to listen on. + * + * For example, you could specify that the socket should only accept connections over ethernet, + * and not other interfaces such as wifi. + * + * The interface may be specified by name (e.g. "en1" or "lo0") or by IP address (e.g. "192.168.4.34"). + * You may also use the special strings "localhost" or "loopback" to specify that + * the socket only accept connections from the local machine. + * + * You can see the list of interfaces via the command line utility "ifconfig", + * or programmatically via the getifaddrs() function. + * + * To accept connections on any interface pass nil, or simply use the acceptOnPort:error: method. +**/ +- (BOOL)acceptOnInterface:(NSString *)interface port:(uint16_t)port error:(NSError **)errPtr; + +#pragma mark Connecting + +/** + * Connects to the given host and port. + * + * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: + * and uses the default interface, and no timeout. +**/ +- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * Connects to the given host and port with an optional timeout. + * + * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: and uses the default interface. +**/ +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Connects to the given host & port, via the optional interface, with an optional timeout. + * + * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). + * The host may also be the special strings "localhost" or "loopback" to specify connecting + * to a service on the local machine. + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * The interface may also be used to specify the local port (see below). + * + * To not time out use a negative time interval. + * + * This method will return NO if an error is detected, and set the error pointer (if one was given). + * Possible errors would be a nil host, invalid interface, or socket is already connected. + * + * If no errors are detected, this method will start a background connect operation and immediately return YES. + * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. + * + * Since this class supports queued reads and writes, you can immediately start reading and/or writing. + * All read/write operations will be queued, and upon socket connection, + * the operations will be dequeued and processed in order. + * + * The interface may optionally contain a port number at the end of the string, separated by a colon. + * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) + * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". + * To specify only local port: ":8082". + * Please note this is an advanced feature, and is somewhat hidden on purpose. + * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. + * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. + * Local ports do NOT need to match remote ports. In fact, they almost never do. + * This feature is here for networking professionals using very advanced techniques. +**/ +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + viaInterface:(NSString *)interface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Connects to the given address, specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetService's addresses method. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * This method invokes connectToAdd +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; + +/** + * This method is the same as connectToAddress:error: with an additional timeout option. + * To not time out use a negative time interval, or simply use the connectToAddress:error: method. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +/** + * Connects to the given address, using the specified interface and timeout. + * + * The address is specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetService's addresses method. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * The interface may also be used to specify the local port (see below). + * + * The timeout is optional. To not time out use a negative time interval. + * + * This method will return NO if an error is detected, and set the error pointer (if one was given). + * Possible errors would be a nil host, invalid interface, or socket is already connected. + * + * If no errors are detected, this method will start a background connect operation and immediately return YES. + * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. + * + * Since this class supports queued reads and writes, you can immediately start reading and/or writing. + * All read/write operations will be queued, and upon socket connection, + * the operations will be dequeued and processed in order. + * + * The interface may optionally contain a port number at the end of the string, separated by a colon. + * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) + * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". + * To specify only local port: ":8082". + * Please note this is an advanced feature, and is somewhat hidden on purpose. + * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. + * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. + * Local ports do NOT need to match remote ports. In fact, they almost never do. + * This feature is here for networking professionals using very advanced techniques. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr + viaInterface:(NSString *)interface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +#pragma mark Disconnecting + +/** + * Disconnects immediately (synchronously). Any pending reads or writes are dropped. + * + * If the socket is not already disconnected, an invocation to the socketDidDisconnect:withError: delegate method + * will be queued onto the delegateQueue asynchronously (behind any previously queued delegate methods). + * In other words, the disconnected delegate method will be invoked sometime shortly after this method returns. + * + * Please note the recommended way of releasing a GCDAsyncSocket instance (e.g. in a dealloc method) + * [asyncSocket setDelegate:nil]; + * [asyncSocket disconnect]; + * [asyncSocket release]; + * + * If you plan on disconnecting the socket, and then immediately asking it to connect again, + * you'll likely want to do so like this: + * [asyncSocket setDelegate:nil]; + * [asyncSocket disconnect]; + * [asyncSocket setDelegate:self]; + * [asyncSocket connect...]; +**/ +- (void)disconnect; + +/** + * Disconnects after all pending reads have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending writes. +**/ +- (void)disconnectAfterReading; + +/** + * Disconnects after all pending writes have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending reads. +**/ +- (void)disconnectAfterWriting; + +/** + * Disconnects after all pending reads and writes have completed. + * After calling this, the read and write methods will do nothing. +**/ +- (void)disconnectAfterReadingAndWriting; + +#pragma mark Diagnostics + +/** + * Returns whether the socket is disconnected or connected. + * + * A disconnected socket may be recycled. + * That is, it can used again for connecting or listening. + * + * If a socket is in the process of connecting, it may be neither disconnected nor connected. +**/ +@property (atomic, readonly) BOOL isDisconnected; +@property (atomic, readonly) BOOL isConnected; + +/** + * Returns the local or remote host and port to which this socket is connected, or nil and 0 if not connected. + * The host will be an IP address. +**/ +@property (atomic, readonly) NSString *connectedHost; +@property (atomic, readonly) uint16_t connectedPort; + +@property (atomic, readonly) NSString *localHost; +@property (atomic, readonly) uint16_t localPort; + +/** + * Returns the local or remote address to which this socket is connected, + * specified as a sockaddr structure wrapped in a NSData object. + * + * @seealso connectedHost + * @seealso connectedPort + * @seealso localHost + * @seealso localPort +**/ +@property (atomic, readonly) NSData *connectedAddress; +@property (atomic, readonly) NSData *localAddress; + +/** + * Returns whether the socket is IPv4 or IPv6. + * An accepting socket may be both. +**/ +@property (atomic, readonly) BOOL isIPv4; +@property (atomic, readonly) BOOL isIPv6; + +/** + * Returns whether or not the socket has been secured via SSL/TLS. + * + * See also the startTLS method. +**/ +@property (atomic, readonly) BOOL isSecure; + +#pragma mark Reading + +// The readData and writeData methods won't block (they are asynchronous). +// +// When a read is complete the socket:didReadData:withTag: delegate method is dispatched on the delegateQueue. +// When a write is complete the socket:didWriteDataWithTag: delegate method is dispatched on the delegateQueue. +// +// You may optionally set a timeout for any read/write operation. (To not timeout, use a negative time interval.) +// If a read/write opertion times out, the corresponding "socket:shouldTimeout..." delegate method +// is called to optionally allow you to extend the timeout. +// Upon a timeout, the "socket:didDisconnectWithError:" method is called +// +// The tag is for your convenience. +// You can use it as an array index, step number, state id, pointer, etc. + +/** + * Reads the first available bytes that become available on the socket. + * + * If the timeout value is negative, the read operation will not use a timeout. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, the socket will create a buffer for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * A maximum of length bytes will be read. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * If maxLength is zero, no length restriction is enforced. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Reads the given number of bytes. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If the length is 0, this method does nothing and the delegate is not called. +**/ +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the given number of bytes. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If the length is 0, this method does nothing and the delegate is not called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * If you pass a maxLength parameter that is less than the length of the data parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass a maxLength parameter that is less than the length of the data (separator) parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Returns progress of the current read, from 0.0 to 1.0, or NaN if no current read (use isnan() to check). + * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. +**/ +- (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr; + +#pragma mark Writing + +/** + * Writes data to the socket, and calls the delegate when finished. + * + * If you pass in nil or zero-length data, this method does nothing and the delegate will not be called. + * If the timeout value is negative, the write operation will not use a timeout. + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is writing it. In other words, it's not safe to alter the data until after the delegate method + * socket:didWriteDataWithTag: is invoked signifying that this particular write operation has completed. + * This is due to the fact that GCDAsyncSocket does NOT copy the data. It simply retains it. + * This is for performance reasons. Often times, if NSMutableData is passed, it is because + * a request/response was built up in memory. Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes writing the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Returns progress of the current write, from 0.0 to 1.0, or NaN if no current write (use isnan() to check). + * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. +**/ +- (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr; + +#pragma mark Security + +/** + * Secures the connection using SSL/TLS. + * + * This method may be called at any time, and the TLS handshake will occur after all pending reads and writes + * are finished. This allows one the option of sending a protocol dependent StartTLS message, and queuing + * the upgrade to TLS at the same time, without having to wait for the write to finish. + * Any reads or writes scheduled after this method is called will occur over the secured connection. + * + * ==== The available TOP-LEVEL KEYS are: + * + * - GCDAsyncSocketManuallyEvaluateTrust + * The value must be of type NSNumber, encapsulating a BOOL value. + * If you set this to YES, then the underlying SecureTransport system will not evaluate the SecTrustRef of the peer. + * Instead it will pause at the moment evaulation would typically occur, + * and allow us to handle the security evaluation however we see fit. + * So GCDAsyncSocket will invoke the delegate method socket:shouldTrustPeer: passing the SecTrustRef. + * + * Note that if you set this option, then all other configuration keys are ignored. + * Evaluation will be completely up to you during the socket:didReceiveTrust:completionHandler: delegate method. + * + * For more information on trust evaluation see: + * Apple's Technical Note TN2232 - HTTPS Server Trust Evaluation + * https://developer.apple.com/library/ios/technotes/tn2232/_index.html + * + * If unspecified, the default value is NO. + * + * - GCDAsyncSocketUseCFStreamForTLS (iOS only) + * The value must be of type NSNumber, encapsulating a BOOL value. + * By default GCDAsyncSocket will use the SecureTransport layer to perform encryption. + * This gives us more control over the security protocol (many more configuration options), + * plus it allows us to optimize things like sys calls and buffer allocation. + * + * However, if you absolutely must, you can instruct GCDAsyncSocket to use the old-fashioned encryption + * technique by going through the CFStream instead. So instead of using SecureTransport, GCDAsyncSocket + * will instead setup a CFRead/CFWriteStream. And then set the kCFStreamPropertySSLSettings property + * (via CFReadStreamSetProperty / CFWriteStreamSetProperty) and will pass the given options to this method. + * + * Thus all the other keys in the given dictionary will be ignored by GCDAsyncSocket, + * and will passed directly CFReadStreamSetProperty / CFWriteStreamSetProperty. + * For more infomation on these keys, please see the documentation for kCFStreamPropertySSLSettings. + * + * If unspecified, the default value is NO. + * + * ==== The available CONFIGURATION KEYS are: + * + * - kCFStreamSSLPeerName + * The value must be of type NSString. + * It should match the name in the X.509 certificate given by the remote party. + * See Apple's documentation for SSLSetPeerDomainName. + * + * - kCFStreamSSLCertificates + * The value must be of type NSArray. + * See Apple's documentation for SSLSetCertificate. + * + * - kCFStreamSSLIsServer + * The value must be of type NSNumber, encapsulationg a BOOL value. + * See Apple's documentation for SSLCreateContext for iOS. + * This is optional for iOS. If not supplied, a NO value is the default. + * This is not needed for Mac OS X, and the value is ignored. + * + * - GCDAsyncSocketSSLPeerID + * The value must be of type NSData. + * You must set this value if you want to use TLS session resumption. + * See Apple's documentation for SSLSetPeerID. + * + * - GCDAsyncSocketSSLProtocolVersionMin + * - GCDAsyncSocketSSLProtocolVersionMax + * The value(s) must be of type NSNumber, encapsulting a SSLProtocol value. + * See Apple's documentation for SSLSetProtocolVersionMin & SSLSetProtocolVersionMax. + * See also the SSLProtocol typedef. + * + * - GCDAsyncSocketSSLSessionOptionFalseStart + * The value must be of type NSNumber, encapsulating a BOOL value. + * See Apple's documentation for kSSLSessionOptionFalseStart. + * + * - GCDAsyncSocketSSLSessionOptionSendOneByteRecord + * The value must be of type NSNumber, encapsulating a BOOL value. + * See Apple's documentation for kSSLSessionOptionSendOneByteRecord. + * + * - GCDAsyncSocketSSLCipherSuites + * The values must be of type NSArray. + * Each item within the array must be a NSNumber, encapsulating + * See Apple's documentation for SSLSetEnabledCiphers. + * See also the SSLCipherSuite typedef. + * + * - GCDAsyncSocketSSLDiffieHellmanParameters (Mac OS X only) + * The value must be of type NSData. + * See Apple's documentation for SSLSetDiffieHellmanParams. + * + * ==== The following UNAVAILABLE KEYS are: (with throw an exception) + * + * - kCFStreamSSLAllowsAnyRoot (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsAnyRoot + * + * - kCFStreamSSLAllowsExpiredRoots (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsExpiredRoots + * + * - kCFStreamSSLAllowsExpiredCertificates (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsExpiredCerts + * + * - kCFStreamSSLValidatesCertificateChain (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetEnableCertVerify + * + * - kCFStreamSSLLevel (UNAVAILABLE) + * You MUST use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMin instead. + * Corresponding deprecated method: SSLSetProtocolVersionEnabled + * + * + * Please refer to Apple's documentation for corresponding SSLFunctions. + * + * If you pass in nil or an empty dictionary, the default settings will be used. + * + * IMPORTANT SECURITY NOTE: + * The default settings will check to make sure the remote party's certificate is signed by a + * trusted 3rd party certificate agency (e.g. verisign) and that the certificate is not expired. + * However it will not verify the name on the certificate unless you + * give it a name to verify against via the kCFStreamSSLPeerName key. + * The security implications of this are important to understand. + * Imagine you are attempting to create a secure connection to MySecureServer.com, + * but your socket gets directed to MaliciousServer.com because of a hacked DNS server. + * If you simply use the default settings, and MaliciousServer.com has a valid certificate, + * the default settings will not detect any problems since the certificate is valid. + * To properly secure your connection in this particular scenario you + * should set the kCFStreamSSLPeerName property to "MySecureServer.com". + * + * You can also perform additional validation in socketDidSecure. +**/ +- (void)startTLS:(NSDictionary *)tlsSettings; + +#pragma mark Advanced + +/** + * Traditionally sockets are not closed until the conversation is over. + * However, it is technically possible for the remote enpoint to close its write stream. + * Our socket would then be notified that there is no more data to be read, + * but our socket would still be writeable and the remote endpoint could continue to receive our data. + * + * The argument for this confusing functionality stems from the idea that a client could shut down its + * write stream after sending a request to the server, thus notifying the server there are to be no further requests. + * In practice, however, this technique did little to help server developers. + * + * To make matters worse, from a TCP perspective there is no way to tell the difference from a read stream close + * and a full socket close. They both result in the TCP stack receiving a FIN packet. The only way to tell + * is by continuing to write to the socket. If it was only a read stream close, then writes will continue to work. + * Otherwise an error will be occur shortly (when the remote end sends us a RST packet). + * + * In addition to the technical challenges and confusion, many high level socket/stream API's provide + * no support for dealing with the problem. If the read stream is closed, the API immediately declares the + * socket to be closed, and shuts down the write stream as well. In fact, this is what Apple's CFStream API does. + * It might sound like poor design at first, but in fact it simplifies development. + * + * The vast majority of the time if the read stream is closed it's because the remote endpoint closed its socket. + * Thus it actually makes sense to close the socket at this point. + * And in fact this is what most networking developers want and expect to happen. + * However, if you are writing a server that interacts with a plethora of clients, + * you might encounter a client that uses the discouraged technique of shutting down its write stream. + * If this is the case, you can set this property to NO, + * and make use of the socketDidCloseReadStream delegate method. + * + * The default value is YES. +**/ +@property (atomic, assign, readwrite) BOOL autoDisconnectOnClosedReadStream; + +/** + * GCDAsyncSocket maintains thread safety by using an internal serial dispatch_queue. + * In most cases, the instance creates this queue itself. + * However, to allow for maximum flexibility, the internal queue may be passed in the init method. + * This allows for some advanced options such as controlling socket priority via target queues. + * However, when one begins to use target queues like this, they open the door to some specific deadlock issues. + * + * For example, imagine there are 2 queues: + * dispatch_queue_t socketQueue; + * dispatch_queue_t socketTargetQueue; + * + * If you do this (pseudo-code): + * socketQueue.targetQueue = socketTargetQueue; + * + * Then all socketQueue operations will actually get run on the given socketTargetQueue. + * This is fine and works great in most situations. + * But if you run code directly from within the socketTargetQueue that accesses the socket, + * you could potentially get deadlock. Imagine the following code: + * + * - (BOOL)socketHasSomething + * { + * __block BOOL result = NO; + * dispatch_block_t block = ^{ + * result = [self someInternalMethodToBeRunOnlyOnSocketQueue]; + * } + * if (is_executing_on_queue(socketQueue)) + * block(); + * else + * dispatch_sync(socketQueue, block); + * + * return result; + * } + * + * What happens if you call this method from the socketTargetQueue? The result is deadlock. + * This is because the GCD API offers no mechanism to discover a queue's targetQueue. + * Thus we have no idea if our socketQueue is configured with a targetQueue. + * If we had this information, we could easily avoid deadlock. + * But, since these API's are missing or unfeasible, you'll have to explicitly set it. + * + * IF you pass a socketQueue via the init method, + * AND you've configured the passed socketQueue with a targetQueue, + * THEN you should pass the end queue in the target hierarchy. + * + * For example, consider the following queue hierarchy: + * socketQueue -> ipQueue -> moduleQueue + * + * This example demonstrates priority shaping within some server. + * All incoming client connections from the same IP address are executed on the same target queue. + * And all connections for a particular module are executed on the same target queue. + * Thus, the priority of all networking for the entire module can be changed on the fly. + * Additionally, networking traffic from a single IP cannot monopolize the module. + * + * Here's how you would accomplish something like that: + * - (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock + * { + * dispatch_queue_t socketQueue = dispatch_queue_create("", NULL); + * dispatch_queue_t ipQueue = [self ipQueueForAddress:address]; + * + * dispatch_set_target_queue(socketQueue, ipQueue); + * dispatch_set_target_queue(iqQueue, moduleQueue); + * + * return socketQueue; + * } + * - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket + * { + * [clientConnections addObject:newSocket]; + * [newSocket markSocketQueueTargetQueue:moduleQueue]; + * } + * + * Note: This workaround is ONLY needed if you intend to execute code directly on the ipQueue or moduleQueue. + * This is often NOT the case, as such queues are used solely for execution shaping. +**/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreConfiguredTargetQueue; +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreviouslyConfiguredTargetQueue; + +/** + * It's not thread-safe to access certain variables from outside the socket's internal queue. + * + * For example, the socket file descriptor. + * File descriptors are simply integers which reference an index in the per-process file table. + * However, when one requests a new file descriptor (by opening a file or socket), + * the file descriptor returned is guaranteed to be the lowest numbered unused descriptor. + * So if we're not careful, the following could be possible: + * + * - Thread A invokes a method which returns the socket's file descriptor. + * - The socket is closed via the socket's internal queue on thread B. + * - Thread C opens a file, and subsequently receives the file descriptor that was previously the socket's FD. + * - Thread A is now accessing/altering the file instead of the socket. + * + * In addition to this, other variables are not actually objects, + * and thus cannot be retained/released or even autoreleased. + * An example is the sslContext, of type SSLContextRef, which is actually a malloc'd struct. + * + * Although there are internal variables that make it difficult to maintain thread-safety, + * it is important to provide access to these variables + * to ensure this class can be used in a wide array of environments. + * This method helps to accomplish this by invoking the current block on the socket's internal queue. + * The methods below can be invoked from within the block to access + * those generally thread-unsafe internal variables in a thread-safe manner. + * The given block will be invoked synchronously on the socket's internal queue. + * + * If you save references to any protected variables and use them outside the block, you do so at your own peril. +**/ +- (void)performBlock:(dispatch_block_t)block; + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's file descriptor(s). + * If the socket is a server socket (is accepting incoming connections), + * it might actually have multiple internal socket file descriptors - one for IPv4 and one for IPv6. +**/ +- (int)socketFD; +- (int)socket4FD; +- (int)socket6FD; + +#if TARGET_OS_IPHONE + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's internal CFReadStream/CFWriteStream. + * + * These streams are only used as workarounds for specific iOS shortcomings: + * + * - Apple has decided to keep the SecureTransport framework private is iOS. + * This means the only supplied way to do SSL/TLS is via CFStream or some other API layered on top of it. + * Thus, in order to provide SSL/TLS support on iOS we are forced to rely on CFStream, + * instead of the preferred and faster and more powerful SecureTransport. + * + * - If a socket doesn't have backgrounding enabled, and that socket is closed while the app is backgrounded, + * Apple only bothers to notify us via the CFStream API. + * The faster and more powerful GCD API isn't notified properly in this case. + * + * See also: (BOOL)enableBackgroundingOnSocket +**/ +- (CFReadStreamRef)readStream; +- (CFWriteStreamRef)writeStream; + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Configures the socket to allow it to operate when the iOS application has been backgrounded. + * In other words, this method creates a read & write stream, and invokes: + * + * CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * + * Returns YES if successful, NO otherwise. + * + * Note: Apple does not officially support backgrounding server sockets. + * That is, if your socket is accepting incoming connections, Apple does not officially support + * allowing iOS applications to accept incoming connections while an app is backgrounded. + * + * Example usage: + * + * - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port + * { + * [asyncSocket performBlock:^{ + * [asyncSocket enableBackgroundingOnSocket]; + * }]; + * } +**/ +- (BOOL)enableBackgroundingOnSocket; + +#endif + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's SSLContext, if SSL/TLS has been started on the socket. +**/ +- (SSLContextRef)sslContext; + +#pragma mark Utilities + +/** + * The address lookup utility used by the class. + * This method is synchronous, so it's recommended you use it on a background thread/queue. + * + * The special strings "localhost" and "loopback" return the loopback address for IPv4 and IPv6. + * + * @returns + * A mutable array with all IPv4 and IPv6 addresses returned by getaddrinfo. + * The addresses are specifically for TCP connections. + * You can filter the addresses, if needed, using the other utility methods provided by the class. +**/ ++ (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr; + +/** + * Extracting host and port information from raw address data. +**/ + ++ (NSString *)hostFromAddress:(NSData *)address; ++ (uint16_t)portFromAddress:(NSData *)address; + ++ (BOOL)isIPv4Address:(NSData *)address; ++ (BOOL)isIPv6Address:(NSData *)address; + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address; + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(sa_family_t *)afPtr fromAddress:(NSData *)address; + +/** + * A few common line separators, for use with the readDataToData:... methods. +**/ ++ (NSData *)CRLFData; // 0x0D0A ++ (NSData *)CRData; // 0x0D ++ (NSData *)LFData; // 0x0A ++ (NSData *)ZeroData; // 0x00 + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol GCDAsyncSocketDelegate +@optional + +/** + * This method is called immediately prior to socket:didAcceptNewSocket:. + * It optionally allows a listening socket to specify the socketQueue for a new accepted socket. + * If this method is not implemented, or returns NULL, the new accepted socket will create its own default queue. + * + * Since you cannot autorelease a dispatch_queue, + * this method uses the "new" prefix in its name to specify that the returned queue has been retained. + * + * Thus you could do something like this in the implementation: + * return dispatch_queue_create("MyQueue", NULL); + * + * If you are placing multiple sockets on the same queue, + * then care should be taken to increment the retain count each time this method is invoked. + * + * For example, your implementation might look something like this: + * dispatch_retain(myExistingQueue); + * return myExistingQueue; +**/ +- (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock; + +/** + * Called when a socket accepts a connection. + * Another socket is automatically spawned to handle it. + * + * You must retain the newSocket if you wish to handle the connection. + * Otherwise the newSocket instance will be released and the spawned connection will be closed. + * + * By default the new socket will have the same delegate and delegateQueue. + * You may, of course, change this at any time. +**/ +- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket; + +/** + * Called when a socket connects and is ready for reading and writing. + * The host parameter will be an IP address, not a DNS name. +**/ +- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port; + +/** + * Called when a socket has completed reading the requested data into memory. + * Not called if there is an error. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag; + +/** + * Called when a socket has read in data, but has not yet completed the read. + * This would occur if using readToData: or readToLength: methods. + * It may be used to for things such as updating progress bars. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called when a socket has completed writing the requested data. Not called if there is an error. +**/ +- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag; + +/** + * Called when a socket has written some data, but has not yet completed the entire write. + * It may be used to for things such as updating progress bars. +**/ +- (void)socket:(GCDAsyncSocket *)sock didWritePartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called if a read operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the read's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the read will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been read so far for the read operation. + * + * Note that this method may be called multiple times for a single read if you return positive numbers. +**/ +- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Called if a write operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the write's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the write will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been written so far for the write operation. + * + * Note that this method may be called multiple times for a single write if you return positive numbers. +**/ +- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutWriteWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Conditionally called if the read stream closes, but the write stream may still be writeable. + * + * This delegate method is only called if autoDisconnectOnClosedReadStream has been set to NO. + * See the discussion on the autoDisconnectOnClosedReadStream method for more information. +**/ +- (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock; + +/** + * Called when a socket disconnects with or without error. + * + * If you call the disconnect method, and the socket wasn't already disconnected, + * then an invocation of this delegate method will be enqueued on the delegateQueue + * before the disconnect method returns. + * + * Note: If the GCDAsyncSocket instance is deallocated while it is still connected, + * and the delegate is not also deallocated, then this method will be invoked, + * but the sock parameter will be nil. (It must necessarily be nil since it is no longer available.) + * This is a generally rare, but is possible if one writes code like this: + * + * asyncSocket = nil; // I'm implicitly disconnecting the socket + * + * In this case it may preferrable to nil the delegate beforehand, like this: + * + * asyncSocket.delegate = nil; // Don't invoke my delegate method + * asyncSocket = nil; // I'm implicitly disconnecting the socket + * + * Of course, this depends on how your state machine is configured. +**/ +- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err; + +/** + * Called after the socket has successfully completed SSL/TLS negotiation. + * This method is not called unless you use the provided startTLS method. + * + * If a SSL/TLS negotiation fails (invalid certificate, etc) then the socket will immediately close, + * and the socketDidDisconnect:withError: delegate method will be called with the specific SSL error code. +**/ +- (void)socketDidSecure:(GCDAsyncSocket *)sock; + +/** + * Allows a socket delegate to hook into the TLS handshake and manually validate the peer it's connecting to. + * + * This is only called if startTLS is invoked with options that include: + * - GCDAsyncSocketManuallyEvaluateTrust == YES + * + * Typically the delegate will use SecTrustEvaluate (and related functions) to properly validate the peer. + * + * Note from Apple's documentation: + * Because [SecTrustEvaluate] might look on the network for certificates in the certificate chain, + * [it] might block while attempting network access. You should never call it from your main thread; + * call it only from within a function running on a dispatch queue or on a separate thread. + * + * Thus this method uses a completionHandler block rather than a normal return value. + * The completionHandler block is thread-safe, and may be invoked from a background queue/thread. + * It is safe to invoke the completionHandler block even if the socket has been closed. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust + completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler; + +@end diff --git a/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m b/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m new file mode 100644 index 0000000..531a29d --- /dev/null +++ b/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m @@ -0,0 +1,7719 @@ +// +// GCDAsyncSocket.m +// +// This class is in the public domain. +// Originally created by Robbie Hanson in Q4 2010. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import "GCDAsyncSocket.h" + +#if TARGET_OS_IPHONE +#import +#endif + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +// For more information see: https://github.com/robbiehanson/CocoaAsyncSocket/wiki/ARC +#endif + + +#ifndef GCDAsyncSocketLoggingEnabled +#define GCDAsyncSocketLoggingEnabled 0 +#endif + +#if GCDAsyncSocketLoggingEnabled + +// Logging Enabled - See log level below + +// Logging uses the CocoaLumberjack framework (which is also GCD based). +// https://github.com/robbiehanson/CocoaLumberjack +// +// It allows us to do a lot of logging without significantly slowing down the code. +#import "DDLog.h" + +#define LogAsync YES +#define LogContext GCDAsyncSocketLoggingContext + +#define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) +#define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) + +#define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) + +#define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) + +#define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD) +#define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__) + +#ifndef GCDAsyncSocketLogLevel +#define GCDAsyncSocketLogLevel LOG_LEVEL_VERBOSE +#endif + +// Log levels : off, error, warn, info, verbose +static const int logLevel = GCDAsyncSocketLogLevel; + +#else + +// Logging Disabled + +#define LogError(frmt, ...) {} +#define LogWarn(frmt, ...) {} +#define LogInfo(frmt, ...) {} +#define LogVerbose(frmt, ...) {} + +#define LogCError(frmt, ...) {} +#define LogCWarn(frmt, ...) {} +#define LogCInfo(frmt, ...) {} +#define LogCVerbose(frmt, ...) {} + +#define LogTrace() {} +#define LogCTrace(frmt, ...) {} + +#endif + +/** + * Seeing a return statements within an inner block + * can sometimes be mistaken for a return point of the enclosing method. + * This makes inline blocks a bit easier to read. +**/ +#define return_from_block return + +/** + * A socket file descriptor is really just an integer. + * It represents the index of the socket within the kernel. + * This makes invalid file descriptor comparisons easier to read. +**/ +#define SOCKET_NULL -1 + + +NSString *const GCDAsyncSocketException = @"GCDAsyncSocketException"; +NSString *const GCDAsyncSocketErrorDomain = @"GCDAsyncSocketErrorDomain"; + +NSString *const GCDAsyncSocketQueueName = @"GCDAsyncSocket"; +NSString *const GCDAsyncSocketThreadName = @"GCDAsyncSocket-CFStream"; + +NSString *const GCDAsyncSocketManuallyEvaluateTrust = @"GCDAsyncSocketManuallyEvaluateTrust"; +#if TARGET_OS_IPHONE +NSString *const GCDAsyncSocketUseCFStreamForTLS = @"GCDAsyncSocketUseCFStreamForTLS"; +#endif +NSString *const GCDAsyncSocketSSLPeerID = @"GCDAsyncSocketSSLPeerID"; +NSString *const GCDAsyncSocketSSLProtocolVersionMin = @"GCDAsyncSocketSSLProtocolVersionMin"; +NSString *const GCDAsyncSocketSSLProtocolVersionMax = @"GCDAsyncSocketSSLProtocolVersionMax"; +NSString *const GCDAsyncSocketSSLSessionOptionFalseStart = @"GCDAsyncSocketSSLSessionOptionFalseStart"; +NSString *const GCDAsyncSocketSSLSessionOptionSendOneByteRecord = @"GCDAsyncSocketSSLSessionOptionSendOneByteRecord"; +NSString *const GCDAsyncSocketSSLCipherSuites = @"GCDAsyncSocketSSLCipherSuites"; +#if !TARGET_OS_IPHONE +NSString *const GCDAsyncSocketSSLDiffieHellmanParameters = @"GCDAsyncSocketSSLDiffieHellmanParameters"; +#endif + +enum GCDAsyncSocketFlags +{ + kSocketStarted = 1 << 0, // If set, socket has been started (accepting/connecting) + kConnected = 1 << 1, // If set, the socket is connected + kForbidReadsWrites = 1 << 2, // If set, no new reads or writes are allowed + kReadsPaused = 1 << 3, // If set, reads are paused due to possible timeout + kWritesPaused = 1 << 4, // If set, writes are paused due to possible timeout + kDisconnectAfterReads = 1 << 5, // If set, disconnect after no more reads are queued + kDisconnectAfterWrites = 1 << 6, // If set, disconnect after no more writes are queued + kSocketCanAcceptBytes = 1 << 7, // If set, we know socket can accept bytes. If unset, it's unknown. + kReadSourceSuspended = 1 << 8, // If set, the read source is suspended + kWriteSourceSuspended = 1 << 9, // If set, the write source is suspended + kQueuedTLS = 1 << 10, // If set, we've queued an upgrade to TLS + kStartingReadTLS = 1 << 11, // If set, we're waiting for TLS negotiation to complete + kStartingWriteTLS = 1 << 12, // If set, we're waiting for TLS negotiation to complete + kSocketSecure = 1 << 13, // If set, socket is using secure communication via SSL/TLS + kSocketHasReadEOF = 1 << 14, // If set, we have read EOF from socket + kReadStreamClosed = 1 << 15, // If set, we've read EOF plus prebuffer has been drained + kDealloc = 1 << 16, // If set, the socket is being deallocated +#if TARGET_OS_IPHONE + kAddedStreamsToRunLoop = 1 << 17, // If set, CFStreams have been added to listener thread + kUsingCFStreamForTLS = 1 << 18, // If set, we're forced to use CFStream instead of SecureTransport + kSecureSocketHasBytesAvailable = 1 << 19, // If set, CFReadStream has notified us of bytes available +#endif +}; + +enum GCDAsyncSocketConfig +{ + kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled + kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled + kPreferIPv6 = 1 << 2, // If set, IPv6 is preferred over IPv4 + kAllowHalfDuplexConnection = 1 << 3, // If set, the socket will stay open even if the read stream closes +}; + +#if TARGET_OS_IPHONE + static NSThread *cfstreamThread; // Used for CFStreams + + static uint64_t cfstreamThreadRetainCount; // setup & teardown + static dispatch_queue_t cfstreamThreadSetupQueue; // setup & teardown +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * A PreBuffer is used when there is more data available on the socket + * than is being requested by current read request. + * In this case we slurp up all data from the socket (to minimize sys calls), + * and store additional yet unread data in a "prebuffer". + * + * The prebuffer is entirely drained before we read from the socket again. + * In other words, a large chunk of data is written is written to the prebuffer. + * The prebuffer is then drained via a series of one or more reads (for subsequent read request(s)). + * + * A ring buffer was once used for this purpose. + * But a ring buffer takes up twice as much memory as needed (double the size for mirroring). + * In fact, it generally takes up more than twice the needed size as everything has to be rounded up to vm_page_size. + * And since the prebuffer is always completely drained after being written to, a full ring buffer isn't needed. + * + * The current design is very simple and straight-forward, while also keeping memory requirements lower. +**/ + +@interface GCDAsyncSocketPreBuffer : NSObject +{ + uint8_t *preBuffer; + size_t preBufferSize; + + uint8_t *readPointer; + uint8_t *writePointer; +} + +- (id)initWithCapacity:(size_t)numBytes; + +- (void)ensureCapacityForWrite:(size_t)numBytes; + +- (size_t)availableBytes; +- (uint8_t *)readBuffer; + +- (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr; + +- (size_t)availableSpace; +- (uint8_t *)writeBuffer; + +- (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr; + +- (void)didRead:(size_t)bytesRead; +- (void)didWrite:(size_t)bytesWritten; + +- (void)reset; + +@end + +@implementation GCDAsyncSocketPreBuffer + +- (id)initWithCapacity:(size_t)numBytes +{ + if ((self = [super init])) + { + preBufferSize = numBytes; + preBuffer = malloc(preBufferSize); + + readPointer = preBuffer; + writePointer = preBuffer; + } + return self; +} + +- (void)dealloc +{ + if (preBuffer) + free(preBuffer); +} + +- (void)ensureCapacityForWrite:(size_t)numBytes +{ + size_t availableSpace = [self availableSpace]; + + if (numBytes > availableSpace) + { + size_t additionalBytes = numBytes - availableSpace; + + size_t newPreBufferSize = preBufferSize + additionalBytes; + uint8_t *newPreBuffer = realloc(preBuffer, newPreBufferSize); + + size_t readPointerOffset = readPointer - preBuffer; + size_t writePointerOffset = writePointer - preBuffer; + + preBuffer = newPreBuffer; + preBufferSize = newPreBufferSize; + + readPointer = preBuffer + readPointerOffset; + writePointer = preBuffer + writePointerOffset; + } +} + +- (size_t)availableBytes +{ + return writePointer - readPointer; +} + +- (uint8_t *)readBuffer +{ + return readPointer; +} + +- (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr +{ + if (bufferPtr) *bufferPtr = readPointer; + if (availableBytesPtr) *availableBytesPtr = [self availableBytes]; +} + +- (void)didRead:(size_t)bytesRead +{ + readPointer += bytesRead; + + if (readPointer == writePointer) + { + // The prebuffer has been drained. Reset pointers. + readPointer = preBuffer; + writePointer = preBuffer; + } +} + +- (size_t)availableSpace +{ + return preBufferSize - (writePointer - preBuffer); +} + +- (uint8_t *)writeBuffer +{ + return writePointer; +} + +- (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr +{ + if (bufferPtr) *bufferPtr = writePointer; + if (availableSpacePtr) *availableSpacePtr = [self availableSpace]; +} + +- (void)didWrite:(size_t)bytesWritten +{ + writePointer += bytesWritten; +} + +- (void)reset +{ + readPointer = preBuffer; + writePointer = preBuffer; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncReadPacket encompasses the instructions for any given read. + * The content of a read packet allows the code to determine if we're: + * - reading to a certain length + * - reading to a certain separator + * - or simply reading the first chunk of available data +**/ +@interface GCDAsyncReadPacket : NSObject +{ + @public + NSMutableData *buffer; + NSUInteger startOffset; + NSUInteger bytesDone; + NSUInteger maxLength; + NSTimeInterval timeout; + NSUInteger readLength; + NSData *term; + BOOL bufferOwner; + NSUInteger originalBufferLength; + long tag; +} +- (id)initWithData:(NSMutableData *)d + startOffset:(NSUInteger)s + maxLength:(NSUInteger)m + timeout:(NSTimeInterval)t + readLength:(NSUInteger)l + terminator:(NSData *)e + tag:(long)i; + +- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead; + +- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr; + +- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable; +- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr; +- (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr; + +- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes; + +@end + +@implementation GCDAsyncReadPacket + +- (id)initWithData:(NSMutableData *)d + startOffset:(NSUInteger)s + maxLength:(NSUInteger)m + timeout:(NSTimeInterval)t + readLength:(NSUInteger)l + terminator:(NSData *)e + tag:(long)i +{ + if((self = [super init])) + { + bytesDone = 0; + maxLength = m; + timeout = t; + readLength = l; + term = [e copy]; + tag = i; + + if (d) + { + buffer = d; + startOffset = s; + bufferOwner = NO; + originalBufferLength = [d length]; + } + else + { + if (readLength > 0) + buffer = [[NSMutableData alloc] initWithLength:readLength]; + else + buffer = [[NSMutableData alloc] initWithLength:0]; + + startOffset = 0; + bufferOwner = YES; + originalBufferLength = 0; + } + } + return self; +} + +/** + * Increases the length of the buffer (if needed) to ensure a read of the given size will fit. +**/ +- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead +{ + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + NSUInteger buffSpace = buffSize - buffUsed; + + if (bytesToRead > buffSpace) + { + NSUInteger buffInc = bytesToRead - buffSpace; + + [buffer increaseLengthBy:buffInc]; + } +} + +/** + * This method is used when we do NOT know how much data is available to be read from the socket. + * This method returns the default value unless it exceeds the specified readLength or maxLength. + * + * Furthermore, the shouldPreBuffer decision is based upon the packet type, + * and whether the returned value would fit in the current buffer without requiring a resize of the buffer. +**/ +- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr +{ + NSUInteger result; + + if (readLength > 0) + { + // Read a specific length of data + + result = MIN(defaultValue, (readLength - bytesDone)); + + // There is no need to prebuffer since we know exactly how much data we need to read. + // Even if the buffer isn't currently big enough to fit this amount of data, + // it would have to be resized eventually anyway. + + if (shouldPreBufferPtr) + *shouldPreBufferPtr = NO; + } + else + { + // Either reading until we find a specified terminator, + // or we're simply reading all available data. + // + // In other words, one of: + // + // - readDataToData packet + // - readDataWithTimeout packet + + if (maxLength > 0) + result = MIN(defaultValue, (maxLength - bytesDone)); + else + result = defaultValue; + + // Since we don't know the size of the read in advance, + // the shouldPreBuffer decision is based upon whether the returned value would fit + // in the current buffer without requiring a resize of the buffer. + // + // This is because, in all likelyhood, the amount read from the socket will be less than the default value. + // Thus we should avoid over-allocating the read buffer when we can simply use the pre-buffer instead. + + if (shouldPreBufferPtr) + { + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + NSUInteger buffSpace = buffSize - buffUsed; + + if (buffSpace >= result) + *shouldPreBufferPtr = NO; + else + *shouldPreBufferPtr = YES; + } + } + + return result; +} + +/** + * For read packets without a set terminator, returns the amount of data + * that can be read without exceeding the readLength or maxLength. + * + * The given parameter indicates the number of bytes estimated to be available on the socket, + * which is taken into consideration during the calculation. + * + * The given hint MUST be greater than zero. +**/ +- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable +{ + NSAssert(term == nil, @"This method does not apply to term reads"); + NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); + + if (readLength > 0) + { + // Read a specific length of data + + return MIN(bytesAvailable, (readLength - bytesDone)); + + // No need to avoid resizing the buffer. + // If the user provided their own buffer, + // and told us to read a certain length of data that exceeds the size of the buffer, + // then it is clear that our code will resize the buffer during the read operation. + // + // This method does not actually do any resizing. + // The resizing will happen elsewhere if needed. + } + else + { + // Read all available data + + NSUInteger result = bytesAvailable; + + if (maxLength > 0) + { + result = MIN(result, (maxLength - bytesDone)); + } + + // No need to avoid resizing the buffer. + // If the user provided their own buffer, + // and told us to read all available data without giving us a maxLength, + // then it is clear that our code might resize the buffer during the read operation. + // + // This method does not actually do any resizing. + // The resizing will happen elsewhere if needed. + + return result; + } +} + +/** + * For read packets with a set terminator, returns the amount of data + * that can be read without exceeding the maxLength. + * + * The given parameter indicates the number of bytes estimated to be available on the socket, + * which is taken into consideration during the calculation. + * + * To optimize memory allocations, mem copies, and mem moves + * the shouldPreBuffer boolean value will indicate if the data should be read into a prebuffer first, + * or if the data can be read directly into the read packet's buffer. +**/ +- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); + + + NSUInteger result = bytesAvailable; + + if (maxLength > 0) + { + result = MIN(result, (maxLength - bytesDone)); + } + + // Should the data be read into the read packet's buffer, or into a pre-buffer first? + // + // One would imagine the preferred option is the faster one. + // So which one is faster? + // + // Reading directly into the packet's buffer requires: + // 1. Possibly resizing packet buffer (malloc/realloc) + // 2. Filling buffer (read) + // 3. Searching for term (memcmp) + // 4. Possibly copying overflow into prebuffer (malloc/realloc, memcpy) + // + // Reading into prebuffer first: + // 1. Possibly resizing prebuffer (malloc/realloc) + // 2. Filling buffer (read) + // 3. Searching for term (memcmp) + // 4. Copying underflow into packet buffer (malloc/realloc, memcpy) + // 5. Removing underflow from prebuffer (memmove) + // + // Comparing the performance of the two we can see that reading + // data into the prebuffer first is slower due to the extra memove. + // + // However: + // The implementation of NSMutableData is open source via core foundation's CFMutableData. + // Decreasing the length of a mutable data object doesn't cause a realloc. + // In other words, the capacity of a mutable data object can grow, but doesn't shrink. + // + // This means the prebuffer will rarely need a realloc. + // The packet buffer, on the other hand, may often need a realloc. + // This is especially true if we are the buffer owner. + // Furthermore, if we are constantly realloc'ing the packet buffer, + // and then moving the overflow into the prebuffer, + // then we're consistently over-allocating memory for each term read. + // And now we get into a bit of a tradeoff between speed and memory utilization. + // + // The end result is that the two perform very similarly. + // And we can answer the original question very simply by another means. + // + // If we can read all the data directly into the packet's buffer without resizing it first, + // then we do so. Otherwise we use the prebuffer. + + if (shouldPreBufferPtr) + { + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + if ((buffSize - buffUsed) >= result) + *shouldPreBufferPtr = NO; + else + *shouldPreBufferPtr = YES; + } + + return result; +} + +/** + * For read packets with a set terminator, + * returns the amount of data that can be read from the given preBuffer, + * without going over a terminator or the maxLength. + * + * It is assumed the terminator has not already been read. +**/ +- (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + NSAssert([preBuffer availableBytes] > 0, @"Invoked with empty pre buffer!"); + + // We know that the terminator, as a whole, doesn't exist in our own buffer. + // But it is possible that a _portion_ of it exists in our buffer. + // So we're going to look for the terminator starting with a portion of our own buffer. + // + // Example: + // + // term length = 3 bytes + // bytesDone = 5 bytes + // preBuffer length = 5 bytes + // + // If we append the preBuffer to our buffer, + // it would look like this: + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // --------------------- + // + // So we start our search here: + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // -------^-^-^--------- + // + // And move forwards... + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // ---------^-^-^------- + // + // Until we find the terminator or reach the end. + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // ---------------^-^-^- + + BOOL found = NO; + + NSUInteger termLength = [term length]; + NSUInteger preBufferLength = [preBuffer availableBytes]; + + if ((bytesDone + preBufferLength) < termLength) + { + // Not enough data for a full term sequence yet + return preBufferLength; + } + + NSUInteger maxPreBufferLength; + if (maxLength > 0) { + maxPreBufferLength = MIN(preBufferLength, (maxLength - bytesDone)); + + // Note: maxLength >= termLength + } + else { + maxPreBufferLength = preBufferLength; + } + + uint8_t seq[termLength]; + const void *termBuf = [term bytes]; + + NSUInteger bufLen = MIN(bytesDone, (termLength - 1)); + uint8_t *buf = (uint8_t *)[buffer mutableBytes] + startOffset + bytesDone - bufLen; + + NSUInteger preLen = termLength - bufLen; + const uint8_t *pre = [preBuffer readBuffer]; + + NSUInteger loopCount = bufLen + maxPreBufferLength - termLength + 1; // Plus one. See example above. + + NSUInteger result = maxPreBufferLength; + + NSUInteger i; + for (i = 0; i < loopCount; i++) + { + if (bufLen > 0) + { + // Combining bytes from buffer and preBuffer + + memcpy(seq, buf, bufLen); + memcpy(seq + bufLen, pre, preLen); + + if (memcmp(seq, termBuf, termLength) == 0) + { + result = preLen; + found = YES; + break; + } + + buf++; + bufLen--; + preLen++; + } + else + { + // Comparing directly from preBuffer + + if (memcmp(pre, termBuf, termLength) == 0) + { + NSUInteger preOffset = pre - [preBuffer readBuffer]; // pointer arithmetic + + result = preOffset + termLength; + found = YES; + break; + } + + pre++; + } + } + + // There is no need to avoid resizing the buffer in this particular situation. + + if (foundPtr) *foundPtr = found; + return result; +} + +/** + * For read packets with a set terminator, scans the packet buffer for the term. + * It is assumed the terminator had not been fully read prior to the new bytes. + * + * If the term is found, the number of excess bytes after the term are returned. + * If the term is not found, this method will return -1. + * + * Note: A return value of zero means the term was found at the very end. + * + * Prerequisites: + * The given number of bytes have been added to the end of our buffer. + * Our bytesDone variable has NOT been changed due to the prebuffered bytes. +**/ +- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + + // The implementation of this method is very similar to the above method. + // See the above method for a discussion of the algorithm used here. + + uint8_t *buff = [buffer mutableBytes]; + NSUInteger buffLength = bytesDone + numBytes; + + const void *termBuff = [term bytes]; + NSUInteger termLength = [term length]; + + // Note: We are dealing with unsigned integers, + // so make sure the math doesn't go below zero. + + NSUInteger i = ((buffLength - numBytes) >= termLength) ? (buffLength - numBytes - termLength + 1) : 0; + + while (i + termLength <= buffLength) + { + uint8_t *subBuffer = buff + startOffset + i; + + if (memcmp(subBuffer, termBuff, termLength) == 0) + { + return buffLength - (i + termLength); + } + + i++; + } + + return -1; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncWritePacket encompasses the instructions for any given write. +**/ +@interface GCDAsyncWritePacket : NSObject +{ + @public + NSData *buffer; + NSUInteger bytesDone; + long tag; + NSTimeInterval timeout; +} +- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i; +@end + +@implementation GCDAsyncWritePacket + +- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i +{ + if((self = [super init])) + { + buffer = d; // Retain not copy. For performance as documented in header file. + bytesDone = 0; + timeout = t; + tag = i; + } + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncSpecialPacket encompasses special instructions for interruptions in the read/write queues. + * This class my be altered to support more than just TLS in the future. +**/ +@interface GCDAsyncSpecialPacket : NSObject +{ + @public + NSDictionary *tlsSettings; +} +- (id)initWithTLSSettings:(NSDictionary *)settings; +@end + +@implementation GCDAsyncSpecialPacket + +- (id)initWithTLSSettings:(NSDictionary *)settings +{ + if((self = [super init])) + { + tlsSettings = [settings copy]; + } + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation GCDAsyncSocket +{ + uint32_t flags; + uint16_t config; + + __weak id delegate; + dispatch_queue_t delegateQueue; + + int socket4FD; + int socket6FD; + int stateIndex; + NSData * connectInterface4; + NSData * connectInterface6; + + dispatch_queue_t socketQueue; + + dispatch_source_t accept4Source; + dispatch_source_t accept6Source; + dispatch_source_t connectTimer; + dispatch_source_t readSource; + dispatch_source_t writeSource; + dispatch_source_t readTimer; + dispatch_source_t writeTimer; + + NSMutableArray *readQueue; + NSMutableArray *writeQueue; + + GCDAsyncReadPacket *currentRead; + GCDAsyncWritePacket *currentWrite; + + unsigned long socketFDBytesAvailable; + + GCDAsyncSocketPreBuffer *preBuffer; + +#if TARGET_OS_IPHONE + CFStreamClientContext streamContext; + CFReadStreamRef readStream; + CFWriteStreamRef writeStream; +#endif + SSLContextRef sslContext; + GCDAsyncSocketPreBuffer *sslPreBuffer; + size_t sslWriteCachedLength; + OSStatus sslErrCode; + + void *IsOnSocketQueueOrTargetQueueKey; + + id userData; +} + +- (id)init +{ + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; +} + +- (id)initWithSocketQueue:(dispatch_queue_t)sq +{ + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; +} + +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq +{ + return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; +} + +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq +{ + if((self = [super init])) + { + delegate = aDelegate; + delegateQueue = dq; + + #if !OS_OBJECT_USE_OBJC + if (dq) dispatch_retain(dq); + #endif + + socket4FD = SOCKET_NULL; + socket6FD = SOCKET_NULL; + stateIndex = 0; + + if (sq) + { + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + + socketQueue = sq; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(sq); + #endif + } + else + { + socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL); + } + + // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. + // From the documentation: + // + // > Keys are only compared as pointers and are never dereferenced. + // > Thus, you can use a pointer to a static variable for a specific subsystem or + // > any other value that allows you to identify the value uniquely. + // + // We're just going to use the memory address of an ivar. + // Specifically an ivar that is explicitly named for our purpose to make the code more readable. + // + // However, it feels tedious (and less readable) to include the "&" all the time: + // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey) + // + // So we're going to make it so it doesn't matter if we use the '&' or not, + // by assigning the value of the ivar to the address of the ivar. + // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; + + IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; + + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); + + readQueue = [[NSMutableArray alloc] initWithCapacity:5]; + currentRead = nil; + + writeQueue = [[NSMutableArray alloc] initWithCapacity:5]; + currentWrite = nil; + + preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; + } + return self; +} + +- (void)dealloc +{ + LogInfo(@"%@ - %@ (start)", THIS_METHOD, self); + + // Set dealloc flag. + // This is used by closeWithError to ensure we don't accidentally retain ourself. + flags |= kDealloc; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + [self closeWithError:nil]; + } + else + { + dispatch_sync(socketQueue, ^{ + [self closeWithError:nil]; + }); + } + + delegate = nil; + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); + #endif + delegateQueue = NULL; + + #if !OS_OBJECT_USE_OBJC + if (socketQueue) dispatch_release(socketQueue); + #endif + socketQueue = NULL; + + LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)delegate +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegate; + } + else + { + __block id result; + + dispatch_sync(socketQueue, ^{ + result = delegate; + }); + + return result; + } +} + +- (void)setDelegate:(id)newDelegate synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + delegate = newDelegate; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegate:(id)newDelegate +{ + [self setDelegate:newDelegate synchronously:NO]; +} + +- (void)synchronouslySetDelegate:(id)newDelegate +{ + [self setDelegate:newDelegate synchronously:YES]; +} + +- (dispatch_queue_t)delegateQueue +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegateQueue; + } + else + { + __block dispatch_queue_t result; + + dispatch_sync(socketQueue, ^{ + result = delegateQueue; + }); + + return result; + } +} + +- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); + #endif + + delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegateQueue:newDelegateQueue synchronously:NO]; +} + +- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegateQueue:newDelegateQueue synchronously:YES]; +} + +- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (delegatePtr) *delegatePtr = delegate; + if (delegateQueuePtr) *delegateQueuePtr = delegateQueue; + } + else + { + __block id dPtr = NULL; + __block dispatch_queue_t dqPtr = NULL; + + dispatch_sync(socketQueue, ^{ + dPtr = delegate; + dqPtr = delegateQueue; + }); + + if (delegatePtr) *delegatePtr = dPtr; + if (delegateQueuePtr) *delegateQueuePtr = dqPtr; + } +} + +- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + + delegate = newDelegate; + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); + #endif + + delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:NO]; +} + +- (void)synchronouslySetDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:YES]; +} + +- (BOOL)isIPv4Enabled +{ + // Note: YES means kIPv4Disabled is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kIPv4Disabled) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kIPv4Disabled) == 0); + }); + + return result; + } +} + +- (void)setIPv4Enabled:(BOOL)flag +{ + // Note: YES means kIPv4Disabled is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kIPv4Disabled; + else + config |= kIPv4Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (BOOL)isIPv6Enabled +{ + // Note: YES means kIPv6Disabled is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kIPv6Disabled) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kIPv6Disabled) == 0); + }); + + return result; + } +} + +- (void)setIPv6Enabled:(BOOL)flag +{ + // Note: YES means kIPv6Disabled is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kIPv6Disabled; + else + config |= kIPv6Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (BOOL)isIPv4PreferredOverIPv6 +{ + // Note: YES means kPreferIPv6 is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kPreferIPv6) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kPreferIPv6) == 0); + }); + + return result; + } +} + +- (void)setIPv4PreferredOverIPv6:(BOOL)flag +{ + // Note: YES means kPreferIPv6 is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kPreferIPv6; + else + config |= kPreferIPv6; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (id)userData +{ + __block id result = nil; + + dispatch_block_t block = ^{ + + result = userData; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (void)setUserData:(id)arbitraryUserData +{ + dispatch_block_t block = ^{ + + if (userData != arbitraryUserData) + { + userData = arbitraryUserData; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Accepting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr +{ + return [self acceptOnInterface:nil port:port error:errPtr]; +} + +- (BOOL)acceptOnInterface:(NSString *)inInterface port:(uint16_t)port error:(NSError **)errPtr +{ + LogTrace(); + + // Just in-case interface parameter is immutable. + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *err = nil; + + // CreateSocket Block + // This block will be invoked within the dispatch block below. + + int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { + + int socketFD = socket(domain, SOCK_STREAM, 0); + + if (socketFD == SOCKET_NULL) + { + NSString *reason = @"Error in socket() function"; + err = [self errnoErrorWithReason:reason]; + + return SOCKET_NULL; + } + + int status; + + // Set socket options + + status = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (status == -1) + { + NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + int reuseOn = 1; + status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + if (status == -1) + { + NSString *reason = @"Error enabling address reuse (setsockopt)"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Bind socket + + status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); + if (status == -1) + { + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Listen + + status = listen(socketFD, 1024); + if (status == -1) + { + NSString *reason = @"Error in listen() function"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + return socketFD; + }; + + // Create dispatch block and run on socketQueue + + dispatch_block_t block = ^{ @autoreleasepool { + + if (delegate == nil) // Must have delegate set + { + NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (![self isDisconnected]) // Must be disconnected + { + NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + // Resolve interface from description + + NSMutableData *interface4 = nil; + NSMutableData *interface6 = nil; + + [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:port]; + + if ((interface4 == nil) && (interface6 == nil)) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv4Disabled && (interface6 == nil)) + { + NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && (interface4 == nil)) + { + NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; + err = [self badParamError:msg]; + + return_from_block; + } + + BOOL enableIPv4 = !isIPv4Disabled && (interface4 != nil); + BOOL enableIPv6 = !isIPv6Disabled && (interface6 != nil); + + // Create sockets, configure, bind, and listen + + if (enableIPv4) + { + LogVerbose(@"Creating IPv4 socket"); + socket4FD = createSocket(AF_INET, interface4); + + if (socket4FD == SOCKET_NULL) + { + return_from_block; + } + } + + if (enableIPv6) + { + LogVerbose(@"Creating IPv6 socket"); + + if (enableIPv4 && (port == 0)) + { + // No specific port was specified, so we allowed the OS to pick an available port for us. + // Now we need to make sure the IPv6 socket listens on the same port as the IPv4 socket. + + struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)[interface6 mutableBytes]; + addr6->sin6_port = htons([self localPort4]); + } + + socket6FD = createSocket(AF_INET6, interface6); + + if (socket6FD == SOCKET_NULL) + { + if (socket4FD != SOCKET_NULL) + { + LogVerbose(@"close(socket4FD)"); + close(socket4FD); + } + + return_from_block; + } + } + + // Create accept sources + + if (enableIPv4) + { + accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue); + + int socketFD = socket4FD; + dispatch_source_t acceptSource = accept4Source; + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(accept4Source, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"event4Block"); + + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); + + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); + + while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); + + #pragma clang diagnostic pop + }}); + + + dispatch_source_set_cancel_handler(accept4Source, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(accept4Source)"); + dispatch_release(acceptSource); + #endif + + LogVerbose(@"close(socket4FD)"); + close(socketFD); + + #pragma clang diagnostic pop + }); + + LogVerbose(@"dispatch_resume(accept4Source)"); + dispatch_resume(accept4Source); + } + + if (enableIPv6) + { + accept6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket6FD, 0, socketQueue); + + int socketFD = socket6FD; + dispatch_source_t acceptSource = accept6Source; + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(accept6Source, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"event6Block"); + + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); + + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); + + while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); + + #pragma clang diagnostic pop + }}); + + dispatch_source_set_cancel_handler(accept6Source, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(accept6Source)"); + dispatch_release(acceptSource); + #endif + + LogVerbose(@"close(socket6FD)"); + close(socketFD); + + #pragma clang diagnostic pop + }); + + LogVerbose(@"dispatch_resume(accept6Source)"); + dispatch_resume(accept6Source); + } + + flags |= kSocketStarted; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + LogInfo(@"Error in accept: %@", err); + + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (BOOL)doAccept:(int)parentSocketFD +{ + LogTrace(); + + BOOL isIPv4; + int childSocketFD; + NSData *childSocketAddress; + + if (parentSocketFD == socket4FD) + { + isIPv4 = YES; + + struct sockaddr_in addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + else // if (parentSocketFD == socket6FD) + { + isIPv4 = NO; + + struct sockaddr_in6 addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + + // Enable non-blocking IO on the socket + + int result = fcntl(childSocketFD, F_SETFL, O_NONBLOCK); + if (result == -1) + { + LogWarn(@"Error enabling non-blocking IO on accepted socket (fcntl)"); + return NO; + } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(childSocketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + // Notify delegate + + if (delegateQueue) + { + __strong id theDelegate = delegate; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + // Query delegate for custom socket queue + + dispatch_queue_t childSocketQueue = NULL; + + if ([theDelegate respondsToSelector:@selector(newSocketQueueForConnectionFromAddress:onSocket:)]) + { + childSocketQueue = [theDelegate newSocketQueueForConnectionFromAddress:childSocketAddress + onSocket:self]; + } + + // Create GCDAsyncSocket instance for accepted socket + + GCDAsyncSocket *acceptedSocket = [[GCDAsyncSocket alloc] initWithDelegate:theDelegate + delegateQueue:delegateQueue + socketQueue:childSocketQueue]; + + if (isIPv4) + acceptedSocket->socket4FD = childSocketFD; + else + acceptedSocket->socket6FD = childSocketFD; + + acceptedSocket->flags = (kSocketStarted | kConnected); + + // Setup read and write sources for accepted socket + + dispatch_async(acceptedSocket->socketQueue, ^{ @autoreleasepool { + + [acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD]; + }}); + + // Notify delegate + + if ([theDelegate respondsToSelector:@selector(socket:didAcceptNewSocket:)]) + { + [theDelegate socket:self didAcceptNewSocket:acceptedSocket]; + } + + // Release the socket queue returned from the delegate (it was retained by acceptedSocket) + #if !OS_OBJECT_USE_OBJC + if (childSocketQueue) dispatch_release(childSocketQueue); + #endif + + // The accepted socket should have been retained by the delegate. + // Otherwise it gets properly released when exiting the block. + }}); + } + + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Connecting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method runs through the various checks required prior to a connection attempt. + * It is shared between the connectToHost and connectToAddress methods. + * +**/ +- (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if (delegate == nil) // Must have delegate set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (![self isDisconnected]) // Must be disconnected + { + if (errPtr) + { + NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + if (errPtr) + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (interface) + { + NSMutableData *interface4 = nil; + NSMutableData *interface6 = nil; + + [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0]; + + if ((interface4 == nil) && (interface6 == nil)) + { + if (errPtr) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + if (isIPv4Disabled && (interface6 == nil)) + { + if (errPtr) + { + NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + if (isIPv6Disabled && (interface4 == nil)) + { + if (errPtr) + { + NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + connectInterface4 = interface4; + connectInterface6 = interface6; + } + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + return YES; +} + +- (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr +{ + return [self connectToHost:host onPort:port withTimeout:-1 error:errPtr]; +} + +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + return [self connectToHost:host onPort:port viaInterface:nil withTimeout:timeout error:errPtr]; +} + +- (BOOL)connectToHost:(NSString *)inHost + onPort:(uint16_t)port + viaInterface:(NSString *)inInterface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + LogTrace(); + + // Just in case immutable objects were passed + NSString *host = [inHost copy]; + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *preConnectErr = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with host parameter + + if ([host length] == 0) + { + NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string."; + preConnectErr = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithInterface:interface error:&preConnectErr]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + flags |= kSocketStarted; + + LogVerbose(@"Dispatching DNS lookup..."); + + // It's possible that the given host parameter is actually a NSMutableString. + // So we want to copy it now, within this block that will be executed synchronously. + // This way the asynchronous lookup block below doesn't have to worry about it changing. + + NSString *hostCpy = [host copy]; + + int aStateIndex = stateIndex; + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + NSError *lookupErr = nil; + NSMutableArray *addresses = [GCDAsyncSocket lookupHost:hostCpy port:port error:&lookupErr]; + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + if (lookupErr) + { + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf lookup:aStateIndex didFail:lookupErr]; + }}); + } + else + { + NSData *address4 = nil; + NSData *address6 = nil; + + for (NSData *address in addresses) + { + if (!address4 && [GCDAsyncSocket isIPv4Address:address]) + { + address4 = address; + } + else if (!address6 && [GCDAsyncSocket isIPv6Address:address]) + { + address6 = address; + } + } + + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6]; + }}); + } + + #pragma clang diagnostic pop + }}); + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + + if (errPtr) *errPtr = preConnectErr; + return result; +} + +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr +{ + return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:-1 error:errPtr]; +} + +- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr +{ + return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:timeout error:errPtr]; +} + +- (BOOL)connectToAddress:(NSData *)inRemoteAddr + viaInterface:(NSString *)inInterface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + LogTrace(); + + // Just in case immutable objects were passed + NSData *remoteAddr = [inRemoteAddr copy]; + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with remoteAddr parameter + + NSData *address4 = nil; + NSData *address6 = nil; + + if ([remoteAddr length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddr = (const struct sockaddr *)[remoteAddr bytes]; + + if (sockaddr->sa_family == AF_INET) + { + if ([remoteAddr length] == sizeof(struct sockaddr_in)) + { + address4 = remoteAddr; + } + } + else if (sockaddr->sa_family == AF_INET6) + { + if ([remoteAddr length] == sizeof(struct sockaddr_in6)) + { + address6 = remoteAddr; + } + } + } + + if ((address4 == nil) && (address6 == nil)) + { + NSString *msg = @"A valid IPv4 or IPv6 address was not given"; + err = [self badParamError:msg]; + + return_from_block; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && (address4 != nil)) + { + NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && (address6 != nil)) + { + NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithInterface:interface error:&err]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + if (![self connectWithAddress4:address4 address6:address6 error:&err]) + { + return_from_block; + } + + flags |= kSocketStarted; + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6 +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert(address4 || address6, @"Expected at least one valid address"); + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring lookupDidSucceed, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + // Check for problems + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && (address6 == nil)) + { + NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + if (isIPv6Disabled && (address4 == nil)) + { + NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + // Start the normal connection process + + NSError *err = nil; + if (![self connectWithAddress4:address4 address6:address6 error:&err]) + { + [self closeWithError:err]; + } +} + +/** + * This method is called if the DNS lookup fails. + * This method is executed on the socketQueue. + * + * Since the DNS lookup executed synchronously on a global concurrent queue, + * the original connection request may have already been cancelled or timed-out by the time this method is invoked. + * The lookupIndex tells us whether the lookup is still valid or not. +**/ +- (void)lookup:(int)aStateIndex didFail:(NSError *)error +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring lookup:didFail: - already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + [self endConnectTimeout]; + [self closeWithError:error]; +} + +- (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + LogVerbose(@"IPv4: %@:%hu", [[self class] hostFromAddress:address4], [[self class] portFromAddress:address4]); + LogVerbose(@"IPv6: %@:%hu", [[self class] hostFromAddress:address6], [[self class] portFromAddress:address6]); + + // Determine socket type + + BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; + + BOOL useIPv6 = ((preferIPv6 && address6) || (address4 == nil)); + + // Create the socket + + int socketFD; + NSData *address; + NSData *connectInterface; + + if (useIPv6) + { + LogVerbose(@"Creating IPv6 socket"); + + socket6FD = socket(AF_INET6, SOCK_STREAM, 0); + + socketFD = socket6FD; + address = address6; + connectInterface = connectInterface6; + } + else + { + LogVerbose(@"Creating IPv4 socket"); + + socket4FD = socket(AF_INET, SOCK_STREAM, 0); + + socketFD = socket4FD; + address = address4; + connectInterface = connectInterface4; + } + + if (socketFD == SOCKET_NULL) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; + + return NO; + } + + // Bind the socket to the desired interface (if needed) + + if (connectInterface) + { + LogVerbose(@"Binding socket..."); + + if ([[self class] portFromAddress:connectInterface] > 0) + { + // Since we're going to be binding to a specific port, + // we should turn on reuseaddr to allow us to override sockets in time_wait. + + int reuseOn = 1; + setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + } + + const struct sockaddr *interfaceAddr = (const struct sockaddr *)[connectInterface bytes]; + + int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]); + if (result != 0) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in bind() function"]; + + return NO; + } + } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + // Start the connection process in a background queue + + int aStateIndex = stateIndex; + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]); + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + if (result == 0) + { + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf didConnect:aStateIndex]; + }}); + } + else + { + NSError *error = [strongSelf errnoErrorWithReason:@"Error in connect() function"]; + + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf didNotConnect:aStateIndex error:error]; + }}); + } + + #pragma clang diagnostic pop + }); + + LogVerbose(@"Connecting..."); + + return YES; +} + +- (void)didConnect:(int)aStateIndex +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring didConnect, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + flags |= kConnected; + + [self endConnectTimeout]; + + #if TARGET_OS_IPHONE + // The endConnectTimeout method executed above incremented the stateIndex. + aStateIndex = stateIndex; + #endif + + // Setup read/write streams (as workaround for specific shortcomings in the iOS platform) + // + // Note: + // There may be configuration options that must be set by the delegate before opening the streams. + // The primary example is the kCFStreamNetworkServiceTypeVoIP flag, which only works on an unopened stream. + // + // Thus we wait until after the socket:didConnectToHost:port: delegate method has completed. + // This gives the delegate time to properly configure the streams if needed. + + dispatch_block_t SetupStreamsPart1 = ^{ + #if TARGET_OS_IPHONE + + if (![self createReadAndWriteStream]) + { + [self closeWithError:[self otherError:@"Error creating CFStreams"]]; + return; + } + + if (![self registerForStreamCallbacksIncludingReadWrite:NO]) + { + [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; + return; + } + + #endif + }; + dispatch_block_t SetupStreamsPart2 = ^{ + #if TARGET_OS_IPHONE + + if (aStateIndex != stateIndex) + { + // The socket has been disconnected. + return; + } + + if (![self addStreamsToRunLoop]) + { + [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; + return; + } + + if (![self openStreams]) + { + [self closeWithError:[self otherError:@"Error creating CFStreams"]]; + return; + } + + #endif + }; + + // Notify delegate + + NSString *host = [self connectedHost]; + uint16_t port = [self connectedPort]; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didConnectToHost:port:)]) + { + SetupStreamsPart1(); + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didConnectToHost:host port:port]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + SetupStreamsPart2(); + }}); + }}); + } + else + { + SetupStreamsPart1(); + SetupStreamsPart2(); + } + + // Get the connected socket + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : socket6FD; + + // Enable non-blocking IO on the socket + + int result = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (result == -1) + { + NSString *errMsg = @"Error enabling non-blocking IO on socket (fcntl)"; + [self closeWithError:[self otherError:errMsg]]; + + return; + } + + // Setup our read/write sources + + [self setupReadAndWriteSourcesForNewlyConnectedSocket:socketFD]; + + // Dequeue any pending read/write requests + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; +} + +- (void)didNotConnect:(int)aStateIndex error:(NSError *)error +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring didNotConnect, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + [self closeWithError:error]; +} + +- (void)startConnectTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + connectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(connectTimer, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doConnectTimeout]; + + #pragma clang diagnostic pop + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theConnectTimer = connectTimer; + dispatch_source_set_cancel_handler(connectTimer, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(connectTimer)"); + dispatch_release(theConnectTimer); + + #pragma clang diagnostic pop + }); + #endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + dispatch_source_set_timer(connectTimer, tt, DISPATCH_TIME_FOREVER, 0); + + dispatch_resume(connectTimer); + } +} + +- (void)endConnectTimeout +{ + LogTrace(); + + if (connectTimer) + { + dispatch_source_cancel(connectTimer); + connectTimer = NULL; + } + + // Increment stateIndex. + // This will prevent us from processing results from any related background asynchronous operations. + // + // Note: This should be called from close method even if connectTimer is NULL. + // This is because one might disconnect a socket prior to a successful connection which had no timeout. + + stateIndex++; + + if (connectInterface4) + { + connectInterface4 = nil; + } + if (connectInterface6) + { + connectInterface6 = nil; + } +} + +- (void)doConnectTimeout +{ + LogTrace(); + + [self endConnectTimeout]; + [self closeWithError:[self connectTimeoutError]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Disconnecting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)closeWithError:(NSError *)error +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + [self endConnectTimeout]; + + if (currentRead != nil) [self endCurrentRead]; + if (currentWrite != nil) [self endCurrentWrite]; + + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + [preBuffer reset]; + + #if TARGET_OS_IPHONE + { + if (readStream || writeStream) + { + [self removeStreamsFromRunLoop]; + + if (readStream) + { + CFReadStreamSetClient(readStream, kCFStreamEventNone, NULL, NULL); + CFReadStreamClose(readStream); + CFRelease(readStream); + readStream = NULL; + } + if (writeStream) + { + CFWriteStreamSetClient(writeStream, kCFStreamEventNone, NULL, NULL); + CFWriteStreamClose(writeStream); + CFRelease(writeStream); + writeStream = NULL; + } + } + } + #endif + + [sslPreBuffer reset]; + sslErrCode = noErr; + + if (sslContext) + { + // Getting a linker error here about the SSLx() functions? + // You need to add the Security Framework to your application. + + SSLClose(sslContext); + + #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) + CFRelease(sslContext); + #else + SSLDisposeContext(sslContext); + #endif + + sslContext = NULL; + } + + // For some crazy reason (in my opinion), cancelling a dispatch source doesn't + // invoke the cancel handler if the dispatch source is paused. + // So we have to unpause the source if needed. + // This allows the cancel handler to be run, which in turn releases the source and closes the socket. + + if (!accept4Source && !accept6Source && !readSource && !writeSource) + { + LogVerbose(@"manually closing close"); + + if (socket4FD != SOCKET_NULL) + { + LogVerbose(@"close(socket4FD)"); + close(socket4FD); + socket4FD = SOCKET_NULL; + } + + if (socket6FD != SOCKET_NULL) + { + LogVerbose(@"close(socket6FD)"); + close(socket6FD); + socket6FD = SOCKET_NULL; + } + } + else + { + if (accept4Source) + { + LogVerbose(@"dispatch_source_cancel(accept4Source)"); + dispatch_source_cancel(accept4Source); + + // We never suspend accept4Source + + accept4Source = NULL; + } + + if (accept6Source) + { + LogVerbose(@"dispatch_source_cancel(accept6Source)"); + dispatch_source_cancel(accept6Source); + + // We never suspend accept6Source + + accept6Source = NULL; + } + + if (readSource) + { + LogVerbose(@"dispatch_source_cancel(readSource)"); + dispatch_source_cancel(readSource); + + [self resumeReadSource]; + + readSource = NULL; + } + + if (writeSource) + { + LogVerbose(@"dispatch_source_cancel(writeSource)"); + dispatch_source_cancel(writeSource); + + [self resumeWriteSource]; + + writeSource = NULL; + } + + // The sockets will be closed by the cancel handlers of the corresponding source + + socket4FD = SOCKET_NULL; + socket6FD = SOCKET_NULL; + } + + // If the client has passed the connect/accept method, then the connection has at least begun. + // Notify delegate that it is now ending. + BOOL shouldCallDelegate = (flags & kSocketStarted) ? YES : NO; + BOOL isDeallocating = (flags & kDealloc) ? YES : NO; + + // Clear stored socket info and all flags (config remains as is) + socketFDBytesAvailable = 0; + flags = 0; + + if (shouldCallDelegate) + { + __strong id theDelegate = delegate; + __strong id theSelf = isDeallocating ? nil : self; + + if (delegateQueue && [theDelegate respondsToSelector: @selector(socketDidDisconnect:withError:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidDisconnect:theSelf withError:error]; + }}); + } + } +} + +- (void)disconnect +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + [self closeWithError:nil]; + } + }}; + + // Synchronous disconnection, as documented in the header file + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); +} + +- (void)disconnectAfterReading +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + flags |= (kForbidReadsWrites | kDisconnectAfterReads); + [self maybeClose]; + } + }}); +} + +- (void)disconnectAfterWriting +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + flags |= (kForbidReadsWrites | kDisconnectAfterWrites); + [self maybeClose]; + } + }}); +} + +- (void)disconnectAfterReadingAndWriting +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + flags |= (kForbidReadsWrites | kDisconnectAfterReads | kDisconnectAfterWrites); + [self maybeClose]; + } + }}); +} + +/** + * Closes the socket if possible. + * That is, if all writes have completed, and we're set to disconnect after writing, + * or if all reads have completed, and we're set to disconnect after reading. +**/ +- (void)maybeClose +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + BOOL shouldClose = NO; + + if (flags & kDisconnectAfterReads) + { + if (([readQueue count] == 0) && (currentRead == nil)) + { + if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + shouldClose = YES; + } + } + else + { + shouldClose = YES; + } + } + } + else if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + shouldClose = YES; + } + } + + if (shouldClose) + { + [self closeWithError:nil]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Errors +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSError *)badConfigError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadConfigError userInfo:userInfo]; +} + +- (NSError *)badParamError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo]; +} + ++ (NSError *)gaiError:(int)gai_error +{ + NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo]; +} + +- (NSError *)errnoErrorWithReason:(NSString *)reason +{ + NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:errMsg, NSLocalizedDescriptionKey, + reason, NSLocalizedFailureReasonErrorKey, nil]; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; +} + +- (NSError *)errnoError +{ + NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; +} + +- (NSError *)sslError:(OSStatus)ssl_error +{ + NSString *msg = @"Error code definition can be found in Apple's SecureTransport.h"; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:msg forKey:NSLocalizedRecoverySuggestionErrorKey]; + + return [NSError errorWithDomain:@"kCFStreamErrorDomainSSL" code:ssl_error userInfo:userInfo]; +} + +- (NSError *)connectTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketConnectTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Attempt to connect to host timed out", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketConnectTimeoutError userInfo:userInfo]; +} + +/** + * Returns a standard AsyncSocket maxed out error. +**/ +- (NSError *)readMaxedOutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadMaxedOutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Read operation reached set maximum length", nil); + + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadMaxedOutError userInfo:info]; +} + +/** + * Returns a standard AsyncSocket write timeout error. +**/ +- (NSError *)readTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Read operation timed out", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadTimeoutError userInfo:userInfo]; +} + +/** + * Returns a standard AsyncSocket write timeout error. +**/ +- (NSError *)writeTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketWriteTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Write operation timed out", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketWriteTimeoutError userInfo:userInfo]; +} + +- (NSError *)connectionClosedError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketClosedError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Socket closed by remote peer", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketClosedError userInfo:userInfo]; +} + +- (NSError *)otherError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Diagnostics +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isDisconnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (flags & kSocketStarted) ? NO : YES; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isConnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (flags & kConnected) ? YES : NO; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (NSString *)connectedHost +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self connectedHostFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self connectedHostFromSocket6:socket6FD]; + + return nil; + } + else + { + __block NSString *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (socket4FD != SOCKET_NULL) + result = [self connectedHostFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self connectedHostFromSocket6:socket6FD]; + }}); + + return result; + } +} + +- (uint16_t)connectedPort +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self connectedPortFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self connectedPortFromSocket6:socket6FD]; + + return 0; + } + else + { + __block uint16_t result = 0; + + dispatch_sync(socketQueue, ^{ + // No need for autorelease pool + + if (socket4FD != SOCKET_NULL) + result = [self connectedPortFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self connectedPortFromSocket6:socket6FD]; + }); + + return result; + } +} + +- (NSString *)localHost +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self localHostFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self localHostFromSocket6:socket6FD]; + + return nil; + } + else + { + __block NSString *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (socket4FD != SOCKET_NULL) + result = [self localHostFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self localHostFromSocket6:socket6FD]; + }}); + + return result; + } +} + +- (uint16_t)localPort +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self localPortFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self localPortFromSocket6:socket6FD]; + + return 0; + } + else + { + __block uint16_t result = 0; + + dispatch_sync(socketQueue, ^{ + // No need for autorelease pool + + if (socket4FD != SOCKET_NULL) + result = [self localPortFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self localPortFromSocket6:socket6FD]; + }); + + return result; + } +} + +- (NSString *)connectedHost4 +{ + if (socket4FD != SOCKET_NULL) + return [self connectedHostFromSocket4:socket4FD]; + + return nil; +} + +- (NSString *)connectedHost6 +{ + if (socket6FD != SOCKET_NULL) + return [self connectedHostFromSocket6:socket6FD]; + + return nil; +} + +- (uint16_t)connectedPort4 +{ + if (socket4FD != SOCKET_NULL) + return [self connectedPortFromSocket4:socket4FD]; + + return 0; +} + +- (uint16_t)connectedPort6 +{ + if (socket6FD != SOCKET_NULL) + return [self connectedPortFromSocket6:socket6FD]; + + return 0; +} + +- (NSString *)localHost4 +{ + if (socket4FD != SOCKET_NULL) + return [self localHostFromSocket4:socket4FD]; + + return nil; +} + +- (NSString *)localHost6 +{ + if (socket6FD != SOCKET_NULL) + return [self localHostFromSocket6:socket6FD]; + + return nil; +} + +- (uint16_t)localPort4 +{ + if (socket4FD != SOCKET_NULL) + return [self localPortFromSocket4:socket4FD]; + + return 0; +} + +- (uint16_t)localPort6 +{ + if (socket6FD != SOCKET_NULL) + return [self localPortFromSocket6:socket6FD]; + + return 0; +} + +- (NSString *)connectedHostFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr4:&sockaddr4]; +} + +- (NSString *)connectedHostFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr6:&sockaddr6]; +} + +- (uint16_t)connectedPortFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr4:&sockaddr4]; +} + +- (uint16_t)connectedPortFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr6:&sockaddr6]; +} + +- (NSString *)localHostFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr4:&sockaddr4]; +} + +- (NSString *)localHostFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr6:&sockaddr6]; +} + +- (uint16_t)localPortFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr4:&sockaddr4]; +} + +- (uint16_t)localPortFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr6:&sockaddr6]; +} + +- (NSData *)connectedAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + if (socket4FD != SOCKET_NULL) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; + } + } + + if (socket6FD != SOCKET_NULL) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; + } + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (NSData *)localAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + if (socket4FD != SOCKET_NULL) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; + } + } + + if (socket6FD != SOCKET_NULL) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; + } + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isIPv4 +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (socket4FD != SOCKET_NULL); + } + else + { + __block BOOL result = NO; + + dispatch_sync(socketQueue, ^{ + result = (socket4FD != SOCKET_NULL); + }); + + return result; + } +} + +- (BOOL)isIPv6 +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (socket6FD != SOCKET_NULL); + } + else + { + __block BOOL result = NO; + + dispatch_sync(socketQueue, ^{ + result = (socket6FD != SOCKET_NULL); + }); + + return result; + } +} + +- (BOOL)isSecure +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (flags & kSocketSecure) ? YES : NO; + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = (flags & kSocketSecure) ? YES : NO; + }); + + return result; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Finds the address of an interface description. + * An inteface description may be an interface name (en0, en1, lo0) or corresponding IP (192.168.4.34). + * + * The interface description may optionally contain a port number at the end, separated by a colon. + * If a non-zero port parameter is provided, any port number in the interface description is ignored. + * + * The returned value is a 'struct sockaddr' wrapped in an NSMutableData object. +**/ +- (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr + address6:(NSMutableData **)interfaceAddr6Ptr + fromDescription:(NSString *)interfaceDescription + port:(uint16_t)port +{ + NSMutableData *addr4 = nil; + NSMutableData *addr6 = nil; + + NSString *interface = nil; + + NSArray *components = [interfaceDescription componentsSeparatedByString:@":"]; + if ([components count] > 0) + { + NSString *temp = [components objectAtIndex:0]; + if ([temp length] > 0) + { + interface = temp; + } + } + if ([components count] > 1 && port == 0) + { + long portL = strtol([[components objectAtIndex:1] UTF8String], NULL, 10); + + if (portL > 0 && portL <= UINT16_MAX) + { + port = (uint16_t)portL; + } + } + + if (interface == nil) + { + // ANY address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(sockaddr4); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(sockaddr6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_any; + + addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else if ([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"]) + { + // LOOPBACK address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(sockaddr4); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(sockaddr6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_loopback; + + addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else + { + const char *iface = [interface UTF8String]; + + struct ifaddrs *addrs; + const struct ifaddrs *cursor; + + if ((getifaddrs(&addrs) == 0)) + { + cursor = addrs; + while (cursor != NULL) + { + if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) + { + // IPv4 + + struct sockaddr_in nativeAddr4; + memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4)); + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + nativeAddr4.sin_port = htons(port); + + addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + else + { + char ip[INET_ADDRSTRLEN]; + + const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + nativeAddr4.sin_port = htons(port); + + addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + } + } + else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) + { + // IPv6 + + struct sockaddr_in6 nativeAddr6; + memcpy(&nativeAddr6, cursor->ifa_addr, sizeof(nativeAddr6)); + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + else + { + char ip[INET6_ADDRSTRLEN]; + + const char *conversion = inet_ntop(AF_INET6, &nativeAddr6.sin6_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + } + } + + cursor = cursor->ifa_next; + } + + freeifaddrs(addrs); + } + } + + if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; + if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; +} + +- (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD +{ + readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketFD, 0, socketQueue); + writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socketFD, 0, socketQueue); + + // Setup event handlers + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(readSource, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"readEventBlock"); + + strongSelf->socketFDBytesAvailable = dispatch_source_get_data(strongSelf->readSource); + LogVerbose(@"socketFDBytesAvailable: %lu", strongSelf->socketFDBytesAvailable); + + if (strongSelf->socketFDBytesAvailable > 0) + [strongSelf doReadData]; + else + [strongSelf doReadEOF]; + + #pragma clang diagnostic pop + }}); + + dispatch_source_set_event_handler(writeSource, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"writeEventBlock"); + + strongSelf->flags |= kSocketCanAcceptBytes; + [strongSelf doWriteData]; + + #pragma clang diagnostic pop + }}); + + // Setup cancel handlers + + __block int socketFDRefCount = 2; + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theReadSource = readSource; + dispatch_source_t theWriteSource = writeSource; + #endif + + dispatch_source_set_cancel_handler(readSource, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"readCancelBlock"); + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(readSource)"); + dispatch_release(theReadSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socketFD)"); + close(socketFD); + } + + #pragma clang diagnostic pop + }); + + dispatch_source_set_cancel_handler(writeSource, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"writeCancelBlock"); + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(writeSource)"); + dispatch_release(theWriteSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socketFD)"); + close(socketFD); + } + + #pragma clang diagnostic pop + }); + + // We will not be able to read until data arrives. + // But we should be able to write immediately. + + socketFDBytesAvailable = 0; + flags &= ~kReadSourceSuspended; + + LogVerbose(@"dispatch_resume(readSource)"); + dispatch_resume(readSource); + + flags |= kSocketCanAcceptBytes; + flags |= kWriteSourceSuspended; +} + +- (BOOL)usingCFStreamForTLS +{ + #if TARGET_OS_IPHONE + + if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) + { + // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. + + return YES; + } + + #endif + + return NO; +} + +- (BOOL)usingSecureTransportForTLS +{ + // Invoking this method is equivalent to ![self usingCFStreamForTLS] (just more readable) + + #if TARGET_OS_IPHONE + + if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) + { + // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. + + return NO; + } + + #endif + + return YES; +} + +- (void)suspendReadSource +{ + if (!(flags & kReadSourceSuspended)) + { + LogVerbose(@"dispatch_suspend(readSource)"); + + dispatch_suspend(readSource); + flags |= kReadSourceSuspended; + } +} + +- (void)resumeReadSource +{ + if (flags & kReadSourceSuspended) + { + LogVerbose(@"dispatch_resume(readSource)"); + + dispatch_resume(readSource); + flags &= ~kReadSourceSuspended; + } +} + +- (void)suspendWriteSource +{ + if (!(flags & kWriteSourceSuspended)) + { + LogVerbose(@"dispatch_suspend(writeSource)"); + + dispatch_suspend(writeSource); + flags |= kWriteSourceSuspended; + } +} + +- (void)resumeWriteSource +{ + if (flags & kWriteSourceSuspended) + { + LogVerbose(@"dispatch_resume(writeSource)"); + + dispatch_resume(writeSource); + flags &= ~kWriteSourceSuspended; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Reading +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag +{ + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:length + timeout:timeout + readLength:0 + terminator:nil + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToLength:length withTimeout:timeout buffer:nil bufferOffset:0 tag:tag]; +} + +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + if (length == 0) { + LogWarn(@"Cannot read: length == 0"); + return; + } + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:0 + timeout:timeout + readLength:length + terminator:nil + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; +} + +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:length tag:tag]; +} + +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)maxLength + tag:(long)tag +{ + if ([data length] == 0) { + LogWarn(@"Cannot read: [data length] == 0"); + return; + } + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + if (maxLength > 0 && maxLength < [data length]) { + LogWarn(@"Cannot read: maxLength > 0 && maxLength < [data length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:maxLength + timeout:timeout + readLength:0 + terminator:data + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr +{ + __block float result = 0.0F; + + dispatch_block_t block = ^{ + + if (!currentRead || ![currentRead isKindOfClass:[GCDAsyncReadPacket class]]) + { + // We're not reading anything right now. + + if (tagPtr != NULL) *tagPtr = 0; + if (donePtr != NULL) *donePtr = 0; + if (totalPtr != NULL) *totalPtr = 0; + + result = NAN; + } + else + { + // It's only possible to know the progress of our read if we're reading to a certain length. + // If we're reading to data, we of course have no idea when the data will arrive. + // If we're reading to timeout, then we have no idea when the next chunk of data will arrive. + + NSUInteger done = currentRead->bytesDone; + NSUInteger total = currentRead->readLength; + + if (tagPtr != NULL) *tagPtr = currentRead->tag; + if (donePtr != NULL) *donePtr = done; + if (totalPtr != NULL) *totalPtr = total; + + if (total > 0) + result = (float)done / (float)total; + else + result = 1.0F; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +/** + * This method starts a new read, if needed. + * + * It is called when: + * - a user requests a read + * - after a read request has finished (to handle the next request) + * - immediately after the socket opens to handle any pending requests + * + * This method also handles auto-disconnect post read/write completion. +**/ +- (void)maybeDequeueRead +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + // If we're not currently processing a read AND we have an available read stream + if ((currentRead == nil) && (flags & kConnected)) + { + if ([readQueue count] > 0) + { + // Dequeue the next object in the write queue + currentRead = [readQueue objectAtIndex:0]; + [readQueue removeObjectAtIndex:0]; + + + if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]]) + { + LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); + + // Attempt to start TLS + flags |= kStartingReadTLS; + + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set + [self maybeStartTLS]; + } + else + { + LogVerbose(@"Dequeued GCDAsyncReadPacket"); + + // Setup read timer (if needed) + [self setupReadTimerWithTimeout:currentRead->timeout]; + + // Immediately read, if possible + [self doReadData]; + } + } + else if (flags & kDisconnectAfterReads) + { + if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + [self closeWithError:nil]; + } + } + else + { + [self closeWithError:nil]; + } + } + else if (flags & kSocketSecure) + { + [self flushSSLBuffers]; + + // Edge case: + // + // We just drained all data from the ssl buffers, + // and all known data from the socket (socketFDBytesAvailable). + // + // If we didn't get any data from this process, + // then we may have reached the end of the TCP stream. + // + // Be sure callbacks are enabled so we're notified about a disconnection. + + if ([preBuffer availableBytes] == 0) + { + if ([self usingCFStreamForTLS]) { + // Callbacks never disabled + } + else { + [self resumeReadSource]; + } + } + } + } +} + +- (void)flushSSLBuffers +{ + LogTrace(); + + NSAssert((flags & kSocketSecure), @"Cannot flush ssl buffers on non-secure socket"); + + if ([preBuffer availableBytes] > 0) + { + // Only flush the ssl buffers if the prebuffer is empty. + // This is to avoid growing the prebuffer inifinitely large. + + return; + } + + #if TARGET_OS_IPHONE + + if ([self usingCFStreamForTLS]) + { + if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) + { + LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); + + CFIndex defaultBytesToRead = (1024 * 4); + + [preBuffer ensureCapacityForWrite:defaultBytesToRead]; + + uint8_t *buffer = [preBuffer writeBuffer]; + + CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead); + LogVerbose(@"%@ - CFReadStreamRead(): result = %i", THIS_METHOD, (int)result); + + if (result > 0) + { + [preBuffer didWrite:result]; + } + + flags &= ~kSecureSocketHasBytesAvailable; + } + + return; + } + + #endif + + __block NSUInteger estimatedBytesAvailable = 0; + + dispatch_block_t updateEstimatedBytesAvailable = ^{ + + // Figure out if there is any data available to be read + // + // socketFDBytesAvailable <- Number of encrypted bytes we haven't read from the bsd socket + // [sslPreBuffer availableBytes] <- Number of encrypted bytes we've buffered from bsd socket + // sslInternalBufSize <- Number of decrypted bytes SecureTransport has buffered + // + // We call the variable "estimated" because we don't know how many decrypted bytes we'll get + // from the encrypted bytes in the sslPreBuffer. + // However, we do know this is an upper bound on the estimation. + + estimatedBytesAvailable = socketFDBytesAvailable + [sslPreBuffer availableBytes]; + + size_t sslInternalBufSize = 0; + SSLGetBufferedReadSize(sslContext, &sslInternalBufSize); + + estimatedBytesAvailable += sslInternalBufSize; + }; + + updateEstimatedBytesAvailable(); + + if (estimatedBytesAvailable > 0) + { + LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); + + BOOL done = NO; + do + { + LogVerbose(@"%@ - estimatedBytesAvailable = %lu", THIS_METHOD, (unsigned long)estimatedBytesAvailable); + + // Make sure there's enough room in the prebuffer + + [preBuffer ensureCapacityForWrite:estimatedBytesAvailable]; + + // Read data into prebuffer + + uint8_t *buffer = [preBuffer writeBuffer]; + size_t bytesRead = 0; + + OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead); + LogVerbose(@"%@ - read from secure socket = %u", THIS_METHOD, (unsigned)bytesRead); + + if (bytesRead > 0) + { + [preBuffer didWrite:bytesRead]; + } + + LogVerbose(@"%@ - prebuffer.length = %zu", THIS_METHOD, [preBuffer availableBytes]); + + if (result != noErr) + { + done = YES; + } + else + { + updateEstimatedBytesAvailable(); + } + + } while (!done && estimatedBytesAvailable > 0); + } +} + +- (void)doReadData +{ + LogTrace(); + + // This method is called on the socketQueue. + // It might be called directly, or via the readSource when data is available to be read. + + if ((currentRead == nil) || (flags & kReadsPaused)) + { + LogVerbose(@"No currentRead or kReadsPaused"); + + // Unable to read at this time + + if (flags & kSocketSecure) + { + // Here's the situation: + // + // We have an established secure connection. + // There may not be a currentRead, but there might be encrypted data sitting around for us. + // When the user does get around to issuing a read, that encrypted data will need to be decrypted. + // + // So why make the user wait? + // We might as well get a head start on decrypting some data now. + // + // The other reason we do this has to do with detecting a socket disconnection. + // The SSL/TLS protocol has it's own disconnection handshake. + // So when a secure socket is closed, a "goodbye" packet comes across the wire. + // We want to make sure we read the "goodbye" packet so we can properly detect the TCP disconnection. + + [self flushSSLBuffers]; + } + + if ([self usingCFStreamForTLS]) + { + // CFReadStream only fires once when there is available data. + // It won't fire again until we've invoked CFReadStreamRead. + } + else + { + // If the readSource is firing, we need to pause it + // or else it will continue to fire over and over again. + // + // If the readSource is not firing, + // we want it to continue monitoring the socket. + + if (socketFDBytesAvailable > 0) + { + [self suspendReadSource]; + } + } + return; + } + + BOOL hasBytesAvailable = NO; + unsigned long estimatedBytesAvailable = 0; + + if ([self usingCFStreamForTLS]) + { + #if TARGET_OS_IPHONE + + // Requested CFStream, rather than SecureTransport, for TLS (via GCDAsyncSocketUseCFStreamForTLS) + + estimatedBytesAvailable = 0; + if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) + hasBytesAvailable = YES; + else + hasBytesAvailable = NO; + + #endif + } + else + { + estimatedBytesAvailable = socketFDBytesAvailable; + + if (flags & kSocketSecure) + { + // There are 2 buffers to be aware of here. + // + // We are using SecureTransport, a TLS/SSL security layer which sits atop TCP. + // We issue a read to the SecureTranport API, which in turn issues a read to our SSLReadFunction. + // Our SSLReadFunction then reads from the BSD socket and returns the encrypted data to SecureTransport. + // SecureTransport then decrypts the data, and finally returns the decrypted data back to us. + // + // The first buffer is one we create. + // SecureTransport often requests small amounts of data. + // This has to do with the encypted packets that are coming across the TCP stream. + // But it's non-optimal to do a bunch of small reads from the BSD socket. + // So our SSLReadFunction reads all available data from the socket (optimizing the sys call) + // and may store excess in the sslPreBuffer. + + estimatedBytesAvailable += [sslPreBuffer availableBytes]; + + // The second buffer is within SecureTransport. + // As mentioned earlier, there are encrypted packets coming across the TCP stream. + // SecureTransport needs the entire packet to decrypt it. + // But if the entire packet produces X bytes of decrypted data, + // and we only asked SecureTransport for X/2 bytes of data, + // it must store the extra X/2 bytes of decrypted data for the next read. + // + // The SSLGetBufferedReadSize function will tell us the size of this internal buffer. + // From the documentation: + // + // "This function does not block or cause any low-level read operations to occur." + + size_t sslInternalBufSize = 0; + SSLGetBufferedReadSize(sslContext, &sslInternalBufSize); + + estimatedBytesAvailable += sslInternalBufSize; + } + + hasBytesAvailable = (estimatedBytesAvailable > 0); + } + + if ((hasBytesAvailable == NO) && ([preBuffer availableBytes] == 0)) + { + LogVerbose(@"No data available to read..."); + + // No data available to read. + + if (![self usingCFStreamForTLS]) + { + // Need to wait for readSource to fire and notify us of + // available data in the socket's internal read buffer. + + [self resumeReadSource]; + } + return; + } + + if (flags & kStartingReadTLS) + { + LogVerbose(@"Waiting for SSL/TLS handshake to complete"); + + // The readQueue is waiting for SSL/TLS handshake to complete. + + if (flags & kStartingWriteTLS) + { + if ([self usingSecureTransportForTLS]) + { + // We are in the process of a SSL Handshake. + // We were waiting for incoming data which has just arrived. + + [self ssl_continueSSLHandshake]; + } + } + else + { + // We are still waiting for the writeQueue to drain and start the SSL/TLS process. + // We now know data is available to read. + + if (![self usingCFStreamForTLS]) + { + // Suspend the read source or else it will continue to fire nonstop. + + [self suspendReadSource]; + } + } + + return; + } + + BOOL done = NO; // Completed read operation + NSError *error = nil; // Error occured + + NSUInteger totalBytesReadForCurrentRead = 0; + + // + // STEP 1 - READ FROM PREBUFFER + // + + if ([preBuffer availableBytes] > 0) + { + // There are 3 types of read packets: + // + // 1) Read all available data. + // 2) Read a specific length of data. + // 3) Read up to a particular terminator. + + NSUInteger bytesToCopy; + + if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; + } + else + { + // Read type #1 or #2 + + bytesToCopy = [currentRead readLengthForNonTermWithHint:[preBuffer availableBytes]]; + } + + // Make sure we have enough room in the buffer for our read. + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; + + // Copy bytes from prebuffer into packet buffer + + uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(buffer, [preBuffer readBuffer], bytesToCopy); + + // Remove the copied bytes from the preBuffer + [preBuffer didRead:bytesToCopy]; + + LogVerbose(@"copied(%lu) preBufferLength(%zu)", (unsigned long)bytesToCopy, [preBuffer availableBytes]); + + // Update totals + + currentRead->bytesDone += bytesToCopy; + totalBytesReadForCurrentRead += bytesToCopy; + + // Check to see if the read operation is done + + if (currentRead->readLength > 0) + { + // Read type #2 - read a specific length of data + + done = (currentRead->bytesDone == currentRead->readLength); + } + else if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method + + if (!done && currentRead->maxLength > 0) + { + // We're not done and there's a set maxLength. + // Have we reached that maxLength yet? + + if (currentRead->bytesDone >= currentRead->maxLength) + { + error = [self readMaxedOutError]; + } + } + } + else + { + // Read type #1 - read all available data + // + // We're done as soon as + // - we've read all available data (in prebuffer and socket) + // - we've read the maxLength of read packet. + + done = ((currentRead->maxLength > 0) && (currentRead->bytesDone == currentRead->maxLength)); + } + + } + + // + // STEP 2 - READ FROM SOCKET + // + + BOOL socketEOF = (flags & kSocketHasReadEOF) ? YES : NO; // Nothing more to read via socket (end of file) + BOOL waiting = !done && !error && !socketEOF && !hasBytesAvailable; // Ran out of data, waiting for more + + if (!done && !error && !socketEOF && hasBytesAvailable) + { + NSAssert(([preBuffer availableBytes] == 0), @"Invalid logic"); + + BOOL readIntoPreBuffer = NO; + uint8_t *buffer = NULL; + size_t bytesRead = 0; + + if (flags & kSocketSecure) + { + if ([self usingCFStreamForTLS]) + { + #if TARGET_OS_IPHONE + + // Using CFStream, rather than SecureTransport, for TLS + + NSUInteger defaultReadLength = (1024 * 32); + + NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength + shouldPreBuffer:&readIntoPreBuffer]; + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // Read data into buffer + + CFIndex result = CFReadStreamRead(readStream, buffer, (CFIndex)bytesToRead); + LogVerbose(@"CFReadStreamRead(): result = %i", (int)result); + + if (result < 0) + { + error = (__bridge_transfer NSError *)CFReadStreamCopyError(readStream); + } + else if (result == 0) + { + socketEOF = YES; + } + else + { + waiting = YES; + bytesRead = (size_t)result; + } + + // We only know how many decrypted bytes were read. + // The actual number of bytes read was likely more due to the overhead of the encryption. + // So we reset our flag, and rely on the next callback to alert us of more data. + flags &= ~kSecureSocketHasBytesAvailable; + + #endif + } + else + { + // Using SecureTransport for TLS + // + // We know: + // - how many bytes are available on the socket + // - how many encrypted bytes are sitting in the sslPreBuffer + // - how many decypted bytes are sitting in the sslContext + // + // But we do NOT know: + // - how many encypted bytes are sitting in the sslContext + // + // So we play the regular game of using an upper bound instead. + + NSUInteger defaultReadLength = (1024 * 32); + + if (defaultReadLength < estimatedBytesAvailable) { + defaultReadLength = estimatedBytesAvailable + (1024 * 16); + } + + NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength + shouldPreBuffer:&readIntoPreBuffer]; + + if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t + bytesToRead = SIZE_MAX; + } + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // The documentation from Apple states: + // + // "a read operation might return errSSLWouldBlock, + // indicating that less data than requested was actually transferred" + // + // However, starting around 10.7, the function will sometimes return noErr, + // even if it didn't read as much data as requested. So we need to watch out for that. + + OSStatus result; + do + { + void *loop_buffer = buffer + bytesRead; + size_t loop_bytesToRead = (size_t)bytesToRead - bytesRead; + size_t loop_bytesRead = 0; + + result = SSLRead(sslContext, loop_buffer, loop_bytesToRead, &loop_bytesRead); + LogVerbose(@"read from secure socket = %u", (unsigned)loop_bytesRead); + + bytesRead += loop_bytesRead; + + } while ((result == noErr) && (bytesRead < bytesToRead)); + + + if (result != noErr) + { + if (result == errSSLWouldBlock) + waiting = YES; + else + { + if (result == errSSLClosedGraceful || result == errSSLClosedAbort) + { + // We've reached the end of the stream. + // Handle this the same way we would an EOF from the socket. + socketEOF = YES; + sslErrCode = result; + } + else + { + error = [self sslError:result]; + } + } + // It's possible that bytesRead > 0, even if the result was errSSLWouldBlock. + // This happens when the SSLRead function is able to read some data, + // but not the entire amount we requested. + + if (bytesRead <= 0) + { + bytesRead = 0; + } + } + + // Do not modify socketFDBytesAvailable. + // It will be updated via the SSLReadFunction(). + } + } + else + { + // Normal socket operation + + NSUInteger bytesToRead; + + // There are 3 types of read packets: + // + // 1) Read all available data. + // 2) Read a specific length of data. + // 3) Read up to a particular terminator. + + if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable + shouldPreBuffer:&readIntoPreBuffer]; + } + else + { + // Read type #1 or #2 + + bytesToRead = [currentRead readLengthForNonTermWithHint:estimatedBytesAvailable]; + } + + if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t (read param 3) + bytesToRead = SIZE_MAX; + } + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // Read data into buffer + + int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD; + + ssize_t result = read(socketFD, buffer, (size_t)bytesToRead); + LogVerbose(@"read from socket = %i", (int)result); + + if (result < 0) + { + if (errno == EWOULDBLOCK) + waiting = YES; + else + error = [self errnoErrorWithReason:@"Error in read() function"]; + + socketFDBytesAvailable = 0; + } + else if (result == 0) + { + socketEOF = YES; + socketFDBytesAvailable = 0; + } + else + { + bytesRead = result; + + if (bytesRead < bytesToRead) + { + // The read returned less data than requested. + // This means socketFDBytesAvailable was a bit off due to timing, + // because we read from the socket right when the readSource event was firing. + socketFDBytesAvailable = 0; + } + else + { + if (socketFDBytesAvailable <= bytesRead) + socketFDBytesAvailable = 0; + else + socketFDBytesAvailable -= bytesRead; + } + + if (socketFDBytesAvailable == 0) + { + waiting = YES; + } + } + } + + if (bytesRead > 0) + { + // Check to see if the read operation is done + + if (currentRead->readLength > 0) + { + // Read type #2 - read a specific length of data + // + // Note: We should never be using a prebuffer when we're reading a specific length of data. + + NSAssert(readIntoPreBuffer == NO, @"Invalid logic"); + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + + done = (currentRead->bytesDone == currentRead->readLength); + } + else if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + if (readIntoPreBuffer) + { + // We just read a big chunk of data into the preBuffer + + [preBuffer didWrite:bytesRead]; + LogVerbose(@"read data into preBuffer - preBuffer.length = %zu", [preBuffer availableBytes]); + + // Search for the terminating sequence + + NSUInteger bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; + LogVerbose(@"copying %lu bytes from preBuffer", (unsigned long)bytesToCopy); + + // Ensure there's room on the read packet's buffer + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; + + // Copy bytes from prebuffer into read buffer + + uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(readBuf, [preBuffer readBuffer], bytesToCopy); + + // Remove the copied bytes from the prebuffer + [preBuffer didRead:bytesToCopy]; + LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); + + // Update totals + currentRead->bytesDone += bytesToCopy; + totalBytesReadForCurrentRead += bytesToCopy; + + // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method above + } + else + { + // We just read a big chunk of data directly into the packet's buffer. + // We need to move any overflow into the prebuffer. + + NSInteger overflow = [currentRead searchForTermAfterPreBuffering:bytesRead]; + + if (overflow == 0) + { + // Perfect match! + // Every byte we read stays in the read buffer, + // and the last byte we read was the last byte of the term. + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + done = YES; + } + else if (overflow > 0) + { + // The term was found within the data that we read, + // and there are extra bytes that extend past the end of the term. + // We need to move these excess bytes out of the read packet and into the prebuffer. + + NSInteger underflow = bytesRead - overflow; + + // Copy excess data into preBuffer + + LogVerbose(@"copying %ld overflow bytes into preBuffer", (long)overflow); + [preBuffer ensureCapacityForWrite:overflow]; + + uint8_t *overflowBuffer = buffer + underflow; + memcpy([preBuffer writeBuffer], overflowBuffer, overflow); + + [preBuffer didWrite:overflow]; + LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); + + // Note: The completeCurrentRead method will trim the buffer for us. + + currentRead->bytesDone += underflow; + totalBytesReadForCurrentRead += underflow; + done = YES; + } + else + { + // The term was not found within the data that we read. + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + done = NO; + } + } + + if (!done && currentRead->maxLength > 0) + { + // We're not done and there's a set maxLength. + // Have we reached that maxLength yet? + + if (currentRead->bytesDone >= currentRead->maxLength) + { + error = [self readMaxedOutError]; + } + } + } + else + { + // Read type #1 - read all available data + + if (readIntoPreBuffer) + { + // We just read a chunk of data into the preBuffer + + [preBuffer didWrite:bytesRead]; + + // Now copy the data into the read packet. + // + // Recall that we didn't read directly into the packet's buffer to avoid + // over-allocating memory since we had no clue how much data was available to be read. + // + // Ensure there's room on the read packet's buffer + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesRead]; + + // Copy bytes from prebuffer into read buffer + + uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(readBuf, [preBuffer readBuffer], bytesRead); + + // Remove the copied bytes from the prebuffer + [preBuffer didRead:bytesRead]; + + // Update totals + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + } + else + { + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + } + + done = YES; + } + + } // if (bytesRead > 0) + + } // if (!done && !error && !socketEOF && hasBytesAvailable) + + + if (!done && currentRead->readLength == 0 && currentRead->term == nil) + { + // Read type #1 - read all available data + // + // We might arrive here if we read data from the prebuffer but not from the socket. + + done = (totalBytesReadForCurrentRead > 0); + } + + // Check to see if we're done, or if we've made progress + + if (done) + { + [self completeCurrentRead]; + + if (!error && (!socketEOF || [preBuffer availableBytes] > 0)) + { + [self maybeDequeueRead]; + } + } + else if (totalBytesReadForCurrentRead > 0) + { + // We're not done read type #2 or #3 yet, but we have read in some bytes + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadPartialDataOfLength:tag:)]) + { + long theReadTag = currentRead->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReadPartialDataOfLength:totalBytesReadForCurrentRead tag:theReadTag]; + }}); + } + } + + // Check for errors + + if (error) + { + [self closeWithError:error]; + } + else if (socketEOF) + { + [self doReadEOF]; + } + else if (waiting) + { + if (![self usingCFStreamForTLS]) + { + // Monitor the socket for readability (if we're not already doing so) + [self resumeReadSource]; + } + } + + // Do not add any code here without first adding return statements in the error cases above. +} + +- (void)doReadEOF +{ + LogTrace(); + + // This method may be called more than once. + // If the EOF is read while there is still data in the preBuffer, + // then this method may be called continually after invocations of doReadData to see if it's time to disconnect. + + flags |= kSocketHasReadEOF; + + if (flags & kSocketSecure) + { + // If the SSL layer has any buffered data, flush it into the preBuffer now. + + [self flushSSLBuffers]; + } + + BOOL shouldDisconnect = NO; + NSError *error = nil; + + if ((flags & kStartingReadTLS) || (flags & kStartingWriteTLS)) + { + // We received an EOF during or prior to startTLS. + // The SSL/TLS handshake is now impossible, so this is an unrecoverable situation. + + shouldDisconnect = YES; + + if ([self usingSecureTransportForTLS]) + { + error = [self sslError:errSSLClosedAbort]; + } + } + else if (flags & kReadStreamClosed) + { + // The preBuffer has already been drained. + // The config allows half-duplex connections. + // We've previously checked the socket, and it appeared writeable. + // So we marked the read stream as closed and notified the delegate. + // + // As per the half-duplex contract, the socket will be closed when a write fails, + // or when the socket is manually closed. + + shouldDisconnect = NO; + } + else if ([preBuffer availableBytes] > 0) + { + LogVerbose(@"Socket reached EOF, but there is still data available in prebuffer"); + + // Although we won't be able to read any more data from the socket, + // there is existing data that has been prebuffered that we can read. + + shouldDisconnect = NO; + } + else if (config & kAllowHalfDuplexConnection) + { + // We just received an EOF (end of file) from the socket's read stream. + // This means the remote end of the socket (the peer we're connected to) + // has explicitly stated that it will not be sending us any more data. + // + // Query the socket to see if it is still writeable. (Perhaps the peer will continue reading data from us) + + int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD; + + struct pollfd pfd[1]; + pfd[0].fd = socketFD; + pfd[0].events = POLLOUT; + pfd[0].revents = 0; + + poll(pfd, 1, 0); + + if (pfd[0].revents & POLLOUT) + { + // Socket appears to still be writeable + + shouldDisconnect = NO; + flags |= kReadStreamClosed; + + // Notify the delegate that we're going half-duplex + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidCloseReadStream:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidCloseReadStream:self]; + }}); + } + } + else + { + shouldDisconnect = YES; + } + } + else + { + shouldDisconnect = YES; + } + + + if (shouldDisconnect) + { + if (error == nil) + { + if ([self usingSecureTransportForTLS]) + { + if (sslErrCode != noErr && sslErrCode != errSSLClosedGraceful) + { + error = [self sslError:sslErrCode]; + } + else + { + error = [self connectionClosedError]; + } + } + else + { + error = [self connectionClosedError]; + } + } + [self closeWithError:error]; + } + else + { + if (![self usingCFStreamForTLS]) + { + // Suspend the read source (if needed) + + [self suspendReadSource]; + } + } +} + +- (void)completeCurrentRead +{ + LogTrace(); + + NSAssert(currentRead, @"Trying to complete current read when there is no current read."); + + + NSData *result = nil; + + if (currentRead->bufferOwner) + { + // We created the buffer on behalf of the user. + // Trim our buffer to be the proper size. + [currentRead->buffer setLength:currentRead->bytesDone]; + + result = currentRead->buffer; + } + else + { + // We did NOT create the buffer. + // The buffer is owned by the caller. + // Only trim the buffer if we had to increase its size. + + if ([currentRead->buffer length] > currentRead->originalBufferLength) + { + NSUInteger readSize = currentRead->startOffset + currentRead->bytesDone; + NSUInteger origSize = currentRead->originalBufferLength; + + NSUInteger buffSize = MAX(readSize, origSize); + + [currentRead->buffer setLength:buffSize]; + } + + uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset; + + result = [NSData dataWithBytesNoCopy:buffer length:currentRead->bytesDone freeWhenDone:NO]; + } + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadData:withTag:)]) + { + GCDAsyncReadPacket *theRead = currentRead; // Ensure currentRead retained since result may not own buffer + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReadData:result withTag:theRead->tag]; + }}); + } + + [self endCurrentRead]; +} + +- (void)endCurrentRead +{ + if (readTimer) + { + dispatch_source_cancel(readTimer); + readTimer = NULL; + } + + currentRead = nil; +} + +- (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(readTimer, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doReadTimeout]; + + #pragma clang diagnostic pop + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theReadTimer = readTimer; + dispatch_source_set_cancel_handler(readTimer, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(readTimer)"); + dispatch_release(theReadTimer); + + #pragma clang diagnostic pop + }); + #endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(readTimer); + } +} + +- (void)doReadTimeout +{ + // This is a little bit tricky. + // Ideally we'd like to synchronously query the delegate about a timeout extension. + // But if we do so synchronously we risk a possible deadlock. + // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. + + flags |= kReadsPaused; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutReadWithTag:elapsed:bytesDone:)]) + { + GCDAsyncReadPacket *theRead = currentRead; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + NSTimeInterval timeoutExtension = 0.0; + + timeoutExtension = [theDelegate socket:self shouldTimeoutReadWithTag:theRead->tag + elapsed:theRead->timeout + bytesDone:theRead->bytesDone]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self doReadTimeoutWithExtension:timeoutExtension]; + }}); + }}); + } + else + { + [self doReadTimeoutWithExtension:0.0]; + } +} + +- (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension +{ + if (currentRead) + { + if (timeoutExtension > 0.0) + { + currentRead->timeout += timeoutExtension; + + // Reschedule the timer + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); + dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); + + // Unpause reads, and continue + flags &= ~kReadsPaused; + [self doReadData]; + } + else + { + LogVerbose(@"ReadTimeout"); + + [self closeWithError:[self readTimeoutError]]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Writing +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + if ([data length] == 0) return; + + GCDAsyncWritePacket *packet = [[GCDAsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [writeQueue addObject:packet]; + [self maybeDequeueWrite]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr +{ + __block float result = 0.0F; + + dispatch_block_t block = ^{ + + if (!currentWrite || ![currentWrite isKindOfClass:[GCDAsyncWritePacket class]]) + { + // We're not writing anything right now. + + if (tagPtr != NULL) *tagPtr = 0; + if (donePtr != NULL) *donePtr = 0; + if (totalPtr != NULL) *totalPtr = 0; + + result = NAN; + } + else + { + NSUInteger done = currentWrite->bytesDone; + NSUInteger total = [currentWrite->buffer length]; + + if (tagPtr != NULL) *tagPtr = currentWrite->tag; + if (donePtr != NULL) *donePtr = done; + if (totalPtr != NULL) *totalPtr = total; + + result = (float)done / (float)total; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +/** + * Conditionally starts a new write. + * + * It is called when: + * - a user requests a write + * - after a write request has finished (to handle the next request) + * - immediately after the socket opens to handle any pending requests + * + * This method also handles auto-disconnect post read/write completion. +**/ +- (void)maybeDequeueWrite +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + // If we're not currently processing a write AND we have an available write stream + if ((currentWrite == nil) && (flags & kConnected)) + { + if ([writeQueue count] > 0) + { + // Dequeue the next object in the write queue + currentWrite = [writeQueue objectAtIndex:0]; + [writeQueue removeObjectAtIndex:0]; + + + if ([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]]) + { + LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); + + // Attempt to start TLS + flags |= kStartingWriteTLS; + + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set + [self maybeStartTLS]; + } + else + { + LogVerbose(@"Dequeued GCDAsyncWritePacket"); + + // Setup write timer (if needed) + [self setupWriteTimerWithTimeout:currentWrite->timeout]; + + // Immediately write, if possible + [self doWriteData]; + } + } + else if (flags & kDisconnectAfterWrites) + { + if (flags & kDisconnectAfterReads) + { + if (([readQueue count] == 0) && (currentRead == nil)) + { + [self closeWithError:nil]; + } + } + else + { + [self closeWithError:nil]; + } + } + } +} + +- (void)doWriteData +{ + LogTrace(); + + // This method is called by the writeSource via the socketQueue + + if ((currentWrite == nil) || (flags & kWritesPaused)) + { + LogVerbose(@"No currentWrite or kWritesPaused"); + + // Unable to write at this time + + if ([self usingCFStreamForTLS]) + { + // CFWriteStream only fires once when there is available data. + // It won't fire again until we've invoked CFWriteStreamWrite. + } + else + { + // If the writeSource is firing, we need to pause it + // or else it will continue to fire over and over again. + + if (flags & kSocketCanAcceptBytes) + { + [self suspendWriteSource]; + } + } + return; + } + + if (!(flags & kSocketCanAcceptBytes)) + { + LogVerbose(@"No space available to write..."); + + // No space available to write. + + if (![self usingCFStreamForTLS]) + { + // Need to wait for writeSource to fire and notify us of + // available space in the socket's internal write buffer. + + [self resumeWriteSource]; + } + return; + } + + if (flags & kStartingWriteTLS) + { + LogVerbose(@"Waiting for SSL/TLS handshake to complete"); + + // The writeQueue is waiting for SSL/TLS handshake to complete. + + if (flags & kStartingReadTLS) + { + if ([self usingSecureTransportForTLS]) + { + // We are in the process of a SSL Handshake. + // We were waiting for available space in the socket's internal OS buffer to continue writing. + + [self ssl_continueSSLHandshake]; + } + } + else + { + // We are still waiting for the readQueue to drain and start the SSL/TLS process. + // We now know we can write to the socket. + + if (![self usingCFStreamForTLS]) + { + // Suspend the write source or else it will continue to fire nonstop. + + [self suspendWriteSource]; + } + } + + return; + } + + // Note: This method is not called if currentWrite is a GCDAsyncSpecialPacket (startTLS packet) + + BOOL waiting = NO; + NSError *error = nil; + size_t bytesWritten = 0; + + if (flags & kSocketSecure) + { + if ([self usingCFStreamForTLS]) + { + #if TARGET_OS_IPHONE + + // + // Writing data using CFStream (over internal TLS) + // + + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + CFIndex result = CFWriteStreamWrite(writeStream, buffer, (CFIndex)bytesToWrite); + LogVerbose(@"CFWriteStreamWrite(%lu) = %li", (unsigned long)bytesToWrite, result); + + if (result < 0) + { + error = (__bridge_transfer NSError *)CFWriteStreamCopyError(writeStream); + } + else + { + bytesWritten = (size_t)result; + + // We always set waiting to true in this scenario. + // CFStream may have altered our underlying socket to non-blocking. + // Thus if we attempt to write without a callback, we may end up blocking our queue. + waiting = YES; + } + + #endif + } + else + { + // We're going to use the SSLWrite function. + // + // OSStatus SSLWrite(SSLContextRef context, const void *data, size_t dataLength, size_t *processed) + // + // Parameters: + // context - An SSL session context reference. + // data - A pointer to the buffer of data to write. + // dataLength - The amount, in bytes, of data to write. + // processed - On return, the length, in bytes, of the data actually written. + // + // It sounds pretty straight-forward, + // but there are a few caveats you should be aware of. + // + // The SSLWrite method operates in a non-obvious (and rather annoying) manner. + // According to the documentation: + // + // Because you may configure the underlying connection to operate in a non-blocking manner, + // a write operation might return errSSLWouldBlock, indicating that less data than requested + // was actually transferred. In this case, you should repeat the call to SSLWrite until some + // other result is returned. + // + // This sounds perfect, but when our SSLWriteFunction returns errSSLWouldBlock, + // then the SSLWrite method returns (with the proper errSSLWouldBlock return value), + // but it sets processed to dataLength !! + // + // In other words, if the SSLWrite function doesn't completely write all the data we tell it to, + // then it doesn't tell us how many bytes were actually written. So, for example, if we tell it to + // write 256 bytes then it might actually write 128 bytes, but then report 0 bytes written. + // + // You might be wondering: + // If the SSLWrite function doesn't tell us how many bytes were written, + // then how in the world are we supposed to update our parameters (buffer & bytesToWrite) + // for the next time we invoke SSLWrite? + // + // The answer is that SSLWrite cached all the data we told it to write, + // and it will push out that data next time we call SSLWrite. + // If we call SSLWrite with new data, it will push out the cached data first, and then the new data. + // If we call SSLWrite with empty data, then it will simply push out the cached data. + // + // For this purpose we're going to break large writes into a series of smaller writes. + // This allows us to report progress back to the delegate. + + OSStatus result; + + BOOL hasCachedDataToWrite = (sslWriteCachedLength > 0); + BOOL hasNewDataToWrite = YES; + + if (hasCachedDataToWrite) + { + size_t processed = 0; + + result = SSLWrite(sslContext, NULL, 0, &processed); + + if (result == noErr) + { + bytesWritten = sslWriteCachedLength; + sslWriteCachedLength = 0; + + if ([currentWrite->buffer length] == (currentWrite->bytesDone + bytesWritten)) + { + // We've written all data for the current write. + hasNewDataToWrite = NO; + } + } + else + { + if (result == errSSLWouldBlock) + { + waiting = YES; + } + else + { + error = [self sslError:result]; + } + + // Can't write any new data since we were unable to write the cached data. + hasNewDataToWrite = NO; + } + } + + if (hasNewDataToWrite) + { + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + + currentWrite->bytesDone + + bytesWritten; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone - bytesWritten; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + size_t bytesRemaining = bytesToWrite; + + BOOL keepLooping = YES; + while (keepLooping) + { + const size_t sslMaxBytesToWrite = 32768; + size_t sslBytesToWrite = MIN(bytesRemaining, sslMaxBytesToWrite); + size_t sslBytesWritten = 0; + + result = SSLWrite(sslContext, buffer, sslBytesToWrite, &sslBytesWritten); + + if (result == noErr) + { + buffer += sslBytesWritten; + bytesWritten += sslBytesWritten; + bytesRemaining -= sslBytesWritten; + + keepLooping = (bytesRemaining > 0); + } + else + { + if (result == errSSLWouldBlock) + { + waiting = YES; + sslWriteCachedLength = sslBytesToWrite; + } + else + { + error = [self sslError:result]; + } + + keepLooping = NO; + } + + } // while (keepLooping) + + } // if (hasNewDataToWrite) + } + } + else + { + // + // Writing data directly over raw socket + // + + int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD; + + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite); + LogVerbose(@"wrote to socket = %zd", result); + + // Check results + if (result < 0) + { + if (errno == EWOULDBLOCK) + { + waiting = YES; + } + else + { + error = [self errnoErrorWithReason:@"Error in write() function"]; + } + } + else + { + bytesWritten = result; + } + } + + // We're done with our writing. + // If we explictly ran into a situation where the socket told us there was no room in the buffer, + // then we immediately resume listening for notifications. + // + // We must do this before we dequeue another write, + // as that may in turn invoke this method again. + // + // Note that if CFStream is involved, it may have maliciously put our socket in blocking mode. + + if (waiting) + { + flags &= ~kSocketCanAcceptBytes; + + if (![self usingCFStreamForTLS]) + { + [self resumeWriteSource]; + } + } + + // Check our results + + BOOL done = NO; + + if (bytesWritten > 0) + { + // Update total amount read for the current write + currentWrite->bytesDone += bytesWritten; + LogVerbose(@"currentWrite->bytesDone = %lu", (unsigned long)currentWrite->bytesDone); + + // Is packet done? + done = (currentWrite->bytesDone == [currentWrite->buffer length]); + } + + if (done) + { + [self completeCurrentWrite]; + + if (!error) + { + dispatch_async(socketQueue, ^{ @autoreleasepool{ + + [self maybeDequeueWrite]; + }}); + } + } + else + { + // We were unable to finish writing the data, + // so we're waiting for another callback to notify us of available space in the lower-level output buffer. + + if (!waiting && !error) + { + // This would be the case if our write was able to accept some data, but not all of it. + + flags &= ~kSocketCanAcceptBytes; + + if (![self usingCFStreamForTLS]) + { + [self resumeWriteSource]; + } + } + + if (bytesWritten > 0) + { + // We're not done with the entire write, but we have written some bytes + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWritePartialDataOfLength:tag:)]) + { + long theWriteTag = currentWrite->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didWritePartialDataOfLength:bytesWritten tag:theWriteTag]; + }}); + } + } + } + + // Check for errors + + if (error) + { + [self closeWithError:[self errnoErrorWithReason:@"Error in write() function"]]; + } + + // Do not add any code here without first adding a return statement in the error case above. +} + +- (void)completeCurrentWrite +{ + LogTrace(); + + NSAssert(currentWrite, @"Trying to complete current write when there is no current write."); + + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWriteDataWithTag:)]) + { + long theWriteTag = currentWrite->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didWriteDataWithTag:theWriteTag]; + }}); + } + + [self endCurrentWrite]; +} + +- (void)endCurrentWrite +{ + if (writeTimer) + { + dispatch_source_cancel(writeTimer); + writeTimer = NULL; + } + + currentWrite = nil; +} + +- (void)setupWriteTimerWithTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + writeTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(writeTimer, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doWriteTimeout]; + + #pragma clang diagnostic pop + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theWriteTimer = writeTimer; + dispatch_source_set_cancel_handler(writeTimer, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(writeTimer)"); + dispatch_release(theWriteTimer); + + #pragma clang diagnostic pop + }); + #endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(writeTimer); + } +} + +- (void)doWriteTimeout +{ + // This is a little bit tricky. + // Ideally we'd like to synchronously query the delegate about a timeout extension. + // But if we do so synchronously we risk a possible deadlock. + // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. + + flags |= kWritesPaused; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutWriteWithTag:elapsed:bytesDone:)]) + { + GCDAsyncWritePacket *theWrite = currentWrite; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + NSTimeInterval timeoutExtension = 0.0; + + timeoutExtension = [theDelegate socket:self shouldTimeoutWriteWithTag:theWrite->tag + elapsed:theWrite->timeout + bytesDone:theWrite->bytesDone]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self doWriteTimeoutWithExtension:timeoutExtension]; + }}); + }}); + } + else + { + [self doWriteTimeoutWithExtension:0.0]; + } +} + +- (void)doWriteTimeoutWithExtension:(NSTimeInterval)timeoutExtension +{ + if (currentWrite) + { + if (timeoutExtension > 0.0) + { + currentWrite->timeout += timeoutExtension; + + // Reschedule the timer + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); + dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); + + // Unpause writes, and continue + flags &= ~kWritesPaused; + [self doWriteData]; + } + else + { + LogVerbose(@"WriteTimeout"); + + [self closeWithError:[self writeTimeoutError]]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)startTLS:(NSDictionary *)tlsSettings +{ + LogTrace(); + + if (tlsSettings == nil) + { + // Passing nil/NULL to CFReadStreamSetProperty will appear to work the same as passing an empty dictionary, + // but causes problems if we later try to fetch the remote host's certificate. + // + // To be exact, it causes the following to return NULL instead of the normal result: + // CFReadStreamCopyProperty(readStream, kCFStreamPropertySSLPeerCertificates) + // + // So we use an empty dictionary instead, which works perfectly. + + tlsSettings = [NSDictionary dictionary]; + } + + GCDAsyncSpecialPacket *packet = [[GCDAsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if ((flags & kSocketStarted) && !(flags & kQueuedTLS) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [writeQueue addObject:packet]; + + flags |= kQueuedTLS; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } + }}); + +} + +- (void)maybeStartTLS +{ + // We can't start TLS until: + // - All queued reads prior to the user calling startTLS are complete + // - All queued writes prior to the user calling startTLS are complete + // + // We'll know these conditions are met when both kStartingReadTLS and kStartingWriteTLS are set + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + BOOL useSecureTransport = YES; + + #if TARGET_OS_IPHONE + { + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + NSDictionary *tlsSettings = tlsPacket->tlsSettings; + + NSNumber *value = [tlsSettings objectForKey:GCDAsyncSocketUseCFStreamForTLS]; + if (value && [value boolValue] == YES) + useSecureTransport = NO; + } + #endif + + if (useSecureTransport) + { + [self ssl_startTLS]; + } + else + { + #if TARGET_OS_IPHONE + [self cf_startTLS]; + #endif + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security via SecureTransport +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (OSStatus)sslReadWithBuffer:(void *)buffer length:(size_t *)bufferLength +{ + LogVerbose(@"sslReadWithBuffer:%p length:%lu", buffer, (unsigned long)*bufferLength); + + if ((socketFDBytesAvailable == 0) && ([sslPreBuffer availableBytes] == 0)) + { + LogVerbose(@"%@ - No data available to read...", THIS_METHOD); + + // No data available to read. + // + // Need to wait for readSource to fire and notify us of + // available data in the socket's internal read buffer. + + [self resumeReadSource]; + + *bufferLength = 0; + return errSSLWouldBlock; + } + + size_t totalBytesRead = 0; + size_t totalBytesLeftToBeRead = *bufferLength; + + BOOL done = NO; + BOOL socketError = NO; + + // + // STEP 1 : READ FROM SSL PRE BUFFER + // + + size_t sslPreBufferLength = [sslPreBuffer availableBytes]; + + if (sslPreBufferLength > 0) + { + LogVerbose(@"%@: Reading from SSL pre buffer...", THIS_METHOD); + + size_t bytesToCopy; + if (sslPreBufferLength > totalBytesLeftToBeRead) + bytesToCopy = totalBytesLeftToBeRead; + else + bytesToCopy = sslPreBufferLength; + + LogVerbose(@"%@: Copying %zu bytes from sslPreBuffer", THIS_METHOD, bytesToCopy); + + memcpy(buffer, [sslPreBuffer readBuffer], bytesToCopy); + [sslPreBuffer didRead:bytesToCopy]; + + LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); + + totalBytesRead += bytesToCopy; + totalBytesLeftToBeRead -= bytesToCopy; + + done = (totalBytesLeftToBeRead == 0); + + if (done) LogVerbose(@"%@: Complete", THIS_METHOD); + } + + // + // STEP 2 : READ FROM SOCKET + // + + if (!done && (socketFDBytesAvailable > 0)) + { + LogVerbose(@"%@: Reading from socket...", THIS_METHOD); + + int socketFD = (socket6FD == SOCKET_NULL) ? socket4FD : socket6FD; + + BOOL readIntoPreBuffer; + size_t bytesToRead; + uint8_t *buf; + + if (socketFDBytesAvailable > totalBytesLeftToBeRead) + { + // Read all available data from socket into sslPreBuffer. + // Then copy requested amount into dataBuffer. + + LogVerbose(@"%@: Reading into sslPreBuffer...", THIS_METHOD); + + [sslPreBuffer ensureCapacityForWrite:socketFDBytesAvailable]; + + readIntoPreBuffer = YES; + bytesToRead = (size_t)socketFDBytesAvailable; + buf = [sslPreBuffer writeBuffer]; + } + else + { + // Read available data from socket directly into dataBuffer. + + LogVerbose(@"%@: Reading directly into dataBuffer...", THIS_METHOD); + + readIntoPreBuffer = NO; + bytesToRead = totalBytesLeftToBeRead; + buf = (uint8_t *)buffer + totalBytesRead; + } + + ssize_t result = read(socketFD, buf, bytesToRead); + LogVerbose(@"%@: read from socket = %zd", THIS_METHOD, result); + + if (result < 0) + { + LogVerbose(@"%@: read errno = %i", THIS_METHOD, errno); + + if (errno != EWOULDBLOCK) + { + socketError = YES; + } + + socketFDBytesAvailable = 0; + } + else if (result == 0) + { + LogVerbose(@"%@: read EOF", THIS_METHOD); + + socketError = YES; + socketFDBytesAvailable = 0; + } + else + { + size_t bytesReadFromSocket = result; + + if (socketFDBytesAvailable > bytesReadFromSocket) + socketFDBytesAvailable -= bytesReadFromSocket; + else + socketFDBytesAvailable = 0; + + if (readIntoPreBuffer) + { + [sslPreBuffer didWrite:bytesReadFromSocket]; + + size_t bytesToCopy = MIN(totalBytesLeftToBeRead, bytesReadFromSocket); + + LogVerbose(@"%@: Copying %zu bytes out of sslPreBuffer", THIS_METHOD, bytesToCopy); + + memcpy((uint8_t *)buffer + totalBytesRead, [sslPreBuffer readBuffer], bytesToCopy); + [sslPreBuffer didRead:bytesToCopy]; + + totalBytesRead += bytesToCopy; + totalBytesLeftToBeRead -= bytesToCopy; + + LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); + } + else + { + totalBytesRead += bytesReadFromSocket; + totalBytesLeftToBeRead -= bytesReadFromSocket; + } + + done = (totalBytesLeftToBeRead == 0); + + if (done) LogVerbose(@"%@: Complete", THIS_METHOD); + } + } + + *bufferLength = totalBytesRead; + + if (done) + return noErr; + + if (socketError) + return errSSLClosedAbort; + + return errSSLWouldBlock; +} + +- (OSStatus)sslWriteWithBuffer:(const void *)buffer length:(size_t *)bufferLength +{ + if (!(flags & kSocketCanAcceptBytes)) + { + // Unable to write. + // + // Need to wait for writeSource to fire and notify us of + // available space in the socket's internal write buffer. + + [self resumeWriteSource]; + + *bufferLength = 0; + return errSSLWouldBlock; + } + + size_t bytesToWrite = *bufferLength; + size_t bytesWritten = 0; + + BOOL done = NO; + BOOL socketError = NO; + + int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD; + + ssize_t result = write(socketFD, buffer, bytesToWrite); + + if (result < 0) + { + if (errno != EWOULDBLOCK) + { + socketError = YES; + } + + flags &= ~kSocketCanAcceptBytes; + } + else if (result == 0) + { + flags &= ~kSocketCanAcceptBytes; + } + else + { + bytesWritten = result; + + done = (bytesWritten == bytesToWrite); + } + + *bufferLength = bytesWritten; + + if (done) + return noErr; + + if (socketError) + return errSSLClosedAbort; + + return errSSLWouldBlock; +} + +static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; + + NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); + + return [asyncSocket sslReadWithBuffer:data length:dataLength]; +} + +static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; + + NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); + + return [asyncSocket sslWriteWithBuffer:data length:dataLength]; +} + +- (void)ssl_startTLS +{ + LogTrace(); + + LogVerbose(@"Starting TLS (via SecureTransport)..."); + + OSStatus status; + + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + if (tlsPacket == nil) // Code to quiet the analyzer + { + NSAssert(NO, @"Logic error"); + + [self closeWithError:[self otherError:@"Logic error"]]; + return; + } + NSDictionary *tlsSettings = tlsPacket->tlsSettings; + + // Create SSLContext, and setup IO callbacks and connection ref + + BOOL isServer = [[tlsSettings objectForKey:(NSString *)kCFStreamSSLIsServer] boolValue]; + + #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) + { + if (isServer) + sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType); + else + sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType); + + if (sslContext == NULL) + { + [self closeWithError:[self otherError:@"Error in SSLCreateContext"]]; + return; + } + } + #else // (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) + { + status = SSLNewContext(isServer, &sslContext); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLNewContext"]]; + return; + } + } + #endif + + status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]]; + return; + } + + status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetConnection"]]; + return; + } + + + BOOL shouldManuallyEvaluateTrust = [[tlsSettings objectForKey:GCDAsyncSocketManuallyEvaluateTrust] boolValue]; + if (shouldManuallyEvaluateTrust) + { + if (isServer) + { + [self closeWithError:[self otherError:@"Manual trust validation is not supported for server sockets"]]; + return; + } + + status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetSessionOption"]]; + return; + } + + #if !TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) + + // Note from Apple's documentation: + // + // It is only necessary to call SSLSetEnableCertVerify on the Mac prior to OS X 10.8. + // On OS X 10.8 and later setting kSSLSessionOptionBreakOnServerAuth always disables the + // built-in trust evaluation. All versions of iOS behave like OS X 10.8 and thus + // SSLSetEnableCertVerify is not available on that platform at all. + + status = SSLSetEnableCertVerify(sslContext, NO); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetEnableCertVerify"]]; + return; + } + + #endif + } + + // Configure SSLContext from given settings + // + // Checklist: + // 1. kCFStreamSSLPeerName + // 2. kCFStreamSSLCertificates + // 3. GCDAsyncSocketSSLPeerID + // 4. GCDAsyncSocketSSLProtocolVersionMin + // 5. GCDAsyncSocketSSLProtocolVersionMax + // 6. GCDAsyncSocketSSLSessionOptionFalseStart + // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord + // 8. GCDAsyncSocketSSLCipherSuites + // 9. GCDAsyncSocketSSLDiffieHellmanParameters (Mac) + // + // Deprecated (throw error): + // 10. kCFStreamSSLAllowsAnyRoot + // 11. kCFStreamSSLAllowsExpiredRoots + // 12. kCFStreamSSLAllowsExpiredCertificates + // 13. kCFStreamSSLValidatesCertificateChain + // 14. kCFStreamSSLLevel + + id value; + + // 1. kCFStreamSSLPeerName + + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLPeerName]; + if ([value isKindOfClass:[NSString class]]) + { + NSString *peerName = (NSString *)value; + + const char *peer = [peerName UTF8String]; + size_t peerLen = strlen(peer); + + status = SSLSetPeerDomainName(sslContext, peer, peerLen); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetPeerDomainName"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for kCFStreamSSLPeerName. Value must be of type NSString."); + + [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLPeerName."]]; + return; + } + + // 2. kCFStreamSSLCertificates + + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLCertificates]; + if ([value isKindOfClass:[NSArray class]]) + { + CFArrayRef certs = (__bridge CFArrayRef)value; + + status = SSLSetCertificate(sslContext, certs); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetCertificate"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for kCFStreamSSLCertificates. Value must be of type NSArray."); + + [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLCertificates."]]; + return; + } + + // 3. GCDAsyncSocketSSLPeerID + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLPeerID]; + if ([value isKindOfClass:[NSData class]]) + { + NSData *peerIdData = (NSData *)value; + + status = SSLSetPeerID(sslContext, [peerIdData bytes], [peerIdData length]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetPeerID"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLPeerID. Value must be of type NSData." + @" (You can convert strings to data using a method like" + @" [string dataUsingEncoding:NSUTF8StringEncoding])"); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLPeerID."]]; + return; + } + + // 4. GCDAsyncSocketSSLProtocolVersionMin + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMin]; + if ([value isKindOfClass:[NSNumber class]]) + { + SSLProtocol minProtocol = (SSLProtocol)[(NSNumber *)value intValue]; + if (minProtocol != kSSLProtocolUnknown) + { + status = SSLSetProtocolVersionMin(sslContext, minProtocol); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMin"]]; + return; + } + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMin. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMin."]]; + return; + } + + // 5. GCDAsyncSocketSSLProtocolVersionMax + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMax]; + if ([value isKindOfClass:[NSNumber class]]) + { + SSLProtocol maxProtocol = (SSLProtocol)[(NSNumber *)value intValue]; + if (maxProtocol != kSSLProtocolUnknown) + { + status = SSLSetProtocolVersionMax(sslContext, maxProtocol); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMax"]]; + return; + } + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMax. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMax."]]; + return; + } + + // 6. GCDAsyncSocketSSLSessionOptionFalseStart + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionFalseStart]; + if ([value isKindOfClass:[NSNumber class]]) + { + status = SSLSetSessionOption(sslContext, kSSLSessionOptionFalseStart, [value boolValue]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionFalseStart)"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart."]]; + return; + } + + // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionSendOneByteRecord]; + if ([value isKindOfClass:[NSNumber class]]) + { + status = SSLSetSessionOption(sslContext, kSSLSessionOptionSendOneByteRecord, [value boolValue]); + if (status != noErr) + { + [self closeWithError: + [self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionSendOneByteRecord)"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord." + @" Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord."]]; + return; + } + + // 8. GCDAsyncSocketSSLCipherSuites + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLCipherSuites]; + if ([value isKindOfClass:[NSArray class]]) + { + NSArray *cipherSuites = (NSArray *)value; + NSUInteger numberCiphers = [cipherSuites count]; + SSLCipherSuite ciphers[numberCiphers]; + + NSUInteger cipherIndex; + for (cipherIndex = 0; cipherIndex < numberCiphers; cipherIndex++) + { + NSNumber *cipherObject = [cipherSuites objectAtIndex:cipherIndex]; + ciphers[cipherIndex] = [cipherObject shortValue]; + } + + status = SSLSetEnabledCiphers(sslContext, ciphers, numberCiphers); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetEnabledCiphers"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLCipherSuites. Value must be of type NSArray."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLCipherSuites."]]; + return; + } + + // 9. GCDAsyncSocketSSLDiffieHellmanParameters + + #if !TARGET_OS_IPHONE + value = [tlsSettings objectForKey:GCDAsyncSocketSSLDiffieHellmanParameters]; + if ([value isKindOfClass:[NSData class]]) + { + NSData *diffieHellmanData = (NSData *)value; + + status = SSLSetDiffieHellmanParams(sslContext, [diffieHellmanData bytes], [diffieHellmanData length]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetDiffieHellmanParams"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters. Value must be of type NSData."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters."]]; + return; + } + #endif + + // DEPRECATED checks + + // 10. kCFStreamSSLAllowsAnyRoot + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsAnyRoot]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsAnyRoot" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsAnyRoot"]]; + return; + } + + // 11. kCFStreamSSLAllowsExpiredRoots + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsExpiredRoots]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredRoots" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredRoots"]]; + return; + } + + // 12. kCFStreamSSLValidatesCertificateChain + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLValidatesCertificateChain]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLValidatesCertificateChain" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLValidatesCertificateChain"]]; + return; + } + + // 13. kCFStreamSSLAllowsExpiredCertificates + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsExpiredCertificates]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates"]]; + return; + } + + // 14. kCFStreamSSLLevel + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLLevel]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLLevel" + @" - You must use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMax"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLLevel"]]; + return; + } + + // Setup the sslPreBuffer + // + // Any data in the preBuffer needs to be moved into the sslPreBuffer, + // as this data is now part of the secure read stream. + + sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; + + size_t preBufferLength = [preBuffer availableBytes]; + + if (preBufferLength > 0) + { + [sslPreBuffer ensureCapacityForWrite:preBufferLength]; + + memcpy([sslPreBuffer writeBuffer], [preBuffer readBuffer], preBufferLength); + [preBuffer didRead:preBufferLength]; + [sslPreBuffer didWrite:preBufferLength]; + } + + sslErrCode = noErr; + + // Start the SSL Handshake process + + [self ssl_continueSSLHandshake]; +} + +- (void)ssl_continueSSLHandshake +{ + LogTrace(); + + // If the return value is noErr, the session is ready for normal secure communication. + // If the return value is errSSLWouldBlock, the SSLHandshake function must be called again. + // If the return value is errSSLServerAuthCompleted, we ask delegate if we should trust the + // server and then call SSLHandshake again to resume the handshake or close the connection + // errSSLPeerBadCert SSL error. + // Otherwise, the return value indicates an error code. + + OSStatus status = SSLHandshake(sslContext); + + if (status == noErr) + { + LogVerbose(@"SSLHandshake complete"); + + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + flags |= kSocketSecure; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidSecure:self]; + }}); + } + + [self endCurrentRead]; + [self endCurrentWrite]; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } + else if (status == errSSLPeerAuthCompleted) + { + LogVerbose(@"SSLHandshake peerAuthCompleted - awaiting delegate approval"); + + __block SecTrustRef trust = NULL; + status = SSLCopyPeerTrust(sslContext, &trust); + if (status != noErr) + { + [self closeWithError:[self sslError:status]]; + return; + } + + int aStateIndex = stateIndex; + dispatch_queue_t theSocketQueue = socketQueue; + + __weak GCDAsyncSocket *weakSelf = self; + + void (^comletionHandler)(BOOL) = ^(BOOL shouldTrust){ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + dispatch_async(theSocketQueue, ^{ @autoreleasepool { + + if (trust) { + CFRelease(trust); + trust = NULL; + } + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf) + { + [strongSelf ssl_shouldTrustPeer:shouldTrust stateIndex:aStateIndex]; + } + }}); + + #pragma clang diagnostic pop + }}; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReceiveTrust:completionHandler:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler]; + }}); + } + else + { + if (trust) { + CFRelease(trust); + trust = NULL; + } + + NSString *msg = @"GCDAsyncSocketManuallyEvaluateTrust specified in tlsSettings," + @" but delegate doesn't implement socket:shouldTrustPeer:"; + + [self closeWithError:[self otherError:msg]]; + return; + } + } + else if (status == errSSLWouldBlock) + { + LogVerbose(@"SSLHandshake continues..."); + + // Handshake continues... + // + // This method will be called again from doReadData or doWriteData. + } + else + { + [self closeWithError:[self sslError:status]]; + } +} + +- (void)ssl_shouldTrustPeer:(BOOL)shouldTrust stateIndex:(int)aStateIndex +{ + LogTrace(); + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring ssl_shouldTrustPeer - invalid state (maybe disconnected)"); + + // One of the following is true + // - the socket was disconnected + // - the startTLS operation timed out + // - the completionHandler was already invoked once + + return; + } + + // Increment stateIndex to ensure completionHandler can only be called once. + stateIndex++; + + if (shouldTrust) + { + [self ssl_continueSSLHandshake]; + } + else + { + [self closeWithError:[self sslError:errSSLPeerBadCert]]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security via CFStream +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_OS_IPHONE + +- (void)cf_finishSSLHandshake +{ + LogTrace(); + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + flags |= kSocketSecure; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidSecure:self]; + }}); + } + + [self endCurrentRead]; + [self endCurrentWrite]; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } +} + +- (void)cf_abortSSLHandshake:(NSError *)error +{ + LogTrace(); + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + [self closeWithError:error]; + } +} + +- (void)cf_startTLS +{ + LogTrace(); + + LogVerbose(@"Starting TLS (via CFStream)..."); + + if ([preBuffer availableBytes] > 0) + { + NSString *msg = @"Invalid TLS transition. Handshake has already been read from socket."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + [self suspendReadSource]; + [self suspendWriteSource]; + + socketFDBytesAvailable = 0; + flags &= ~kSocketCanAcceptBytes; + flags &= ~kSecureSocketHasBytesAvailable; + + flags |= kUsingCFStreamForTLS; + + if (![self createReadAndWriteStream]) + { + [self closeWithError:[self otherError:@"Error in CFStreamCreatePairWithSocket"]]; + return; + } + + if (![self registerForStreamCallbacksIncludingReadWrite:YES]) + { + [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; + return; + } + + if (![self addStreamsToRunLoop]) + { + [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; + return; + } + + NSAssert([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid read packet for startTLS"); + NSAssert([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid write packet for startTLS"); + + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + CFDictionaryRef tlsSettings = (__bridge CFDictionaryRef)tlsPacket->tlsSettings; + + // Getting an error concerning kCFStreamPropertySSLSettings ? + // You need to add the CFNetwork framework to your iOS application. + + BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings); + BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings); + + // For some reason, starting around the time of iOS 4.3, + // the first call to set the kCFStreamPropertySSLSettings will return true, + // but the second will return false. + // + // Order doesn't seem to matter. + // So you could call CFReadStreamSetProperty and then CFWriteStreamSetProperty, or you could reverse the order. + // Either way, the first call will return true, and the second returns false. + // + // Interestingly, this doesn't seem to affect anything. + // Which is not altogether unusual, as the documentation seems to suggest that (for many settings) + // setting it on one side of the stream automatically sets it for the other side of the stream. + // + // Although there isn't anything in the documentation to suggest that the second attempt would fail. + // + // Furthermore, this only seems to affect streams that are negotiating a security upgrade. + // In other words, the socket gets connected, there is some back-and-forth communication over the unsecure + // connection, and then a startTLS is issued. + // So this mostly affects newer protocols (XMPP, IMAP) as opposed to older protocols (HTTPS). + + if (!r1 && !r2) // Yes, the && is correct - workaround for apple bug. + { + [self closeWithError:[self otherError:@"Error in CFStreamSetProperty"]]; + return; + } + + if (![self openStreams]) + { + [self closeWithError:[self otherError:@"Error in CFStreamOpen"]]; + return; + } + + LogVerbose(@"Waiting for SSL Handshake to complete..."); +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark CFStream +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_OS_IPHONE + ++ (void)ignore:(id)_ +{} + ++ (void)startCFStreamThreadIfNeeded +{ + LogTrace(); + + static dispatch_once_t predicate; + dispatch_once(&predicate, ^{ + + cfstreamThreadRetainCount = 0; + cfstreamThreadSetupQueue = dispatch_queue_create("GCDAsyncSocket-CFStreamThreadSetup", DISPATCH_QUEUE_SERIAL); + }); + + dispatch_sync(cfstreamThreadSetupQueue, ^{ @autoreleasepool { + + if (++cfstreamThreadRetainCount == 1) + { + cfstreamThread = [[NSThread alloc] initWithTarget:self + selector:@selector(cfstreamThread) + object:nil]; + [cfstreamThread start]; + } + }}); +} + ++ (void)stopCFStreamThreadIfNeeded +{ + LogTrace(); + + // The creation of the cfstreamThread is relatively expensive. + // So we'd like to keep it available for recycling. + // However, there's a tradeoff here, because it shouldn't remain alive forever. + // So what we're going to do is use a little delay before taking it down. + // This way it can be reused properly in situations where multiple sockets are continually in flux. + + int delayInSeconds = 30; + dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); + dispatch_after(when, cfstreamThreadSetupQueue, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + if (cfstreamThreadRetainCount == 0) + { + LogWarn(@"Logic error concerning cfstreamThread start / stop"); + return_from_block; + } + + if (--cfstreamThreadRetainCount == 0) + { + [cfstreamThread cancel]; // set isCancelled flag + + // wake up the thread + [GCDAsyncSocket performSelector:@selector(ignore:) + onThread:cfstreamThread + withObject:[NSNull null] + waitUntilDone:NO]; + + cfstreamThread = nil; + } + + #pragma clang diagnostic pop + }}); +} + ++ (void)cfstreamThread { @autoreleasepool +{ + [[NSThread currentThread] setName:GCDAsyncSocketThreadName]; + + LogInfo(@"CFStreamThread: Started"); + + // We can't run the run loop unless it has an associated input source or a timer. + // So we'll just create a timer that will never fire - unless the server runs for decades. + [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] + target:self + selector:@selector(ignore:) + userInfo:nil + repeats:YES]; + + NSThread *currentThread = [NSThread currentThread]; + NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop]; + + BOOL isCancelled = [currentThread isCancelled]; + + while (!isCancelled && [currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) + { + isCancelled = [currentThread isCancelled]; + } + + LogInfo(@"CFStreamThread: Stopped"); +}} + ++ (void)scheduleCFStreams:(GCDAsyncSocket *)asyncSocket +{ + LogTrace(); + NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncSocket->readStream) + CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); + + if (asyncSocket->writeStream) + CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); +} + ++ (void)unscheduleCFStreams:(GCDAsyncSocket *)asyncSocket +{ + LogTrace(); + NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncSocket->readStream) + CFReadStreamUnscheduleFromRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); + + if (asyncSocket->writeStream) + CFWriteStreamUnscheduleFromRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); +} + +static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; + + switch(type) + { + case kCFStreamEventHasBytesAvailable: + { + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable"); + + if (asyncSocket->readStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. + // (A callback related to the tcp stream, but not to the SSL layer). + + if (CFReadStreamHasBytesAvailable(asyncSocket->readStream)) + { + asyncSocket->flags |= kSecureSocketHasBytesAvailable; + [asyncSocket cf_finishSSLHandshake]; + } + } + else + { + asyncSocket->flags |= kSecureSocketHasBytesAvailable; + [asyncSocket doReadData]; + } + }}); + + break; + } + default: + { + NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream); + + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncSocket connectionClosedError]; + } + + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFReadStreamCallback - Other"); + + if (asyncSocket->readStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + [asyncSocket cf_abortSSLHandshake:error]; + } + else + { + [asyncSocket closeWithError:error]; + } + }}); + + break; + } + } + +} + +static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; + + switch(type) + { + case kCFStreamEventCanAcceptBytes: + { + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes"); + + if (asyncSocket->writeStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. + // (A callback related to the tcp stream, but not to the SSL layer). + + if (CFWriteStreamCanAcceptBytes(asyncSocket->writeStream)) + { + asyncSocket->flags |= kSocketCanAcceptBytes; + [asyncSocket cf_finishSSLHandshake]; + } + } + else + { + asyncSocket->flags |= kSocketCanAcceptBytes; + [asyncSocket doWriteData]; + } + }}); + + break; + } + default: + { + NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream); + + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncSocket connectionClosedError]; + } + + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFWriteStreamCallback - Other"); + + if (asyncSocket->writeStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + [asyncSocket cf_abortSSLHandshake:error]; + } + else + { + [asyncSocket closeWithError:error]; + } + }}); + + break; + } + } + +} + +- (BOOL)createReadAndWriteStream +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (readStream || writeStream) + { + // Streams already created + return YES; + } + + int socketFD = (socket6FD == SOCKET_NULL) ? socket4FD : socket6FD; + + if (socketFD == SOCKET_NULL) + { + // Cannot create streams without a file descriptor + return NO; + } + + if (![self isConnected]) + { + // Cannot create streams until file descriptor is connected + return NO; + } + + LogVerbose(@"Creating read and write stream..."); + + CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socketFD, &readStream, &writeStream); + + // The kCFStreamPropertyShouldCloseNativeSocket property should be false by default (for our case). + // But let's not take any chances. + + if (readStream) + CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + if (writeStream) + CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + + if ((readStream == NULL) || (writeStream == NULL)) + { + LogWarn(@"Unable to create read and write stream..."); + + if (readStream) + { + CFReadStreamClose(readStream); + CFRelease(readStream); + readStream = NULL; + } + if (writeStream) + { + CFWriteStreamClose(writeStream); + CFRelease(writeStream); + writeStream = NULL; + } + + return NO; + } + + return YES; +} + +- (BOOL)registerForStreamCallbacksIncludingReadWrite:(BOOL)includeReadWrite +{ + LogVerbose(@"%@ %@", THIS_METHOD, (includeReadWrite ? @"YES" : @"NO")); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + streamContext.version = 0; + streamContext.info = (__bridge void *)(self); + streamContext.retain = nil; + streamContext.release = nil; + streamContext.copyDescription = nil; + + CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + if (includeReadWrite) + readStreamEvents |= kCFStreamEventHasBytesAvailable; + + if (!CFReadStreamSetClient(readStream, readStreamEvents, &CFReadStreamCallback, &streamContext)) + { + return NO; + } + + CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + if (includeReadWrite) + writeStreamEvents |= kCFStreamEventCanAcceptBytes; + + if (!CFWriteStreamSetClient(writeStream, writeStreamEvents, &CFWriteStreamCallback, &streamContext)) + { + return NO; + } + + return YES; +} + +- (BOOL)addStreamsToRunLoop +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + if (!(flags & kAddedStreamsToRunLoop)) + { + LogVerbose(@"Adding streams to runloop..."); + + [[self class] startCFStreamThreadIfNeeded]; + [[self class] performSelector:@selector(scheduleCFStreams:) + onThread:cfstreamThread + withObject:self + waitUntilDone:YES]; + + flags |= kAddedStreamsToRunLoop; + } + + return YES; +} + +- (void)removeStreamsFromRunLoop +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + if (flags & kAddedStreamsToRunLoop) + { + LogVerbose(@"Removing streams from runloop..."); + + [[self class] performSelector:@selector(unscheduleCFStreams:) + onThread:cfstreamThread + withObject:self + waitUntilDone:YES]; + [[self class] stopCFStreamThreadIfNeeded]; + + flags &= ~kAddedStreamsToRunLoop; + } +} + +- (BOOL)openStreams +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + CFStreamStatus readStatus = CFReadStreamGetStatus(readStream); + CFStreamStatus writeStatus = CFWriteStreamGetStatus(writeStream); + + if ((readStatus == kCFStreamStatusNotOpen) || (writeStatus == kCFStreamStatusNotOpen)) + { + LogVerbose(@"Opening read and write stream..."); + + BOOL r1 = CFReadStreamOpen(readStream); + BOOL r2 = CFWriteStreamOpen(writeStream); + + if (!r1 || !r2) + { + LogError(@"Error in CFStreamOpen"); + return NO; + } + } + + return YES; +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Advanced +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * See header file for big discussion of this method. +**/ +- (BOOL)autoDisconnectOnClosedReadStream +{ + // Note: YES means kAllowHalfDuplexConnection is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kAllowHalfDuplexConnection) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kAllowHalfDuplexConnection) == 0); + }); + + return result; + } +} + +/** + * See header file for big discussion of this method. +**/ +- (void)setAutoDisconnectOnClosedReadStream:(BOOL)flag +{ + // Note: YES means kAllowHalfDuplexConnection is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kAllowHalfDuplexConnection; + else + config |= kAllowHalfDuplexConnection; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + + +/** + * See header file for big discussion of this method. +**/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketNewTargetQueue +{ + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketNewTargetQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); +} + +/** + * See header file for big discussion of this method. +**/ +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketOldTargetQueue +{ + dispatch_queue_set_specific(socketOldTargetQueue, IsOnSocketQueueOrTargetQueueKey, NULL, NULL); +} + +/** + * See header file for big discussion of this method. +**/ +- (void)performBlock:(dispatch_block_t)block +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); +} + +/** + * Questions? Have you read the header file? +**/ +- (int)socketFD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + if (socket4FD != SOCKET_NULL) + return socket4FD; + else + return socket6FD; +} + +/** + * Questions? Have you read the header file? +**/ +- (int)socket4FD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + return socket4FD; +} + +/** + * Questions? Have you read the header file? +**/ +- (int)socket6FD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + return socket6FD; +} + +#if TARGET_OS_IPHONE + +/** + * Questions? Have you read the header file? +**/ +- (CFReadStreamRef)readStream +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + if (readStream == NULL) + [self createReadAndWriteStream]; + + return readStream; +} + +/** + * Questions? Have you read the header file? +**/ +- (CFWriteStreamRef)writeStream +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + if (writeStream == NULL) + [self createReadAndWriteStream]; + + return writeStream; +} + +- (BOOL)enableBackgroundingOnSocketWithCaveat:(BOOL)caveat +{ + if (![self createReadAndWriteStream]) + { + // Error occured creating streams (perhaps socket isn't open) + return NO; + } + + BOOL r1, r2; + + LogVerbose(@"Enabling backgrouding on socket"); + + r1 = CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + r2 = CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + + if (!r1 || !r2) + { + return NO; + } + + if (!caveat) + { + if (![self openStreams]) + { + return NO; + } + } + + return YES; +} + +/** + * Questions? Have you read the header file? +**/ +- (BOOL)enableBackgroundingOnSocket +{ + LogTrace(); + + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NO; + } + + return [self enableBackgroundingOnSocketWithCaveat:NO]; +} + +- (BOOL)enableBackgroundingOnSocketWithCaveat // Deprecated in iOS 4.??? +{ + // This method was created as a workaround for a bug in iOS. + // Apple has since fixed this bug. + // I'm not entirely sure which version of iOS they fixed it in... + + LogTrace(); + + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NO; + } + + return [self enableBackgroundingOnSocketWithCaveat:YES]; +} + +#endif + +- (SSLContextRef)sslContext +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + return sslContext; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Class Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr +{ + LogTrace(); + + NSMutableArray *addresses = nil; + NSError *error = nil; + + if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) + { + // Use LOOPBACK address + struct sockaddr_in nativeAddr4; + nativeAddr4.sin_len = sizeof(struct sockaddr_in); + nativeAddr4.sin_family = AF_INET; + nativeAddr4.sin_port = htons(port); + nativeAddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero)); + + struct sockaddr_in6 nativeAddr6; + nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); + nativeAddr6.sin6_family = AF_INET6; + nativeAddr6.sin6_port = htons(port); + nativeAddr6.sin6_flowinfo = 0; + nativeAddr6.sin6_addr = in6addr_loopback; + nativeAddr6.sin6_scope_id = 0; + + // Wrap the native address structures + + NSData *address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + NSData *address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + + addresses = [NSMutableArray arrayWithCapacity:2]; + [addresses addObject:address4]; + [addresses addObject:address6]; + } + else + { + NSString *portStr = [NSString stringWithFormat:@"%hu", port]; + + struct addrinfo hints, *res, *res0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); + + if (gai_error) + { + error = [self gaiError:gai_error]; + } + else + { + NSUInteger capacity = 0; + for (res = res0; res; res = res->ai_next) + { + if (res->ai_family == AF_INET || res->ai_family == AF_INET6) { + capacity++; + } + } + + addresses = [NSMutableArray arrayWithCapacity:capacity]; + + for (res = res0; res; res = res->ai_next) + { + if (res->ai_family == AF_INET) + { + // Found IPv4 address. + // Wrap the native address structure, and add to results. + + NSData *address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + [addresses addObject:address4]; + } + else if (res->ai_family == AF_INET6) + { + // Found IPv6 address. + // Wrap the native address structure, and add to results. + + NSData *address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + [addresses addObject:address6]; + } + } + freeaddrinfo(res0); + + if ([addresses count] == 0) + { + error = [self gaiError:EAI_FAIL]; + } + } + } + + if (errPtr) *errPtr = error; + return addresses; +} + ++ (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 +{ + char addrBuf[INET_ADDRSTRLEN]; + + if (inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + ++ (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 +{ + char addrBuf[INET6_ADDRSTRLEN]; + + if (inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + ++ (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 +{ + return ntohs(pSockaddr4->sin_port); +} + ++ (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 +{ + return ntohs(pSockaddr6->sin6_port); +} + ++ (NSString *)hostFromAddress:(NSData *)address +{ + NSString *host; + + if ([self getHost:&host port:NULL fromAddress:address]) + return host; + else + return nil; +} + ++ (uint16_t)portFromAddress:(NSData *)address +{ + uint16_t port; + + if ([self getHost:NULL port:&port fromAddress:address]) + return port; + else + return 0; +} + ++ (BOOL)isIPv4Address:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = [address bytes]; + + if (sockaddrX->sa_family == AF_INET) { + return YES; + } + } + + return NO; +} + ++ (BOOL)isIPv6Address:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = [address bytes]; + + if (sockaddrX->sa_family == AF_INET6) { + return YES; + } + } + + return NO; +} + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address +{ + return [self getHost:hostPtr port:portPtr family:NULL fromAddress:address]; +} + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(sa_family_t *)afPtr fromAddress:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = [address bytes]; + + if (sockaddrX->sa_family == AF_INET) + { + if ([address length] >= sizeof(struct sockaddr_in)) + { + struct sockaddr_in sockaddr4; + memcpy(&sockaddr4, sockaddrX, sizeof(sockaddr4)); + + if (hostPtr) *hostPtr = [self hostFromSockaddr4:&sockaddr4]; + if (portPtr) *portPtr = [self portFromSockaddr4:&sockaddr4]; + if (afPtr) *afPtr = AF_INET; + + return YES; + } + } + else if (sockaddrX->sa_family == AF_INET6) + { + if ([address length] >= sizeof(struct sockaddr_in6)) + { + struct sockaddr_in6 sockaddr6; + memcpy(&sockaddr6, sockaddrX, sizeof(sockaddr6)); + + if (hostPtr) *hostPtr = [self hostFromSockaddr6:&sockaddr6]; + if (portPtr) *portPtr = [self portFromSockaddr6:&sockaddr6]; + if (afPtr) *afPtr = AF_INET6; + + return YES; + } + } + } + + return NO; +} + ++ (NSData *)CRLFData +{ + return [NSData dataWithBytes:"\x0D\x0A" length:2]; +} + ++ (NSData *)CRData +{ + return [NSData dataWithBytes:"\x0D" length:1]; +} + ++ (NSData *)LFData +{ + return [NSData dataWithBytes:"\x0A" length:1]; +} + ++ (NSData *)ZeroData +{ + return [NSData dataWithBytes:"" length:1]; +} + +@end diff --git a/Vendor/CocoaLumberjack/DDASLLogger.h b/Vendor/CocoaLumberjack/DDASLLogger.h new file mode 100755 index 0000000..2aaf4e3 --- /dev/null +++ b/Vendor/CocoaLumberjack/DDASLLogger.h @@ -0,0 +1,41 @@ +#import +#import + +#import "DDLog.h" + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted + * + * + * This class provides a logger for the Apple System Log facility. + * + * As described in the "Getting Started" page, + * the traditional NSLog() function directs it's output to two places: + * + * - Apple System Log + * - StdErr (if stderr is a TTY) so log statements show up in Xcode console + * + * To duplicate NSLog() functionality you can simply add this logger and a tty logger. + * However, if you instead choose to use file logging (for faster performance), + * you may choose to use a file logger and a tty logger. +**/ + +@interface DDASLLogger : DDAbstractLogger +{ + aslclient client; +} + ++ (instancetype)sharedInstance; + +// Inherited from DDAbstractLogger + +// - (id )logFormatter; +// - (void)setLogFormatter:(id )formatter; + +@end diff --git a/Vendor/CocoaLumberjack/DDASLLogger.m b/Vendor/CocoaLumberjack/DDASLLogger.m new file mode 100755 index 0000000..90beff1 --- /dev/null +++ b/Vendor/CocoaLumberjack/DDASLLogger.m @@ -0,0 +1,100 @@ +#import "DDASLLogger.h" + +#import + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted +**/ + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + + +@implementation DDASLLogger + +static DDASLLogger *sharedInstance; + +/** + * The runtime sends initialize to each class in a program exactly one time just before the class, + * or any class that inherits from it, is sent its first message from within the program. (Thus the + * method may never be invoked if the class is not used.) The runtime sends the initialize message to + * classes in a thread-safe manner. Superclasses receive this message before their subclasses. + * + * This method may also be called directly (assumably by accident), hence the safety mechanism. +**/ ++ (void)initialize +{ + static BOOL initialized = NO; + if (!initialized) + { + initialized = YES; + + sharedInstance = [[[self class] alloc] init]; + } +} + ++ (instancetype)sharedInstance +{ + return sharedInstance; +} + +- (id)init +{ + if (sharedInstance != nil) + { + return nil; + } + + if ((self = [super init])) + { + // A default asl client is provided for the main thread, + // but background threads need to create their own client. + + client = asl_open(NULL, "com.apple.console", 0); + } + return self; +} + +- (void)logMessage:(DDLogMessage *)logMessage +{ + NSString *logMsg = logMessage->logMsg; + + if (formatter) + { + logMsg = [formatter formatLogMessage:logMessage]; + } + + if (logMsg) + { + const char *msg = [logMsg UTF8String]; + + int aslLogLevel; + switch (logMessage->logFlag) + { + // Note: By default ASL will filter anything above level 5 (Notice). + // So our mappings shouldn't go above that level. + + case LOG_FLAG_ERROR : aslLogLevel = ASL_LEVEL_ALERT; break; + case LOG_FLAG_WARN : aslLogLevel = ASL_LEVEL_CRIT; break; + case LOG_FLAG_INFO : aslLogLevel = ASL_LEVEL_ERR; break; + case LOG_FLAG_DEBUG : aslLogLevel = ASL_LEVEL_WARNING; break; + default : aslLogLevel = ASL_LEVEL_NOTICE; break; + } + + asl_log(client, NULL, aslLogLevel, "%s", msg); + } +} + +- (NSString *)loggerName +{ + return @"cocoa.lumberjack.aslLogger"; +} + +@end diff --git a/Vendor/CocoaLumberjack/DDAbstractDatabaseLogger.h b/Vendor/CocoaLumberjack/DDAbstractDatabaseLogger.h new file mode 100755 index 0000000..4e0c33c --- /dev/null +++ b/Vendor/CocoaLumberjack/DDAbstractDatabaseLogger.h @@ -0,0 +1,102 @@ +#import + +#import "DDLog.h" + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted + * + * + * This class provides an abstract implementation of a database logger. + * + * That is, it provides the base implementation for a database logger to build atop of. + * All that is needed for a concrete database logger is to extend this class + * and override the methods in the implementation file that are prefixed with "db_". +**/ + +@interface DDAbstractDatabaseLogger : DDAbstractLogger { +@protected + NSUInteger saveThreshold; + NSTimeInterval saveInterval; + NSTimeInterval maxAge; + NSTimeInterval deleteInterval; + BOOL deleteOnEverySave; + + BOOL saveTimerSuspended; + NSUInteger unsavedCount; + dispatch_time_t unsavedTime; + dispatch_source_t saveTimer; + dispatch_time_t lastDeleteTime; + dispatch_source_t deleteTimer; +} + +/** + * Specifies how often to save the data to disk. + * Since saving is an expensive operation (disk io) it is not done after every log statement. + * These properties allow you to configure how/when the logger saves to disk. + * + * A save is done when either (whichever happens first): + * + * - The number of unsaved log entries reaches saveThreshold + * - The amount of time since the oldest unsaved log entry was created reaches saveInterval + * + * You can optionally disable the saveThreshold by setting it to zero. + * If you disable the saveThreshold you are entirely dependent on the saveInterval. + * + * You can optionally disable the saveInterval by setting it to zero (or a negative value). + * If you disable the saveInterval you are entirely dependent on the saveThreshold. + * + * It's not wise to disable both saveThreshold and saveInterval. + * + * The default saveThreshold is 500. + * The default saveInterval is 60 seconds. +**/ +@property (assign, readwrite) NSUInteger saveThreshold; +@property (assign, readwrite) NSTimeInterval saveInterval; + +/** + * It is likely you don't want the log entries to persist forever. + * Doing so would allow the database to grow infinitely large over time. + * + * The maxAge property provides a way to specify how old a log statement can get + * before it should get deleted from the database. + * + * The deleteInterval specifies how often to sweep for old log entries. + * Since deleting is an expensive operation (disk io) is is done on a fixed interval. + * + * An alternative to the deleteInterval is the deleteOnEverySave option. + * This specifies that old log entries should be deleted during every save operation. + * + * You can optionally disable the maxAge by setting it to zero (or a negative value). + * If you disable the maxAge then old log statements are not deleted. + * + * You can optionally disable the deleteInterval by setting it to zero (or a negative value). + * + * If you disable both deleteInterval and deleteOnEverySave then old log statements are not deleted. + * + * It's not wise to enable both deleteInterval and deleteOnEverySave. + * + * The default maxAge is 7 days. + * The default deleteInterval is 5 minutes. + * The default deleteOnEverySave is NO. +**/ +@property (assign, readwrite) NSTimeInterval maxAge; +@property (assign, readwrite) NSTimeInterval deleteInterval; +@property (assign, readwrite) BOOL deleteOnEverySave; + +/** + * Forces a save of any pending log entries (flushes log entries to disk). +**/ +- (void)savePendingLogEntries; + +/** + * Removes any log entries that are older than maxAge. +**/ +- (void)deleteOldLogEntries; + +@end diff --git a/Vendor/CocoaLumberjack/DDAbstractDatabaseLogger.m b/Vendor/CocoaLumberjack/DDAbstractDatabaseLogger.m new file mode 100755 index 0000000..1410f7a --- /dev/null +++ b/Vendor/CocoaLumberjack/DDAbstractDatabaseLogger.m @@ -0,0 +1,727 @@ +#import "DDAbstractDatabaseLogger.h" +#import + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted +**/ + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +@interface DDAbstractDatabaseLogger () +- (void)destroySaveTimer; +- (void)destroyDeleteTimer; +@end + +#pragma mark - + +@implementation DDAbstractDatabaseLogger + +- (id)init +{ + if ((self = [super init])) + { + saveThreshold = 500; + saveInterval = 60; // 60 seconds + maxAge = (60 * 60 * 24 * 7); // 7 days + deleteInterval = (60 * 5); // 5 minutes + } + return self; +} + +- (void)dealloc +{ + [self destroySaveTimer]; + [self destroyDeleteTimer]; + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Override Me +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)db_log:(DDLogMessage *)logMessage +{ + // Override me and add your implementation. + // + // Return YES if an item was added to the buffer. + // Return NO if the logMessage was ignored. + + return NO; +} + +- (void)db_save +{ + // Override me and add your implementation. +} + +- (void)db_delete +{ + // Override me and add your implementation. +} + +- (void)db_saveAndDelete +{ + // Override me and add your implementation. +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Private API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)performSaveAndSuspendSaveTimer +{ + if (unsavedCount > 0) + { + if (deleteOnEverySave) + [self db_saveAndDelete]; + else + [self db_save]; + } + + unsavedCount = 0; + unsavedTime = 0; + + if (saveTimer && !saveTimerSuspended) + { + dispatch_suspend(saveTimer); + saveTimerSuspended = YES; + } +} + +- (void)performDelete +{ + if (maxAge > 0.0) + { + [self db_delete]; + + lastDeleteTime = dispatch_time(DISPATCH_TIME_NOW, 0); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Timers +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)destroySaveTimer +{ + if (saveTimer) + { + dispatch_source_cancel(saveTimer); + if (saveTimerSuspended) + { + // Must resume a timer before releasing it (or it will crash) + dispatch_resume(saveTimer); + saveTimerSuspended = NO; + } + #if !OS_OBJECT_USE_OBJC + dispatch_release(saveTimer); + #endif + saveTimer = NULL; + } +} + +- (void)updateAndResumeSaveTimer +{ + if ((saveTimer != NULL) && (saveInterval > 0.0) && (unsavedTime > 0.0)) + { + uint64_t interval = (uint64_t)(saveInterval * NSEC_PER_SEC); + dispatch_time_t startTime = dispatch_time(unsavedTime, interval); + + dispatch_source_set_timer(saveTimer, startTime, interval, 1.0); + + if (saveTimerSuspended) + { + dispatch_resume(saveTimer); + saveTimerSuspended = NO; + } + } +} + +- (void)createSuspendedSaveTimer +{ + if ((saveTimer == NULL) && (saveInterval > 0.0)) + { + saveTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, loggerQueue); + + dispatch_source_set_event_handler(saveTimer, ^{ @autoreleasepool { + + [self performSaveAndSuspendSaveTimer]; + + }}); + + saveTimerSuspended = YES; + } +} + +- (void)destroyDeleteTimer +{ + if (deleteTimer) + { + dispatch_source_cancel(deleteTimer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(deleteTimer); + #endif + deleteTimer = NULL; + } +} + +- (void)updateDeleteTimer +{ + if ((deleteTimer != NULL) && (deleteInterval > 0.0) && (maxAge > 0.0)) + { + uint64_t interval = (uint64_t)(deleteInterval * NSEC_PER_SEC); + dispatch_time_t startTime; + + if (lastDeleteTime > 0) + startTime = dispatch_time(lastDeleteTime, interval); + else + startTime = dispatch_time(DISPATCH_TIME_NOW, interval); + + dispatch_source_set_timer(deleteTimer, startTime, interval, 1.0); + } +} + +- (void)createAndStartDeleteTimer +{ + if ((deleteTimer == NULL) && (deleteInterval > 0.0) && (maxAge > 0.0)) + { + deleteTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, loggerQueue); + + if (deleteTimer != NULL) { + dispatch_source_set_event_handler(deleteTimer, ^{ @autoreleasepool { + + [self performDelete]; + + }}); + + [self updateDeleteTimer]; + + if (deleteTimer != NULL) dispatch_resume(deleteTimer); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSUInteger)saveThreshold +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSUInteger result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = saveThreshold; + }); + }); + + return result; +} + +- (void)setSaveThreshold:(NSUInteger)threshold +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (saveThreshold != threshold) + { + saveThreshold = threshold; + + // Since the saveThreshold has changed, + // we check to see if the current unsavedCount has surpassed the new threshold. + // + // If it has, we immediately save the log. + + if ((unsavedCount >= saveThreshold) && (saveThreshold > 0)) + { + [self performSaveAndSuspendSaveTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (NSTimeInterval)saveInterval +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSTimeInterval result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = saveInterval; + }); + }); + + return result; +} + +- (void)setSaveInterval:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ @autoreleasepool { + + // C99 recommended floating point comparison macro + // Read: isLessThanOrGreaterThan(floatA, floatB) + + if (/* saveInterval != interval */ islessgreater(saveInterval, interval)) + { + saveInterval = interval; + + // There are several cases we need to handle here. + // + // 1. If the saveInterval was previously enabled and it just got disabled, + // then we need to stop the saveTimer. (And we might as well release it.) + // + // 2. If the saveInterval was previously disabled and it just got enabled, + // then we need to setup the saveTimer. (Plus we might need to do an immediate save.) + // + // 3. If the saveInterval increased, then we need to reset the timer so that it fires at the later date. + // + // 4. If the saveInterval decreased, then we need to reset the timer so that it fires at an earlier date. + // (Plus we might need to do an immediate save.) + + if (saveInterval > 0.0) + { + if (saveTimer == NULL) + { + // Handles #2 + // + // Since the saveTimer uses the unsavedTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self createSuspendedSaveTimer]; + [self updateAndResumeSaveTimer]; + } + else + { + // Handles #3 + // Handles #4 + // + // Since the saveTimer uses the unsavedTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self updateAndResumeSaveTimer]; + } + } + else if (saveTimer) + { + // Handles #1 + + [self destroySaveTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (NSTimeInterval)maxAge +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSTimeInterval result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = maxAge; + }); + }); + + return result; +} + +- (void)setMaxAge:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ @autoreleasepool { + + // C99 recommended floating point comparison macro + // Read: isLessThanOrGreaterThan(floatA, floatB) + + if (/* maxAge != interval */ islessgreater(maxAge, interval)) + { + NSTimeInterval oldMaxAge = maxAge; + NSTimeInterval newMaxAge = interval; + + maxAge = interval; + + // There are several cases we need to handle here. + // + // 1. If the maxAge was previously enabled and it just got disabled, + // then we need to stop the deleteTimer. (And we might as well release it.) + // + // 2. If the maxAge was previously disabled and it just got enabled, + // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) + // + // 3. If the maxAge was increased, + // then we don't need to do anything. + // + // 4. If the maxAge was decreased, + // then we should do an immediate delete. + + BOOL shouldDeleteNow = NO; + + if (oldMaxAge > 0.0) + { + if (newMaxAge <= 0.0) + { + // Handles #1 + + [self destroyDeleteTimer]; + } + else if (oldMaxAge > newMaxAge) + { + // Handles #4 + shouldDeleteNow = YES; + } + } + else if (newMaxAge > 0.0) + { + // Handles #2 + shouldDeleteNow = YES; + } + + if (shouldDeleteNow) + { + [self performDelete]; + + if (deleteTimer) + [self updateDeleteTimer]; + else + [self createAndStartDeleteTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (NSTimeInterval)deleteInterval +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSTimeInterval result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = deleteInterval; + }); + }); + + return result; +} + +- (void)setDeleteInterval:(NSTimeInterval)interval +{ + dispatch_block_t block = ^{ @autoreleasepool { + + // C99 recommended floating point comparison macro + // Read: isLessThanOrGreaterThan(floatA, floatB) + + if (/* deleteInterval != interval */ islessgreater(deleteInterval, interval)) + { + deleteInterval = interval; + + // There are several cases we need to handle here. + // + // 1. If the deleteInterval was previously enabled and it just got disabled, + // then we need to stop the deleteTimer. (And we might as well release it.) + // + // 2. If the deleteInterval was previously disabled and it just got enabled, + // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) + // + // 3. If the deleteInterval increased, then we need to reset the timer so that it fires at the later date. + // + // 4. If the deleteInterval decreased, then we need to reset the timer so that it fires at an earlier date. + // (Plus we might need to do an immediate delete.) + + if (deleteInterval > 0.0) + { + if (deleteTimer == NULL) + { + // Handles #2 + // + // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, + // if a delete is needed the timer will fire immediately. + + [self createAndStartDeleteTimer]; + } + else + { + // Handles #3 + // Handles #4 + // + // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self updateDeleteTimer]; + } + } + else if (deleteTimer) + { + // Handles #1 + + [self destroyDeleteTimer]; + } + } + }}; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +- (BOOL)deleteOnEverySave +{ + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block BOOL result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(loggerQueue, ^{ + result = deleteOnEverySave; + }); + }); + + return result; +} + +- (void)setDeleteOnEverySave:(BOOL)flag +{ + dispatch_block_t block = ^{ + + deleteOnEverySave = flag; + }; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) + { + block(); + } + else + { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(loggerQueue, block); + }); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Public API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)savePendingLogEntries +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [self performSaveAndSuspendSaveTimer]; + }}; + + if ([self isOnInternalLoggerQueue]) + block(); + else + dispatch_async(loggerQueue, block); +} + +- (void)deleteOldLogEntries +{ + dispatch_block_t block = ^{ @autoreleasepool { + + [self performDelete]; + }}; + + if ([self isOnInternalLoggerQueue]) + block(); + else + dispatch_async(loggerQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark DDLogger +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)didAddLogger +{ + // If you override me be sure to invoke [super didAddLogger]; + + [self createSuspendedSaveTimer]; + + [self createAndStartDeleteTimer]; +} + +- (void)willRemoveLogger +{ + // If you override me be sure to invoke [super willRemoveLogger]; + + [self performSaveAndSuspendSaveTimer]; + + [self destroySaveTimer]; + [self destroyDeleteTimer]; +} + +- (void)logMessage:(DDLogMessage *)logMessage +{ + if ([self db_log:logMessage]) + { + BOOL firstUnsavedEntry = (++unsavedCount == 1); + + if ((unsavedCount >= saveThreshold) && (saveThreshold > 0)) + { + [self performSaveAndSuspendSaveTimer]; + } + else if (firstUnsavedEntry) + { + unsavedTime = dispatch_time(DISPATCH_TIME_NOW, 0); + [self updateAndResumeSaveTimer]; + } + } +} + +- (void)flush +{ + // This method is invoked by DDLog's flushLog method. + // + // It is called automatically when the application quits, + // or if the developer invokes DDLog's flushLog method prior to crashing or something. + + [self performSaveAndSuspendSaveTimer]; +} + +@end diff --git a/Vendor/CocoaLumberjack/DDFileLogger.h b/Vendor/CocoaLumberjack/DDFileLogger.h new file mode 100755 index 0000000..e5f20dc --- /dev/null +++ b/Vendor/CocoaLumberjack/DDFileLogger.h @@ -0,0 +1,369 @@ +#import +#import "DDLog.h" + +@class DDLogFileInfo; + +/** + * Welcome to Cocoa Lumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read the "Getting Started" wiki. + * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted + * + * + * This class provides a logger to write log statements to a file. +**/ + + +// Default configuration and safety/sanity values. +// +// maximumFileSize -> DEFAULT_LOG_MAX_FILE_SIZE +// rollingFrequency -> DEFAULT_LOG_ROLLING_FREQUENCY +// maximumNumberOfLogFiles -> DEFAULT_LOG_MAX_NUM_LOG_FILES +// +// You should carefully consider the proper configuration values for your application. + +#define DEFAULT_LOG_MAX_FILE_SIZE (1024 * 1024) // 1 MB +#define DEFAULT_LOG_ROLLING_FREQUENCY (60 * 60 * 24) // 24 Hours +#define DEFAULT_LOG_MAX_NUM_LOG_FILES (5) // 5 Files + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// The LogFileManager protocol is designed to allow you to control all aspects of your log files. +// +// The primary purpose of this is to allow you to do something with the log files after they have been rolled. +// Perhaps you want to compress them to save disk space. +// Perhaps you want to upload them to an FTP server. +// Perhaps you want to run some analytics on the file. +// +// A default LogFileManager is, of course, provided. +// The default LogFileManager simply deletes old log files according to the maximumNumberOfLogFiles property. +// +// This protocol provides various methods to fetch the list of log files. +// +// There are two variants: sorted and unsorted. +// If sorting is not necessary, the unsorted variant is obviously faster. +// The sorted variant will return an array sorted by when the log files were created, +// with the most recently created log file at index 0, and the oldest log file at the end of the array. +// +// You can fetch only the log file paths (full path including name), log file names (name only), +// or an array of DDLogFileInfo objects. +// The DDLogFileInfo class is documented below, and provides a handy wrapper that +// gives you easy access to various file attributes such as the creation date or the file size. + +@protocol DDLogFileManager +@required + +// Public properties + +/** + * The maximum number of archived log files to keep on disk. + * For example, if this property is set to 3, + * then the LogFileManager will only keep 3 archived log files (plus the current active log file) on disk. + * Once the active log file is rolled/archived, then the oldest of the existing 3 rolled/archived log files is deleted. + * + * You may optionally disable deleting old/rolled/archived log files by setting this property to zero. +**/ +@property (readwrite, assign) NSUInteger maximumNumberOfLogFiles; + +// Public methods + +- (NSString *)logsDirectory; + +- (NSArray *)unsortedLogFilePaths; +- (NSArray *)unsortedLogFileNames; +- (NSArray *)unsortedLogFileInfos; + +- (NSArray *)sortedLogFilePaths; +- (NSArray *)sortedLogFileNames; +- (NSArray *)sortedLogFileInfos; + +// Private methods (only to be used by DDFileLogger) + +- (NSString *)createNewLogFile; + +@optional + +// Notifications from DDFileLogger + +- (void)didArchiveLogFile:(NSString *)logFilePath; +- (void)didRollAndArchiveLogFile:(NSString *)logFilePath; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Default log file manager. + * + * All log files are placed inside the logsDirectory. + * If a specific logsDirectory isn't specified, the default directory is used. + * On Mac, this is in ~/Library/Logs/. + * On iPhone, this is in ~/Library/Caches/Logs. + * + * Log files are named "