teachers-assistant

[RADIOACTIVE] oh boy did i make bad apps back in the day
git clone git://git.figbert.com/teachers-assistant.git
Log | Files | Refs

commit c333c18e2bd88a33b2b4586bf9da93ee4a4871e3
Author: Naomi Welner <naomi@Naomis-MacBook-Air.local>
Date:   Mon, 11 Mar 2019 12:35:37 -0700

Adding existing files

Diffstat:
A.DS_Store | 0
APodfile | 14++++++++++++++
APodfile.lock | 27+++++++++++++++++++++++++++
APods/GTMSessionFetcher/LICENSE | 202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GTMSessionFetcher/README.md | 23+++++++++++++++++++++++
APods/GTMSessionFetcher/Source/GTMGatherInputStream.h | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GTMSessionFetcher/Source/GTMGatherInputStream.m | 185+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GTMSessionFetcher/Source/GTMMIMEDocument.h | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GTMSessionFetcher/Source/GTMMIMEDocument.m | 631+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GTMSessionFetcher/Source/GTMReadMonitorInputStream.h | 49+++++++++++++++++++++++++++++++++++++++++++++++++
APods/GTMSessionFetcher/Source/GTMReadMonitorInputStream.m | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GTMSessionFetcher/Source/GTMSessionFetcher.h | 1305+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GTMSessionFetcher/Source/GTMSessionFetcher.m | 4579+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.h | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.m | 982+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GTMSessionFetcher/Source/GTMSessionFetcherService.h | 193+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GTMSessionFetcher/Source/GTMSessionFetcherService.m | 1365+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GTMSessionFetcher/Source/GTMSessionUploadFetcher.h | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GTMSessionFetcher/Source/GTMSessionUploadFetcher.m | 1954+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/LICENSE | 202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/README.md | 48++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/GTLRDefines.h | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRBatchQuery.h | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRBatchQuery.m | 179+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRBatchResult.h | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRBatchResult.m | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRDateTime.h | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRDateTime.m | 373+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRDuration.h | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRDuration.m | 222+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRErrorObject.h | 116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRErrorObject.m | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRObject.h | 317+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRObject.m | 760+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRQuery.h | 253+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRQuery.m | 313+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRRuntimeCommon.h | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRRuntimeCommon.m | 1060+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRService.h | 879+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRService.m | 2883+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRUploadParameters.h | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Objects/GTLRUploadParameters.m | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Utilities/GTLRBase64.h | 29+++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Utilities/GTLRBase64.m | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Utilities/GTLRFramework.h | 34++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Utilities/GTLRFramework.m | 44++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Utilities/GTLRURITemplate.h | 48++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Utilities/GTLRURITemplate.m | 511+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Utilities/GTLRUtilities.h | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/GoogleAPIClientForREST/Source/Utilities/GTLRUtilities.m | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/Manifest.lock | 27+++++++++++++++++++++++++++
APods/Pods.xcodeproj/project.pbxproj | 1140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/Pods.xcodeproj/xcuserdata/naomi.xcuserdatad/xcschemes/GTMSessionFetcher.xcscheme | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/Pods.xcodeproj/xcuserdata/naomi.xcuserdatad/xcschemes/GoogleAPIClientForREST.xcscheme | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/Pods.xcodeproj/xcuserdata/naomi.xcuserdatad/xcschemes/Pods-TeachersAssistant.xcscheme | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/Pods.xcodeproj/xcuserdata/naomi.xcuserdatad/xcschemes/Pods-TeachersAssistantTests.xcscheme | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/Pods.xcodeproj/xcuserdata/naomi.xcuserdatad/xcschemes/xcschememanagement.plist | 39+++++++++++++++++++++++++++++++++++++++
APods/Target Support Files/GTMSessionFetcher/GTMSessionFetcher-Info.plist | 26++++++++++++++++++++++++++
APods/Target Support Files/GTMSessionFetcher/GTMSessionFetcher-dummy.m | 5+++++
APods/Target Support Files/GTMSessionFetcher/GTMSessionFetcher-prefix.pch | 12++++++++++++
APods/Target Support Files/GTMSessionFetcher/GTMSessionFetcher-umbrella.h | 23+++++++++++++++++++++++
APods/Target Support Files/GTMSessionFetcher/GTMSessionFetcher.modulemap | 6++++++
APods/Target Support Files/GTMSessionFetcher/GTMSessionFetcher.xcconfig | 9+++++++++
APods/Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST-Info.plist | 26++++++++++++++++++++++++++
APods/Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST-dummy.m | 5+++++
APods/Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST-prefix.pch | 12++++++++++++
APods/Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST-umbrella.h | 31+++++++++++++++++++++++++++++++
APods/Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST.modulemap | 6++++++
APods/Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST.xcconfig | 9+++++++++
APods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-Info.plist | 26++++++++++++++++++++++++++
APods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-acknowledgements.markdown | 415+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-acknowledgements.plist | 453+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-dummy.m | 5+++++
APods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-frameworks.sh | 165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-umbrella.h | 16++++++++++++++++
APods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant.debug.xcconfig | 9+++++++++
APods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant.modulemap | 6++++++
APods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant.release.xcconfig | 9+++++++++
APods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests-Info.plist | 26++++++++++++++++++++++++++
APods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests-acknowledgements.markdown | 3+++
APods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests-acknowledgements.plist | 29+++++++++++++++++++++++++++++
APods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests-dummy.m | 5+++++
APods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests-umbrella.h | 16++++++++++++++++
APods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests.debug.xcconfig | 9+++++++++
APods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests.modulemap | 6++++++
APods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests.release.xcconfig | 9+++++++++
ATeachers' Assistant/.DS_Store | 0
ATeachers' Assistant/AppDelegate.swift | 46++++++++++++++++++++++++++++++++++++++++++++++
ATeachers' Assistant/Assets.xcassets/AppIcon.appiconset/Contents.json | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATeachers' Assistant/Assets.xcassets/Contents.json | 7+++++++
ATeachers' Assistant/Base.lproj/LaunchScreen.storyboard | 48++++++++++++++++++++++++++++++++++++++++++++++++
ATeachers' Assistant/Base.lproj/Main.storyboard | 260+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATeachers' Assistant/Images/Brandeis_Web_HiRes_Orange.png | 0
ATeachers' Assistant/Images/DarkBlue-Logo.PNG | 0
ATeachers' Assistant/Images/Gray-Logo.png | 0
ATeachers' Assistant/Images/Green-Logo.PNG | 0
ATeachers' Assistant/Images/LightBlue-Logo.PNG | 0
ATeachers' Assistant/Images/LiveList (Dark Blue).png | 0
ATeachers' Assistant/Images/LiveList (Gray).png | 0
ATeachers' Assistant/Images/LiveList (Green).png | 0
ATeachers' Assistant/Images/LiveList (Light Blue).png | 0
ATeachers' Assistant/Images/LiveList (Orange).png | 0
ATeachers' Assistant/Images/LiveList (Pink).png | 0
ATeachers' Assistant/Images/Pink-Logo.PNG | 0
ATeachers' Assistant/Images/Settings Button.png | 0
ATeachers' Assistant/Images/Sign-In (Dark Blue).png | 0
ATeachers' Assistant/Images/Sign-In (Gray).png | 0
ATeachers' Assistant/Images/Sign-In (Green).png | 0
ATeachers' Assistant/Images/Sign-In (Light Blue).png | 0
ATeachers' Assistant/Images/Sign-In (Orange).png | 0
ATeachers' Assistant/Images/Sign-In (Pink).png | 0
ATeachers' Assistant/Images/Sign-Out (Dark Blue).png | 0
ATeachers' Assistant/Images/Sign-Out (Gray).png | 0
ATeachers' Assistant/Images/Sign-Out (Green).png | 0
ATeachers' Assistant/Images/Sign-Out (Light Blue).png | 0
ATeachers' Assistant/Images/Sign-Out (Orange).png | 0
ATeachers' Assistant/Images/Sign-Out (Pink).png | 0
ATeachers' Assistant/Info.plist | 47+++++++++++++++++++++++++++++++++++++++++++++++
ATeachers' Assistant/ViewController.swift | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
ATeachers' Assistant/ViewControllerHome.swift | 182+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATeachers' Assistant/ViewControllerSettings.swift | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATeachers' AssistantTests/Info.plist | 22++++++++++++++++++++++
ATeachers' AssistantTests/Teachers__AssistantTests.swift | 36++++++++++++++++++++++++++++++++++++
ATeachersAssistant.xcodeproj/project.pbxproj | 663+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATeachersAssistant.xcodeproj/project.xcworkspace/contents.xcworkspacedata | 7+++++++
ATeachersAssistant.xcodeproj/xcuserdata/naomi.xcuserdatad/xcschemes/xcschememanagement.plist | 19+++++++++++++++++++
ATeachersAssistant.xcworkspace/contents.xcworkspacedata | 10++++++++++
ATeachersAssistant.xcworkspace/xcuserdata/naomi.xcuserdatad/UserInterfaceState.xcuserstate | 0
128 files changed, 26266 insertions(+), 0 deletions(-)

diff --git a/.DS_Store b/.DS_Store Binary files differ. diff --git a/Podfile b/Podfile @@ -0,0 +1,14 @@ +# Uncomment the next line to define a global platform for your project +platform :ios, '9.0' + +target 'TeachersAssistant' do + # Comment the next line if you're not using Swift and don't want to use dynamic frameworks + use_frameworks! + pod 'GoogleAPIClientForREST', '~> 1.3.8' + + target 'TeachersAssistantTests' do + inherit! :search_paths + # Pods for testing + end + +end diff --git a/Podfile.lock b/Podfile.lock @@ -0,0 +1,27 @@ +PODS: + - GoogleAPIClientForREST (1.3.8): + - GoogleAPIClientForREST/Core (= 1.3.8) + - GTMSessionFetcher (>= 1.1.7) + - GoogleAPIClientForREST/Core (1.3.8): + - GTMSessionFetcher (>= 1.1.7) + - GTMSessionFetcher (1.2.1): + - GTMSessionFetcher/Full (= 1.2.1) + - GTMSessionFetcher/Core (1.2.1) + - GTMSessionFetcher/Full (1.2.1): + - GTMSessionFetcher/Core (= 1.2.1) + +DEPENDENCIES: + - GoogleAPIClientForREST (~> 1.3.8) + +SPEC REPOS: + https://github.com/cocoapods/specs.git: + - GoogleAPIClientForREST + - GTMSessionFetcher + +SPEC CHECKSUMS: + GoogleAPIClientForREST: 5447a194eae517986cafe6421a5330b80b820591 + GTMSessionFetcher: 32aeca0aa144acea523e1c8e053089dec2cb98ca + +PODFILE CHECKSUM: dbc5c2766ede4673e953e610de8390e5215f6c55 + +COCOAPODS: 1.6.1 diff --git a/Pods/GTMSessionFetcher/LICENSE b/Pods/GTMSessionFetcher/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Pods/GTMSessionFetcher/README.md b/Pods/GTMSessionFetcher/README.md @@ -0,0 +1,23 @@ +# Google Toolbox for Mac - Session Fetcher # + +**Project site** <https://github.com/google/gtm-session-fetcher><br> +**Discussion group** <http://groups.google.com/group/google-toolbox-for-mac> + +[![Build Status](https://travis-ci.org/google/gtm-session-fetcher.svg?branch=master)](https://travis-ci.org/google/gtm-session-fetcher) + +`GTMSessionFetcher` makes it easy for Cocoa applications to perform http +operations. The fetcher is implemented as a wrapper on `NSURLSession`, so its +behavior is asynchronous and uses operating-system settings on iOS and Mac OS X. + +Features include: +- Simple to build; only one source/header file pair is required +- Simple to use: takes just two lines of code to fetch a request +- Supports upload and download sessions +- Flexible cookie storage +- Automatic retry on errors, with exponential backoff +- Support for generating multipart MIME upload streams +- Easy, convenient logging of http requests and responses +- Supports plug-in authentication such as with GTMAppAuth +- Easily testable; self-mocking +- Automatic rate limiting when created by the `GTMSessionFetcherService` factory class +- Fully independent of other projects diff --git a/Pods/GTMSessionFetcher/Source/GTMGatherInputStream.h b/Pods/GTMSessionFetcher/Source/GTMGatherInputStream.h @@ -0,0 +1,52 @@ +/* Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// The GTMGatherInput stream is an input stream implementation that is to be +// instantiated with an NSArray of NSData objects. It works in the traditional +// scatter/gather vector I/O model. Rather than allocating a big NSData object +// to hold all of the data and performing a copy into that object, the +// GTMGatherInputStream will maintain a reference to the NSArray and read from +// each NSData in turn as the read method is called. You should not alter the +// underlying set of NSData objects until all read operations on this input +// stream have completed. + +#import <Foundation/Foundation.h> + +#ifndef GTM_NONNULL + #if defined(__has_attribute) + #if __has_attribute(nonnull) + #define GTM_NONNULL(x) __attribute__((nonnull x)) + #else + #define GTM_NONNULL(x) + #endif + #else + #define GTM_NONNULL(x) + #endif +#endif + +// Avoid multiple declaration of this class. +// +// Note: This should match the declaration of GTMGatherInputStream in GTMMIMEDocument.m + +#ifndef GTM_GATHERINPUTSTREAM_DECLARED +#define GTM_GATHERINPUTSTREAM_DECLARED + +@interface GTMGatherInputStream : NSInputStream <NSStreamDelegate> + ++ (NSInputStream *)streamWithArray:(NSArray *)dataArray GTM_NONNULL((1)); + +@end + +#endif // GTM_GATHERINPUTSTREAM_DECLARED diff --git a/Pods/GTMSessionFetcher/Source/GTMGatherInputStream.m b/Pods/GTMSessionFetcher/Source/GTMGatherInputStream.m @@ -0,0 +1,185 @@ +/* Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "GTMGatherInputStream.h" + +@implementation GTMGatherInputStream { + NSArray *_dataArray; // NSDatas that should be "gathered" and streamed. + NSUInteger _arrayIndex; // Index in the array of the current NSData. + long long _dataOffset; // Offset in the current NSData we are processing. + NSStreamStatus _streamStatus; + id<NSStreamDelegate> __weak _delegate; // Stream delegate, defaults to self. +} + ++ (NSInputStream *)streamWithArray:(NSArray *)dataArray { + return [(GTMGatherInputStream *)[self alloc] initWithArray:dataArray]; +} + +- (instancetype)initWithArray:(NSArray *)dataArray { + self = [super init]; + if (self) { + _dataArray = dataArray; + _delegate = self; // An NSStream's default delegate should be self. + } + return self; +} + +#pragma mark - NSStream + +- (void)open { + _arrayIndex = 0; + _dataOffset = 0; + _streamStatus = NSStreamStatusOpen; +} + +- (void)close { + _streamStatus = NSStreamStatusClosed; +} + +- (id<NSStreamDelegate>)delegate { + return _delegate; +} + +- (void)setDelegate:(id<NSStreamDelegate>)delegate { + if (delegate == nil) { + _delegate = self; + } else { + _delegate = delegate; + } +} + +- (id)propertyForKey:(NSString *)key { + if ([key isEqual:NSStreamFileCurrentOffsetKey]) { + return @([self absoluteOffset]); + } + return nil; +} + +- (BOOL)setProperty:(id)property forKey:(NSString *)key { + if ([key isEqual:NSStreamFileCurrentOffsetKey]) { + NSNumber *absoluteOffsetNumber = property; + [self setAbsoluteOffset:absoluteOffsetNumber.longLongValue]; + return YES; + } + return NO; +} + +- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode { +} + +- (void)removeFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode { +} + +- (NSStreamStatus)streamStatus { + return _streamStatus; +} + +- (NSError *)streamError { + return nil; +} + +#pragma mark - NSInputStream + +- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len { + NSInteger bytesRead = 0; + NSUInteger bytesRemaining = len; + + // Read bytes from the currently-indexed array. + while ((bytesRemaining > 0) && (_arrayIndex < _dataArray.count)) { + NSData *data = [_dataArray objectAtIndex:_arrayIndex]; + + NSUInteger dataLen = data.length; + NSUInteger dataBytesLeft = dataLen - (NSUInteger)_dataOffset; + + NSUInteger bytesToCopy = MIN(bytesRemaining, dataBytesLeft); + NSRange range = NSMakeRange((NSUInteger) _dataOffset, bytesToCopy); + + [data getBytes:(buffer + bytesRead) range:range]; + + bytesRead += bytesToCopy; + _dataOffset += bytesToCopy; + bytesRemaining -= bytesToCopy; + + if (_dataOffset == (long long)dataLen) { + _dataOffset = 0; + _arrayIndex++; + } + } + if (_arrayIndex >= _dataArray.count) { + _streamStatus = NSStreamStatusAtEnd; + } + return bytesRead; +} + +- (BOOL)getBuffer:(uint8_t **)buffer length:(NSUInteger *)len { + return NO; // We don't support this style of reading. +} + +- (BOOL)hasBytesAvailable { + // If we return no, the read never finishes, even if we've already delivered all the bytes. + return YES; +} + +#pragma mark - NSStreamDelegate + +- (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent { + id<NSStreamDelegate> delegate = _delegate; + if (delegate != self) { + [delegate stream:self handleEvent:streamEvent]; + } +} + +#pragma mark - Private + +- (long long)absoluteOffset { + long long absoluteOffset = 0; + NSUInteger index = 0; + for (NSData *data in _dataArray) { + if (index >= _arrayIndex) { + break; + } + absoluteOffset += data.length; + ++index; + } + absoluteOffset += _dataOffset; + return absoluteOffset; +} + +- (void)setAbsoluteOffset:(long long)absoluteOffset { + if (absoluteOffset < 0) { + absoluteOffset = 0; + } + _arrayIndex = 0; + _dataOffset = absoluteOffset; + for (NSData *data in _dataArray) { + long long dataLen = (long long) data.length; + if (dataLen > _dataOffset) { + break; + } + _arrayIndex++; + _dataOffset -= dataLen; + } + if (_arrayIndex == _dataArray.count) { + if (_dataOffset > 0) { + _dataOffset = 0; + } + } +} + +@end diff --git a/Pods/GTMSessionFetcher/Source/GTMMIMEDocument.h b/Pods/GTMSessionFetcher/Source/GTMMIMEDocument.h @@ -0,0 +1,148 @@ +/* Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This is a simple class to create or parse a MIME document. + +// To create a MIME document, allocate a new GTMMIMEDocument and start adding parts. +// When you are done adding parts, call generateInputStream or generateDispatchData. +// +// A good reference for MIME is http://en.wikipedia.org/wiki/MIME + +#import <Foundation/Foundation.h> + +#ifndef GTM_NONNULL + #if defined(__has_attribute) + #if __has_attribute(nonnull) + #define GTM_NONNULL(x) __attribute__((nonnull x)) + #else + #define GTM_NONNULL(x) + #endif + #else + #define GTM_NONNULL(x) + #endif +#endif + +#ifndef GTM_DECLARE_GENERICS + #if __has_feature(objc_generics) + #define GTM_DECLARE_GENERICS 1 + #else + #define GTM_DECLARE_GENERICS 0 + #endif +#endif + +#ifndef GTM_NSArrayOf + #if GTM_DECLARE_GENERICS + #define GTM_NSArrayOf(value) NSArray<value> + #define GTM_NSDictionaryOf(key, value) NSDictionary<key, value> + #else + #define GTM_NSArrayOf(value) NSArray + #define GTM_NSDictionaryOf(key, value) NSDictionary + #endif // GTM_DECLARE_GENERICS +#endif // GTM_NSArrayOf + + +// GTMMIMEDocumentPart represents a part of a MIME document. +// +// +[GTMMIMEDocument MIMEPartsWithBoundary:data:] returns an array of these. +@interface GTMMIMEDocumentPart : NSObject + +@property(nonatomic, readonly) GTM_NSDictionaryOf(NSString *, NSString *) *headers; +@property(nonatomic, readonly) NSData *headerData; +@property(nonatomic, readonly) NSData *body; +@property(nonatomic, readonly) NSUInteger length; + ++ (instancetype)partWithHeaders:(NSDictionary *)headers body:(NSData *)body; + +@end + +@interface GTMMIMEDocument : NSObject + +// Get or set the unique boundary for the parts that have been added. +// +// When creating a MIME document from parts, this is typically calculated +// automatically after all parts have been added. +@property(nonatomic, copy) NSString *boundary; + +#pragma mark - Methods for Creating a MIME Document + ++ (instancetype)MIMEDocument; + +// Adds a new part to this mime document with the given headers and body. +// The headers keys and values should be NSStrings. +// Adding a part may cause the boundary string to change. +- (void)addPartWithHeaders:(GTM_NSDictionaryOf(NSString *, NSString *) *)headers + body:(NSData *)body GTM_NONNULL((1,2)); + +// An inputstream that can be used to efficiently read the contents of the MIME document. +// +// Any parameter may be null if the result is not wanted. +- (void)generateInputStream:(NSInputStream **)outStream + length:(unsigned long long *)outLength + boundary:(NSString **)outBoundary; + +// A dispatch_data_t with the contents of the MIME document. +// +// Note: dispatch_data_t is one-way toll-free bridged so the result +// may be cast directly to NSData *. +// +// Any parameter may be null if the result is not wanted. +- (void)generateDispatchData:(dispatch_data_t *)outDispatchData + length:(unsigned long long *)outLength + boundary:(NSString **)outBoundary; + +// Utility method for making a header section, including trailing newlines. ++ (NSData *)dataWithHeaders:(GTM_NSDictionaryOf(NSString *, NSString *) *)headers; + +#pragma mark - Methods for Parsing a MIME Document + +// Method for parsing out an array of MIME parts from a MIME document. +// +// Returns an array of GTMMIMEDocumentParts. Returns nil if no part can +// be found. ++ (GTM_NSArrayOf(GTMMIMEDocumentPart *) *)MIMEPartsWithBoundary:(NSString *)boundary + data:(NSData *)fullDocumentData; + +// Utility method for efficiently searching possibly discontiguous NSData +// for occurrences of target byte. This method does not "flatten" an NSData +// that is composed of discontiguous blocks. +// +// The byte offsets of non-overlapping occurrences of the target are returned as +// NSNumbers in the array. ++ (void)searchData:(NSData *)data + targetBytes:(const void *)targetBytes + targetLength:(NSUInteger)targetLength + foundOffsets:(GTM_NSArrayOf(NSNumber *) **)outFoundOffsets; + +// Utility method to parse header bytes into an NSDictionary. ++ (GTM_NSDictionaryOf(NSString *, NSString *) *)headersWithData:(NSData *)data; + +// ------ UNIT TESTING ONLY BELOW ------ + +// Internal methods, exposed for unit testing only. +- (void)seedRandomWith:(u_int32_t)seed; + ++ (NSUInteger)findBytesWithNeedle:(const unsigned char *)needle + needleLength:(NSUInteger)needleLength + haystack:(const unsigned char *)haystack + haystackLength:(NSUInteger)haystackLength + foundOffset:(NSUInteger *)foundOffset; + ++ (void)searchData:(NSData *)data + targetBytes:(const void *)targetBytes + targetLength:(NSUInteger)targetLength + foundOffsets:(GTM_NSArrayOf(NSNumber *) **)outFoundOffsets + foundBlockNumbers:(GTM_NSArrayOf(NSNumber *) **)outFoundBlockNumbers; + +@end diff --git a/Pods/GTMSessionFetcher/Source/GTMMIMEDocument.m b/Pods/GTMSessionFetcher/Source/GTMMIMEDocument.m @@ -0,0 +1,631 @@ +/* Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "GTMMIMEDocument.h" + +// Avoid a hard dependency on GTMGatherInputStream. +#ifndef GTM_GATHERINPUTSTREAM_DECLARED +#define GTM_GATHERINPUTSTREAM_DECLARED + +@interface GTMGatherInputStream : NSInputStream <NSStreamDelegate> + ++ (NSInputStream *)streamWithArray:(NSArray *)dataArray GTM_NONNULL((1)); + +@end +#endif // GTM_GATHERINPUTSTREAM_DECLARED + +// FindBytes +// +// Helper routine to search for the existence of a set of bytes (needle) within +// a presumed larger set of bytes (haystack). Can find the first part of the +// needle at the very end of the haystack. +// +// Returns the needle length on complete success, the number of bytes matched +// if a partial needle was found at the end of the haystack, and 0 on failure. +static NSUInteger FindBytes(const unsigned char *needle, NSUInteger needleLen, + const unsigned char *haystack, NSUInteger haystackLen, + NSUInteger *foundOffset); + +// SearchDataForBytes +// +// This implements the functionality of the +searchData: methods below. See the documentation +// for those methods. +static void SearchDataForBytes(NSData *data, const void *targetBytes, NSUInteger targetLength, + NSMutableArray *foundOffsets, NSMutableArray *foundBlockNumbers); + +@implementation GTMMIMEDocumentPart { + NSDictionary *_headers; + NSData *_headerData; // Header content including the ending "\r\n". + NSData *_bodyData; +} + +@synthesize headers = _headers, + headerData = _headerData, + body = _bodyData; + +@dynamic length; + ++ (instancetype)partWithHeaders:(NSDictionary *)headers body:(NSData *)body { + return [[self alloc] initWithHeaders:headers body:body]; +} + +- (instancetype)initWithHeaders:(NSDictionary *)headers body:(NSData *)body { + self = [super init]; + if (self) { + _bodyData = body; + _headers = headers; + } + return self; +} + +// Returns true if the part's header or data contain the given set of bytes. +// +// NOTE: We assume that the 'bytes' we are checking for do not contain "\r\n", +// so we don't need to check the concatenation of the header and body bytes. +- (BOOL)containsBytes:(const unsigned char *)bytes length:(NSUInteger)length { + // This uses custom search code rather than strcpy because the encoded data may contain + // null values. + NSData *headerData = self.headerData; + return (FindBytes(bytes, length, headerData.bytes, headerData.length, NULL) == length || + FindBytes(bytes, length, _bodyData.bytes, _bodyData.length, NULL) == length); +} + +- (NSData *)headerData { + if (!_headerData) { + _headerData = [GTMMIMEDocument dataWithHeaders:_headers]; + } + return _headerData; +} + +- (NSData *)body { + return _bodyData; +} + +- (NSUInteger)length { + return _headerData.length + _bodyData.length; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ %p (headers %lu keys, body %lu bytes)", + [self class], self, (unsigned long)_headers.count, + (unsigned long)_bodyData.length]; +} + +- (BOOL)isEqual:(GTMMIMEDocumentPart *)other { + if (self == other) return YES; + if (![other isKindOfClass:[GTMMIMEDocumentPart class]]) return NO; + return ((_bodyData == other->_bodyData || [_bodyData isEqual:other->_bodyData]) + && (_headers == other->_headers || [_headers isEqual:other->_headers])); +} + +- (NSUInteger)hash { + return _bodyData.hash | _headers.hash; +} + +@end + +@implementation GTMMIMEDocument { + NSMutableArray *_parts; // Ordered array of GTMMIMEDocumentParts. + unsigned long long _length; // Length in bytes of the document. + NSString *_boundary; + u_int32_t _randomSeed; // For testing. +} + ++ (instancetype)MIMEDocument { + return [[self alloc] init]; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _parts = [[NSMutableArray alloc] init]; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ %p (%lu parts)", + [self class], self, (unsigned long)_parts.count]; +} + +#pragma mark - Joining Parts + +// Adds a new part to this mime document with the given headers and body. +- (void)addPartWithHeaders:(NSDictionary *)headers body:(NSData *)body { + GTMMIMEDocumentPart *part = [GTMMIMEDocumentPart partWithHeaders:headers body:body]; + [_parts addObject:part]; + _boundary = nil; +} + +// For unit testing only, seeds the random number generator so that we will +// have reproducible boundary strings. +- (void)seedRandomWith:(u_int32_t)seed { + _randomSeed = seed; + _boundary = nil; +} + +- (u_int32_t)random { + if (_randomSeed) { + // For testing only. + return _randomSeed++; + } else { + return arc4random(); + } +} + +// Computes the mime boundary to use. This should only be called +// after all the desired document parts have been added since it must compute +// a boundary that does not exist in the document data. +- (NSString *)boundary { + if (_boundary) { + return _boundary; + } + + // Use an easily-readable boundary string. + NSString *const kBaseBoundary = @"END_OF_PART"; + + _boundary = kBaseBoundary; + + // If the boundary isn't unique, append random numbers, up to 10 attempts; + // if that's still not unique, use a random number sequence instead, and call it good. + BOOL didCollide = NO; + + const int maxTries = 10; // Arbitrarily chosen maximum attempts. + for (int tries = 0; tries < maxTries; ++tries) { + + NSData *data = [_boundary dataUsingEncoding:NSUTF8StringEncoding]; + const void *dataBytes = data.bytes; + NSUInteger dataLen = data.length; + + for (GTMMIMEDocumentPart *part in _parts) { + didCollide = [part containsBytes:dataBytes length:dataLen]; + if (didCollide) break; + } + + if (!didCollide) break; // We're fine, no more attempts needed. + + // Try again with a random number appended. + _boundary = [NSString stringWithFormat:@"%@_%08x", kBaseBoundary, [self random]]; + } + + if (didCollide) { + // Fallback... two random numbers. + _boundary = [NSString stringWithFormat:@"%08x_tedborg_%08x", [self random], [self random]]; + } + return _boundary; +} + +- (void)setBoundary:(NSString *)str { + _boundary = [str copy]; +} + +// Internal method. +- (void)generateDataArray:(NSMutableArray *)dataArray + length:(unsigned long long *)outLength + boundary:(NSString **)outBoundary { + + // The input stream is of the form: + // --boundary + // [part_1_headers] + // [part_1_data] + // --boundary + // [part_2_headers] + // [part_2_data] + // --boundary-- + + // First we set up our boundary NSData objects. + NSString *boundary = self.boundary; + + NSString *mainBoundary = [NSString stringWithFormat:@"\r\n--%@\r\n", boundary]; + NSString *endBoundary = [NSString stringWithFormat:@"\r\n--%@--\r\n", boundary]; + + NSData *mainBoundaryData = [mainBoundary dataUsingEncoding:NSUTF8StringEncoding]; + NSData *endBoundaryData = [endBoundary dataUsingEncoding:NSUTF8StringEncoding]; + + // Now we add them all in proper order to our dataArray. + unsigned long long length = 0; + + for (GTMMIMEDocumentPart *part in _parts) { + [dataArray addObject:mainBoundaryData]; + [dataArray addObject:part.headerData]; + [dataArray addObject:part.body]; + + length += part.length + mainBoundaryData.length; + } + + [dataArray addObject:endBoundaryData]; + length += endBoundaryData.length; + + if (outLength) *outLength = length; + if (outBoundary) *outBoundary = boundary; +} + +- (void)generateInputStream:(NSInputStream **)outStream + length:(unsigned long long *)outLength + boundary:(NSString **)outBoundary { + NSMutableArray *dataArray = outStream ? [NSMutableArray array] : nil; + [self generateDataArray:dataArray + length:outLength + boundary:outBoundary]; + + if (outStream) { + Class streamClass = NSClassFromString(@"GTMGatherInputStream"); + NSAssert(streamClass != nil, @"GTMGatherInputStream not available."); + + *outStream = [streamClass streamWithArray:dataArray]; + } +} + +- (void)generateDispatchData:(dispatch_data_t *)outDispatchData + length:(unsigned long long *)outLength + boundary:(NSString **)outBoundary { + NSMutableArray *dataArray = outDispatchData ? [NSMutableArray array] : nil; + [self generateDataArray:dataArray + length:outLength + boundary:outBoundary]; + + if (outDispatchData) { + // Create an empty data accumulator. + dispatch_data_t dataAccumulator; + + dispatch_queue_t bgQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + + for (NSData *partData in dataArray) { + __block NSData *immutablePartData = [partData copy]; + dispatch_data_t newDataPart = + dispatch_data_create(immutablePartData.bytes, immutablePartData.length, bgQueue, ^{ + // We want the data retained until this block executes. + immutablePartData = nil; + }); + + if (dataAccumulator == nil) { + // First part. + dataAccumulator = newDataPart; + } else { + // Append the additional part. + dataAccumulator = dispatch_data_create_concat(dataAccumulator, newDataPart); + } + } + *outDispatchData = dataAccumulator; + } +} + ++ (NSData *)dataWithHeaders:(NSDictionary *)headers { + // Generate the header data by coalescing the dictionary as lines of "key: value\r\n". + NSMutableString* headerString = [NSMutableString string]; + + // Sort the header keys so we have a deterministic order for unit testing. + SEL sortSel = @selector(caseInsensitiveCompare:); + NSArray *sortedKeys = [headers.allKeys sortedArrayUsingSelector:sortSel]; + + for (NSString *key in sortedKeys) { + NSString *value = [headers objectForKey:key]; + +#if DEBUG + // Look for troublesome characters in the header keys & values. + NSCharacterSet *badKeyChars = [NSCharacterSet characterSetWithCharactersInString:@":\r\n"]; + NSCharacterSet *badValueChars = [NSCharacterSet characterSetWithCharactersInString:@"\r\n"]; + + NSRange badRange = [key rangeOfCharacterFromSet:badKeyChars]; + NSAssert(badRange.location == NSNotFound, @"invalid key: %@", key); + + badRange = [value rangeOfCharacterFromSet:badValueChars]; + NSAssert(badRange.location == NSNotFound, @"invalid value: %@", value); +#endif + + [headerString appendFormat:@"%@: %@\r\n", key, value]; + } + // Headers end with an extra blank line. + [headerString appendString:@"\r\n"]; + + NSData *result = [headerString dataUsingEncoding:NSUTF8StringEncoding]; + return result; +} + +#pragma mark - Separating Parts + ++ (NSArray *)MIMEPartsWithBoundary:(NSString *)boundary + data:(NSData *)fullDocumentData { + // In MIME documents, the boundary is preceded by CRLF and two dashes, and followed + // at the end by two dashes. + NSData *boundaryData = [boundary dataUsingEncoding:NSUTF8StringEncoding]; + NSUInteger boundaryLength = boundaryData.length; + + NSMutableArray *foundBoundaryOffsets; + [self searchData:fullDocumentData + targetBytes:boundaryData.bytes + targetLength:boundaryLength + foundOffsets:&foundBoundaryOffsets]; + + // According to rfc1341, ignore anything before the first boundary, or after the last, though two + // dashes are expected to follow the last boundary. + if (foundBoundaryOffsets.count < 2) { + return nil; + } + + // Wrap the full document data with a dispatch_data_t for more efficient slicing + // and dicing. + dispatch_data_t dataWrapper; + if ([fullDocumentData conformsToProtocol:@protocol(OS_dispatch_data)]) { + dataWrapper = (dispatch_data_t)fullDocumentData; + } else { + // A no-op self invocation on fullDocumentData will keep it retained until the block is invoked. + dispatch_queue_t bgQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dataWrapper = dispatch_data_create(fullDocumentData.bytes, + fullDocumentData.length, + bgQueue, ^{ [fullDocumentData self]; }); + } + NSMutableArray *parts; + NSInteger previousBoundaryOffset = -1; + NSInteger partCounter = -1; + NSInteger numberOfPartsWithHeaders = 0; + for (NSNumber *currentBoundaryOffset in foundBoundaryOffsets) { + ++partCounter; + if (previousBoundaryOffset == -1) { + // This is the first boundary. + previousBoundaryOffset = currentBoundaryOffset.integerValue; + continue; + } else { + // Create a part data subrange between the previous boundary and this one. + // + // The last four bytes before a boundary are CRLF--. + // The first two bytes following a boundary are either CRLF or, for the last boundary, --. + NSInteger previousPartDataStartOffset = + previousBoundaryOffset + (NSInteger)boundaryLength + 2; + NSInteger previousPartDataEndOffset = currentBoundaryOffset.integerValue - 4; + NSInteger previousPartDataLength = previousPartDataEndOffset - previousPartDataStartOffset; + + if (previousPartDataLength < 2) { + // The preceding part was too short to be useful. +#if DEBUG + NSLog(@"MIME part %ld has %ld bytes", (long)partCounter - 1, + (long)previousPartDataLength); +#endif + } else { + if (!parts) parts = [NSMutableArray array]; + + dispatch_data_t partData = + dispatch_data_create_subrange(dataWrapper, + (size_t)previousPartDataStartOffset, (size_t)previousPartDataLength); + // Scan the part data for the separator between headers and body. After the CRLF, + // either the headers start immediately, or there's another CRLF and there are no headers. + // + // We need to map the part data to get the first two bytes. (Or we could cast it to + // NSData and get the bytes pointer of that.) If we're concerned that a single part + // data may be expensive to map, we could make a subrange here for just the first two bytes, + // and map that two-byte subrange. + const void *partDataBuffer; + size_t partDataBufferSize; + dispatch_data_t mappedPartData NS_VALID_UNTIL_END_OF_SCOPE = + dispatch_data_create_map(partData, &partDataBuffer, &partDataBufferSize); + dispatch_data_t bodyData; + NSDictionary *headers; + BOOL hasAnotherCRLF = (((char *)partDataBuffer)[0] == '\r' + && ((char *)partDataBuffer)[1] == '\n'); + mappedPartData = nil; + + if (hasAnotherCRLF) { + // There are no headers; skip the CRLF to get to the body, and leave headers nil. + bodyData = dispatch_data_create_subrange(partData, 2, (size_t)previousPartDataLength - 2); + } else { + // There are part headers. They are separated from body data by CRLFCRLF. + NSArray *crlfOffsets; + [self searchData:(NSData *)partData + targetBytes:"\r\n\r\n" + targetLength:4 + foundOffsets:&crlfOffsets]; + if (crlfOffsets.count == 0) { +#if DEBUG + // We could not distinguish body and headers. + NSLog(@"MIME part %ld lacks a header separator: %@", (long)partCounter - 1, + [[NSString alloc] initWithData:(NSData *)partData encoding:NSUTF8StringEncoding]); +#endif + } else { + NSInteger headerSeparatorOffset = ((NSNumber *)crlfOffsets.firstObject).integerValue; + dispatch_data_t headerData = + dispatch_data_create_subrange(partData, 0, (size_t)headerSeparatorOffset); + headers = [self headersWithData:(NSData *)headerData]; + + bodyData = dispatch_data_create_subrange(partData, (size_t)headerSeparatorOffset + 4, + (size_t)(previousPartDataLength - (headerSeparatorOffset + 4))); + + numberOfPartsWithHeaders++; + } // crlfOffsets.count == 0 + } // hasAnotherCRLF + GTMMIMEDocumentPart *part = [GTMMIMEDocumentPart partWithHeaders:headers + body:(NSData *)bodyData]; + [parts addObject:part]; + } // previousPartDataLength < 2 + previousBoundaryOffset = currentBoundaryOffset.integerValue; + } + } +#if DEBUG + // In debug builds, warn if a reasonably long document lacks any CRLF characters. + if (numberOfPartsWithHeaders == 0) { + NSUInteger length = fullDocumentData.length; + if (length > 20) { // Reasonably long. + NSMutableArray *foundCRLFs; + [self searchData:fullDocumentData + targetBytes:"\r\n" + targetLength:2 + foundOffsets:&foundCRLFs]; + if (foundCRLFs.count == 0) { + // Parts were logged above (due to lacking header separators.) + NSLog(@"Warning: MIME document lacks any headers (may have wrong line endings)"); + } + } + } +#endif // DEBUG + return parts; +} + +// Efficiently search the supplied data for the target bytes. +// +// This uses enumerateByteRangesUsingBlock: to scan for bytes. It can find +// the target even if it spans multiple separate byte ranges. +// +// Returns an array of found byte offset values, as NSNumbers. ++ (void)searchData:(NSData *)data + targetBytes:(const void *)targetBytes + targetLength:(NSUInteger)targetLength + foundOffsets:(GTM_NSArrayOf(NSNumber *) **)outFoundOffsets { + NSMutableArray *foundOffsets = [NSMutableArray array]; + SearchDataForBytes(data, targetBytes, targetLength, foundOffsets, NULL); + *outFoundOffsets = foundOffsets; +} + + +// This version of searchData: also returns the block numbers (0-based) where the +// target was found, used for testing that the supplied dispatch_data buffer +// has not been flattened. ++ (void)searchData:(NSData *)data + targetBytes:(const void *)targetBytes + targetLength:(NSUInteger)targetLength + foundOffsets:(GTM_NSArrayOf(NSNumber *) **)outFoundOffsets + foundBlockNumbers:(GTM_NSArrayOf(NSNumber *) **)outFoundBlockNumbers { + NSMutableArray *foundOffsets = [NSMutableArray array]; + NSMutableArray *foundBlockNumbers = [NSMutableArray array]; + + SearchDataForBytes(data, targetBytes, targetLength, foundOffsets, foundBlockNumbers); + *outFoundOffsets = foundOffsets; + *outFoundBlockNumbers = foundBlockNumbers; +} + +static void SearchDataForBytes(NSData *data, const void *targetBytes, NSUInteger targetLength, + NSMutableArray *foundOffsets, NSMutableArray *foundBlockNumbers) { + __block NSUInteger priorPartialMatchAmount = 0; + __block NSInteger priorPartialMatchStartingBlockNumber = -1; + __block NSInteger blockNumber = -1; + + [data enumerateByteRangesUsingBlock:^(const void *bytes, + NSRange byteRange, + BOOL *stop) { + // Search for the first character in the current range. + const void *ptr = bytes; + NSInteger remainingInCurrentRange = (NSInteger)byteRange.length; + ++blockNumber; + + if (priorPartialMatchAmount > 0) { + NSUInteger amountRemainingToBeMatched = targetLength - priorPartialMatchAmount; + NSUInteger remainingFoundOffset; + NSUInteger amountMatched = FindBytes(targetBytes + priorPartialMatchAmount, + amountRemainingToBeMatched, + ptr, (NSUInteger)remainingInCurrentRange, &remainingFoundOffset); + if (amountMatched == 0 || remainingFoundOffset > 0) { + // No match of the rest of the prior partial match in this range. + } else if (amountMatched < amountRemainingToBeMatched) { + // Another partial match; we're done with this range. + priorPartialMatchAmount = priorPartialMatchAmount + amountMatched; + return; + } else { + // The offset is in an earlier range. + NSUInteger offset = byteRange.location - priorPartialMatchAmount; + [foundOffsets addObject:@(offset)]; + [foundBlockNumbers addObject:@(priorPartialMatchStartingBlockNumber)]; + priorPartialMatchStartingBlockNumber = -1; + } + priorPartialMatchAmount = 0; + } + + while (remainingInCurrentRange > 0) { + NSUInteger offsetFromPtr; + NSUInteger amountMatched = FindBytes(targetBytes, targetLength, ptr, + (NSUInteger)remainingInCurrentRange, &offsetFromPtr); + if (amountMatched == 0) { + // No match in this range. + return; + } + if (amountMatched < targetLength) { + // Found a partial target. If there's another range, we'll check for the rest. + priorPartialMatchAmount = amountMatched; + priorPartialMatchStartingBlockNumber = blockNumber; + return; + } + // Found the full target. + NSUInteger globalOffset = byteRange.location + (NSUInteger)(ptr - bytes) + offsetFromPtr; + + [foundOffsets addObject:@(globalOffset)]; + [foundBlockNumbers addObject:@(blockNumber)]; + + ptr += targetLength + offsetFromPtr; + remainingInCurrentRange -= (targetLength + offsetFromPtr); + } + }]; +} + +// Internal method only for testing; this calls through the static method. ++ (NSUInteger)findBytesWithNeedle:(const unsigned char *)needle + needleLength:(NSUInteger)needleLength + haystack:(const unsigned char *)haystack + haystackLength:(NSUInteger)haystackLength + foundOffset:(NSUInteger *)foundOffset { + return FindBytes(needle, needleLength, haystack, haystackLength, foundOffset); +} + +// Utility method to parse header bytes into an NSDictionary. ++ (NSDictionary *)headersWithData:(NSData *)data { + NSString *headersString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + if (!headersString) return nil; + + NSMutableDictionary *headers = [NSMutableDictionary dictionary]; + NSScanner *scanner = [NSScanner scannerWithString:headersString]; + // The scanner is skipping leading whitespace and newline characters by default. + NSCharacterSet *newlineCharacters = [NSCharacterSet newlineCharacterSet]; + NSString *key; + NSString *value; + while ([scanner scanUpToString:@":" intoString:&key] + && [scanner scanString:@":" intoString:NULL] + && [scanner scanUpToCharactersFromSet:newlineCharacters intoString:&value]) { + [headers setObject:value forKey:key]; + // Discard the trailing newline. + [scanner scanCharactersFromSet:newlineCharacters intoString:NULL]; + } + return headers; +} + +@end + +// Return how much of the needle was found in the haystack. +// +// If the result is less than needleLen, then the beginning of the needle +// was found at the end of the haystack. +static NSUInteger FindBytes(const unsigned char* needle, NSUInteger needleLen, + const unsigned char* haystack, NSUInteger haystackLen, + NSUInteger *foundOffset) { + const unsigned char *ptr = haystack; + NSInteger remain = (NSInteger)haystackLen; + // Assume memchr is an efficient way to find a match for the first + // byte of the needle, and memcmp is an efficient way to compare a + // range of bytes. + while (remain > 0 && (ptr = memchr(ptr, needle[0], (size_t)remain)) != 0) { + // The first character is present. + NSUInteger offset = (NSUInteger)(ptr - haystack); + remain = (NSInteger)(haystackLen - offset); + + NSUInteger amountToCompare = MIN((NSUInteger)remain, needleLen); + if (memcmp(ptr, needle, amountToCompare) == 0) { + if (foundOffset) *foundOffset = offset; + return amountToCompare; + } + ptr++; + remain--; + } + if (foundOffset) *foundOffset = 0; + return 0; +} diff --git a/Pods/GTMSessionFetcher/Source/GTMReadMonitorInputStream.h b/Pods/GTMSessionFetcher/Source/GTMReadMonitorInputStream.h @@ -0,0 +1,49 @@ +/* Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#ifndef GTM_NONNULL + #if defined(__has_attribute) + #if __has_attribute(nonnull) + #define GTM_NONNULL(x) __attribute__((nonnull x)) + #else + #define GTM_NONNULL(x) + #endif + #else + #define GTM_NONNULL(x) + #endif +#endif + + +@interface GTMReadMonitorInputStream : NSInputStream <NSStreamDelegate> + ++ (instancetype)inputStreamWithStream:(NSInputStream *)input GTM_NONNULL((1)); + +- (instancetype)initWithStream:(NSInputStream *)input GTM_NONNULL((1)); + +// The read monitor selector is called when bytes have been read. It should have this signature: +// +// - (void)inputStream:(GTMReadMonitorInputStream *)stream +// readIntoBuffer:(uint8_t *)buffer +// length:(int64_t)length; + +@property(atomic, weak) id readDelegate; +@property(atomic, assign) SEL readSelector; + +// Modes for invoking callbacks, when necessary. +@property(atomic, strong) NSArray *runLoopModes; + +@end diff --git a/Pods/GTMSessionFetcher/Source/GTMReadMonitorInputStream.m b/Pods/GTMSessionFetcher/Source/GTMReadMonitorInputStream.m @@ -0,0 +1,190 @@ +/* Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "GTMReadMonitorInputStream.h" + +@implementation GTMReadMonitorInputStream { + NSInputStream *_inputStream; // Encapsulated stream that does the work. + + NSThread *_thread; // Thread in which this object was created. + NSArray *_runLoopModes; // Modes for calling callbacks, when necessary. +} + + +@synthesize readDelegate = _readDelegate; +@synthesize readSelector = _readSelector; +@synthesize runLoopModes = _runLoopModes; + +// We'll forward all unhandled messages to the NSInputStream class or to the encapsulated input +// stream. This is needed for all messages sent to NSInputStream which aren't handled by our +// superclass; that includes various private run loop calls. ++ (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { + return [NSInputStream methodSignatureForSelector:selector]; +} + ++ (void)forwardInvocation:(NSInvocation*)invocation { + [invocation invokeWithTarget:[NSInputStream class]]; +} + +- (BOOL)respondsToSelector:(SEL)selector { + return [_inputStream respondsToSelector:selector]; +} + +- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector { + return [_inputStream methodSignatureForSelector:selector]; +} + +- (void)forwardInvocation:(NSInvocation*)invocation { + [invocation invokeWithTarget:_inputStream]; +} + +#pragma mark - + ++ (instancetype)inputStreamWithStream:(NSInputStream *)input { + return [[self alloc] initWithStream:input]; +} + +- (instancetype)initWithStream:(NSInputStream *)input { + self = [super init]; + if (self) { + _inputStream = input; + _thread = [NSThread currentThread]; + } + return self; +} + +- (instancetype)init { + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +#pragma mark - + +- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len { + // Read from the encapsulated stream. + NSInteger numRead = [_inputStream read:buffer maxLength:len]; + if (numRead > 0) { + if (_readDelegate && _readSelector) { + // Call the read selector with the buffer and number of bytes actually read into it. + BOOL isOnOriginalThread = [_thread isEqual:[NSThread currentThread]]; + if (isOnOriginalThread) { + // Invoke immediately. + NSData *data = [NSData dataWithBytesNoCopy:buffer + length:(NSUInteger)numRead + freeWhenDone:NO]; + [self invokeReadSelectorWithBuffer:data]; + } else { + // Copy the buffer into an NSData to be retained by the performSelector, + // and invoke on the proper thread. + SEL sel = @selector(invokeReadSelectorWithBuffer:); + NSData *data = [NSData dataWithBytes:buffer length:(NSUInteger)numRead]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + if (_runLoopModes) { + [self performSelector:sel + onThread:_thread + withObject:data + waitUntilDone:NO + modes:_runLoopModes]; + } else { + [self performSelector:sel + onThread:_thread + withObject:data + waitUntilDone:NO]; + } +#pragma clang diagnostic pop + } + } + } + return numRead; +} + +- (void)invokeReadSelectorWithBuffer:(NSData *)data { + const void *buffer = data.bytes; + int64_t length = (int64_t)data.length; + + id argSelf = self; + id readDelegate = _readDelegate; + if (readDelegate) { + NSMethodSignature *signature = [readDelegate methodSignatureForSelector:_readSelector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:_readSelector]; + [invocation setTarget:readDelegate]; + [invocation setArgument:&argSelf atIndex:2]; + [invocation setArgument:&buffer atIndex:3]; + [invocation setArgument:&length atIndex:4]; + [invocation invoke]; + } +} + +- (BOOL)getBuffer:(uint8_t **)buffer length:(NSUInteger *)len { + return [_inputStream getBuffer:buffer length:len]; +} + +- (BOOL)hasBytesAvailable { + return [_inputStream hasBytesAvailable]; +} + +#pragma mark Standard messages + +// Pass expected messages to our encapsulated stream. +// +// We want our encapsulated NSInputStream to handle the standard messages; +// we don't want the superclass to handle them. +- (void)open { + [_inputStream open]; +} + +- (void)close { + [_inputStream close]; +} + +- (id)delegate { + return [_inputStream delegate]; +} + +- (void)setDelegate:(id)delegate { + [_inputStream setDelegate:delegate]; +} + +- (id)propertyForKey:(NSString *)key { + return [_inputStream propertyForKey:key]; +} + +- (BOOL)setProperty:(id)property forKey:(NSString *)key { + return [_inputStream setProperty:property forKey:key]; +} + +- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode { + [_inputStream scheduleInRunLoop:aRunLoop forMode:mode]; +} + +- (void)removeFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode { + [_inputStream removeFromRunLoop:aRunLoop forMode:mode]; +} + +- (NSStreamStatus)streamStatus { + return [_inputStream streamStatus]; +} + +- (NSError *)streamError { + return [_inputStream streamError]; +} + +@end diff --git a/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.h b/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.h @@ -0,0 +1,1305 @@ +/* Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// GTMSessionFetcher is a wrapper around NSURLSession for http operations. +// +// What does this offer on top of of NSURLSession? +// +// - Block-style callbacks for useful functionality like progress rather +// than delegate methods. +// - Out-of-process uploads and downloads using NSURLSession, including +// management of fetches after relaunch. +// - Integration with GTMAppAuth for invisible management and refresh of +// authorization tokens. +// - Pretty-printed http logging. +// - Cookies handling that does not interfere with or get interfered with +// by WebKit cookies or on Mac by Safari and other apps. +// - Credentials handling for the http operation. +// - Rate-limiting and cookie grouping when fetchers are created with +// GTMSessionFetcherService. +// +// If the bodyData or bodyFileURL property is set, then a POST request is assumed. +// +// Each fetcher is assumed to be for a one-shot fetch request; don't reuse the object +// for a second fetch. +// +// The fetcher will be self-retained as long as a connection is pending. +// +// To keep user activity private, URLs must have an https scheme (unless the property +// allowedInsecureSchemes is set to permit the scheme.) +// +// Callbacks will be released when the fetch completes or is stopped, so there is no need +// to use weak self references in the callback blocks. +// +// Sample usage: +// +// _fetcherService = [[GTMSessionFetcherService alloc] init]; +// +// GTMSessionFetcher *myFetcher = [_fetcherService fetcherWithURLString:myURLString]; +// myFetcher.retryEnabled = YES; +// myFetcher.comment = @"First profile image"; +// +// // Optionally specify a file URL or NSData for the request body to upload. +// myFetcher.bodyData = [postString dataUsingEncoding:NSUTF8StringEncoding]; +// +// [myFetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { +// if (error != nil) { +// // Server status code or network error. +// // +// // If the domain is kGTMSessionFetcherStatusDomain then the error code +// // is a failure status from the server. +// } else { +// // Fetch succeeded. +// } +// }]; +// +// There is also a beginFetch call that takes a pointer and selector for the completion handler; +// a pointer and selector is a better style when the callback is a substantial, separate method. +// +// NOTE: Fetches may retrieve data from the server even though the server +// returned an error, so the criteria for success is a non-nil error. +// The completion handler is called when the server status is >= 300 with an NSError +// having domain kGTMSessionFetcherStatusDomain and code set to the server status. +// +// Status codes are at <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html> +// +// +// Background session support: +// +// Out-of-process uploads and downloads may be created by setting the fetcher's +// useBackgroundSession property. Data to be uploaded should be provided via +// the uploadFileURL property; the download destination should be specified with +// the destinationFileURL. NOTE: Background upload files should be in a location +// that will be valid even after the device is restarted, so the file should not +// be uploaded from a system temporary or cache directory. +// +// Background session transfers are slower, and should typically be used only +// for very large downloads or uploads (hundreds of megabytes). +// +// When background sessions are used in iOS apps, the application delegate must +// pass through the parameters from UIApplicationDelegate's +// application:handleEventsForBackgroundURLSession:completionHandler: to the +// fetcher class. +// +// When the application has been relaunched, it may also create a new fetcher +// instance to handle completion of the transfers. +// +// - (void)application:(UIApplication *)application +// handleEventsForBackgroundURLSession:(NSString *)identifier +// completionHandler:(void (^)())completionHandler { +// // Application was re-launched on completing an out-of-process download. +// +// // Pass the URLSession info related to this re-launch to the fetcher class. +// [GTMSessionFetcher application:application +// handleEventsForBackgroundURLSession:identifier +// completionHandler:completionHandler]; +// +// // Get a fetcher related to this re-launch and re-hook up a completionHandler to it. +// GTMSessionFetcher *fetcher = [GTMSessionFetcher fetcherWithSessionIdentifier:identifier]; +// NSURL *destinationFileURL = fetcher.destinationFileURL; +// fetcher.completionHandler = ^(NSData *data, NSError *error) { +// [self downloadCompletedToFile:destinationFileURL error:error]; +// }; +// } +// +// +// Threading and queue support: +// +// Networking always happens on a background thread; there is no advantage to +// changing thread or queue to create or start a fetcher. +// +// Callbacks are run on the main thread; alternatively, the app may set the +// fetcher's callbackQueue to a dispatch queue. +// +// Once the fetcher's beginFetch method has been called, the fetcher's methods and +// properties may be accessed from any thread. +// +// Downloading to disk: +// +// To have downloaded data saved directly to disk, specify a file URL for the +// destinationFileURL property. +// +// HTTP methods and headers: +// +// Alternative HTTP methods, like PUT, and custom headers can be specified by +// creating the fetcher with an appropriate NSMutableURLRequest. +// +// +// Caching: +// +// The fetcher avoids caching. That is best for API requests, but may hurt +// repeat fetches of static data. Apps may enable a persistent disk cache by +// customizing the config: +// +// fetcher.configurationBlock = ^(GTMSessionFetcher *configFetcher, +// NSURLSessionConfiguration *config) { +// config.URLCache = [NSURLCache sharedURLCache]; +// }; +// +// Or use the standard system config to share cookie storage with web views +// and to enable disk caching: +// +// fetcher.configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; +// +// +// Cookies: +// +// There are three supported mechanisms for remembering cookies between fetches. +// +// By default, a standalone GTMSessionFetcher uses a mutable array held +// statically to track cookies for all instantiated fetchers. This avoids +// cookies being set by servers for the application from interfering with +// Safari and WebKit cookie settings, and vice versa. +// The fetcher cookies are lost when the application quits. +// +// To rely instead on WebKit's global NSHTTPCookieStorage, set the fetcher's +// cookieStorage property: +// myFetcher.cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; +// +// To share cookies with other apps, use the method introduced in iOS 9/OS X 10.11: +// myFetcher.cookieStorage = +// [NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:kMyCompanyContainedID]; +// +// To ignore existing cookies and only have cookies related to the single fetch +// be applied, make a temporary cookie storage object: +// myFetcher.cookieStorage = [[GTMSessionCookieStorage alloc] init]; +// +// Note: cookies set while following redirects will be sent to the server, as +// the redirects are followed by the fetcher. +// +// To completely disable cookies, similar to setting cookieStorageMethod to +// kGTMHTTPFetcherCookieStorageMethodNone, adjust the session configuration +// appropriately in the fetcher or fetcher service: +// fetcher.configurationBlock = ^(GTMSessionFetcher *configFetcher, +// NSURLSessionConfiguration *config) { +// config.HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicyNever; +// config.HTTPShouldSetCookies = NO; +// }; +// +// If the fetcher is created from a GTMSessionFetcherService object +// then the cookie storage mechanism is set to use the cookie storage in the +// service object rather than the static storage. Disabling cookies in the +// session configuration set on a service object will disable cookies for all +// fetchers created from that GTMSessionFetcherService object, since the session +// configuration is propagated to the fetcher. +// +// +// Monitoring data transfers. +// +// The fetcher supports a variety of properties for progress monitoring +// progress with callback blocks. +// GTMSessionFetcherSendProgressBlock sendProgressBlock +// GTMSessionFetcherReceivedProgressBlock receivedProgressBlock +// GTMSessionFetcherDownloadProgressBlock downloadProgressBlock +// +// If supplied by the server, the anticipated total download size is available +// as [[myFetcher response] expectedContentLength] (and may be -1 for unknown +// download sizes.) +// +// +// Automatic retrying of fetches +// +// The fetcher can optionally create a timer and reattempt certain kinds of +// fetch failures (status codes 408, request timeout; 502, gateway failure; +// 503, service unavailable; 504, gateway timeout; networking errors +// NSURLErrorTimedOut and NSURLErrorNetworkConnectionLost.) The user may +// set a retry selector to customize the type of errors which will be retried. +// +// Retries are done in an exponential-backoff fashion (that is, after 1 second, +// 2, 4, 8, and so on.) +// +// Enabling automatic retries looks like this: +// myFetcher.retryEnabled = YES; +// +// With retries enabled, the completion callbacks are called only +// when no more retries will be attempted. Calling the fetcher's stopFetching +// method will terminate the retry timer, without the finished or failure +// selectors being invoked. +// +// Optionally, the client may set the maximum retry interval: +// myFetcher.maxRetryInterval = 60.0; // in seconds; default is 60 seconds +// // for downloads, 600 for uploads +// +// Servers should never send a 400 or 500 status for errors that are retryable +// by clients, as those values indicate permanent failures. In nearly all +// cases, the default standard retry behavior is correct for clients, and no +// custom client retry behavior is needed or appropriate. Servers that send +// non-retryable status codes and expect the client to retry the request are +// faulty. +// +// Still, the client may provide a block to determine if a status code or other +// error should be retried. The block returns YES to set the retry timer or NO +// to fail without additional fetch attempts. +// +// The retry method may return the |suggestedWillRetry| argument to get the +// default retry behavior. Server status codes are present in the +// error argument, and have the domain kGTMSessionFetcherStatusDomain. The +// user's method may look something like this: +// +// myFetcher.retryBlock = ^(BOOL suggestedWillRetry, NSError *error, +// GTMSessionFetcherRetryResponse response) { +// // Perhaps examine error.domain and error.code, or fetcher.retryCount +// // +// // Respond with YES to start the retry timer, NO to proceed to the failure +// // callback, or suggestedWillRetry to get default behavior for the +// // current error domain and code values. +// response(suggestedWillRetry); +// }; + + +#import <Foundation/Foundation.h> + +#if TARGET_OS_IPHONE +#import <UIKit/UIKit.h> +#endif +#if TARGET_OS_WATCH +#import <WatchKit/WatchKit.h> +#endif + +// By default it is stripped from non DEBUG builds. Developers can override +// this in their project settings. +#ifndef STRIP_GTM_FETCH_LOGGING + #if !DEBUG + #define STRIP_GTM_FETCH_LOGGING 1 + #else + #define STRIP_GTM_FETCH_LOGGING 0 + #endif +#endif + +// Logs in debug builds. +#ifndef GTMSESSION_LOG_DEBUG + #if DEBUG + #define GTMSESSION_LOG_DEBUG(...) NSLog(__VA_ARGS__) + #else + #define GTMSESSION_LOG_DEBUG(...) do { } while (0) + #endif +#endif + +// Asserts in debug builds (or logs in debug builds if GTMSESSION_ASSERT_AS_LOG +// or NS_BLOCK_ASSERTIONS are defined.) +#ifndef GTMSESSION_ASSERT_DEBUG + #if DEBUG && !defined(NS_BLOCK_ASSERTIONS) && !GTMSESSION_ASSERT_AS_LOG + #undef GTMSESSION_ASSERT_AS_LOG + #define GTMSESSION_ASSERT_AS_LOG 1 + #endif + + #if DEBUG && !GTMSESSION_ASSERT_AS_LOG + #define GTMSESSION_ASSERT_DEBUG(...) NSAssert(__VA_ARGS__) + #elif DEBUG + #define GTMSESSION_ASSERT_DEBUG(pred, ...) if (!(pred)) { NSLog(__VA_ARGS__); } + #else + #define GTMSESSION_ASSERT_DEBUG(pred, ...) do { } while (0) + #endif +#endif + +// Asserts in debug builds, logs in release builds (or logs in debug builds if +// GTMSESSION_ASSERT_AS_LOG is defined.) +#ifndef GTMSESSION_ASSERT_DEBUG_OR_LOG + #if DEBUG && !GTMSESSION_ASSERT_AS_LOG + #define GTMSESSION_ASSERT_DEBUG_OR_LOG(...) NSAssert(__VA_ARGS__) + #else + #define GTMSESSION_ASSERT_DEBUG_OR_LOG(pred, ...) if (!(pred)) { NSLog(__VA_ARGS__); } + #endif +#endif + +// Macro useful for examining messages from NSURLSession during debugging. +#if 0 +#define GTM_LOG_SESSION_DELEGATE(...) GTMSESSION_LOG_DEBUG(__VA_ARGS__) +#else +#define GTM_LOG_SESSION_DELEGATE(...) +#endif + +#ifndef GTM_NULLABLE + #if __has_feature(nullability) // Available starting in Xcode 6.3 + #define GTM_NULLABLE_TYPE __nullable + #define GTM_NONNULL_TYPE __nonnull + #define GTM_NULLABLE nullable + #define GTM_NONNULL_DECL nonnull // GTM_NONNULL is used by GTMDefines.h + #define GTM_NULL_RESETTABLE null_resettable + + #define GTM_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN + #define GTM_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END + #else + #define GTM_NULLABLE_TYPE + #define GTM_NONNULL_TYPE + #define GTM_NULLABLE + #define GTM_NONNULL_DECL + #define GTM_NULL_RESETTABLE + #define GTM_ASSUME_NONNULL_BEGIN + #define GTM_ASSUME_NONNULL_END + #endif // __has_feature(nullability) +#endif // GTM_NULLABLE + +#if (TARGET_OS_TV \ + || TARGET_OS_WATCH \ + || (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_12) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_12) \ + || (TARGET_OS_IPHONE && defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0)) +#define GTMSESSION_DEPRECATE_ON_2016_SDKS(_MSG) __attribute__((deprecated("" _MSG))) +#else +#define GTMSESSION_DEPRECATE_ON_2016_SDKS(_MSG) +#endif + +#ifndef GTM_DECLARE_GENERICS + #if __has_feature(objc_generics) + #define GTM_DECLARE_GENERICS 1 + #else + #define GTM_DECLARE_GENERICS 0 + #endif +#endif + +#ifndef GTM_NSArrayOf + #if GTM_DECLARE_GENERICS + #define GTM_NSArrayOf(value) NSArray<value> + #define GTM_NSDictionaryOf(key, value) NSDictionary<key, value> + #else + #define GTM_NSArrayOf(value) NSArray + #define GTM_NSDictionaryOf(key, value) NSDictionary + #endif // __has_feature(objc_generics) +#endif // GTM_NSArrayOf + +// For iOS, the fetcher can declare itself a background task to allow fetches +// to finish when the app leaves the foreground. +// +// (This is unrelated to providing a background configuration, which allows +// out-of-process uploads and downloads.) +// +// To disallow use of background tasks during fetches, the target should define +// GTM_BACKGROUND_TASK_FETCHING to 0, or alternatively may set the +// skipBackgroundTask property to YES. +#if TARGET_OS_IPHONE && !TARGET_OS_WATCH && !defined(GTM_BACKGROUND_TASK_FETCHING) + #define GTM_BACKGROUND_TASK_FETCHING 1 +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#if (TARGET_OS_TV \ + || TARGET_OS_WATCH \ + || (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_11) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_11) \ + || (TARGET_OS_IPHONE && defined(__IPHONE_9_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0)) + #ifndef GTM_USE_SESSION_FETCHER + #define GTM_USE_SESSION_FETCHER 1 + #endif +#endif + +#if !defined(GTMBridgeFetcher) + // These bridge macros should be identical in GTMHTTPFetcher.h and GTMSessionFetcher.h + #if GTM_USE_SESSION_FETCHER + // Macros to new fetcher class. + #define GTMBridgeFetcher GTMSessionFetcher + #define GTMBridgeFetcherService GTMSessionFetcherService + #define GTMBridgeFetcherServiceProtocol GTMSessionFetcherServiceProtocol + #define GTMBridgeAssertValidSelector GTMSessionFetcherAssertValidSelector + #define GTMBridgeCookieStorage GTMSessionCookieStorage + #define GTMBridgeCleanedUserAgentString GTMFetcherCleanedUserAgentString + #define GTMBridgeSystemVersionString GTMFetcherSystemVersionString + #define GTMBridgeApplicationIdentifier GTMFetcherApplicationIdentifier + #define kGTMBridgeFetcherStatusDomain kGTMSessionFetcherStatusDomain + #define kGTMBridgeFetcherStatusBadRequest GTMSessionFetcherStatusBadRequest + #else + // Macros to old fetcher class. + #define GTMBridgeFetcher GTMHTTPFetcher + #define GTMBridgeFetcherService GTMHTTPFetcherService + #define GTMBridgeFetcherServiceProtocol GTMHTTPFetcherServiceProtocol + #define GTMBridgeAssertValidSelector GTMAssertSelectorNilOrImplementedWithArgs + #define GTMBridgeCookieStorage GTMCookieStorage + #define GTMBridgeCleanedUserAgentString GTMCleanedUserAgentString + #define GTMBridgeSystemVersionString GTMSystemVersionString + #define GTMBridgeApplicationIdentifier GTMApplicationIdentifier + #define kGTMBridgeFetcherStatusDomain kGTMHTTPFetcherStatusDomain + #define kGTMBridgeFetcherStatusBadRequest kGTMHTTPFetcherStatusBadRequest + #endif // GTM_USE_SESSION_FETCHER +#endif + +GTM_ASSUME_NONNULL_BEGIN + +// Notifications +// +// Fetch started and stopped, and fetch retry delay started and stopped. +extern NSString *const kGTMSessionFetcherStartedNotification; +extern NSString *const kGTMSessionFetcherStoppedNotification; +extern NSString *const kGTMSessionFetcherRetryDelayStartedNotification; +extern NSString *const kGTMSessionFetcherRetryDelayStoppedNotification; + +// Completion handler notification. This is intended for use by code capturing +// and replaying fetch requests and results for testing. For fetches where +// destinationFileURL or accumulateDataBlock is set for the fetcher, the data +// will be nil for successful fetches. +// +// This notification is posted on the main thread. +extern NSString *const kGTMSessionFetcherCompletionInvokedNotification; +extern NSString *const kGTMSessionFetcherCompletionDataKey; +extern NSString *const kGTMSessionFetcherCompletionErrorKey; + +// Constants for NSErrors created by the fetcher (excluding server status errors, +// and error objects originating in the OS.) +extern NSString *const kGTMSessionFetcherErrorDomain; + +// The fetcher turns server error status values (3XX, 4XX, 5XX) into NSErrors +// with domain kGTMSessionFetcherStatusDomain. +// +// Any server response body data accompanying the status error is added to the +// userInfo dictionary with key kGTMSessionFetcherStatusDataKey. +extern NSString *const kGTMSessionFetcherStatusDomain; +extern NSString *const kGTMSessionFetcherStatusDataKey; +extern NSString *const kGTMSessionFetcherStatusDataContentTypeKey; + +// When a fetch fails with an error, these keys are included in the error userInfo +// dictionary if retries were attempted. +extern NSString *const kGTMSessionFetcherNumberOfRetriesDoneKey; +extern NSString *const kGTMSessionFetcherElapsedIntervalWithRetriesKey; + +// Background session support requires access to NSUserDefaults. +// If [NSUserDefaults standardUserDefaults] doesn't yield the correct NSUserDefaults for your usage, +// ie for an App Extension, then implement this class/method to return the correct NSUserDefaults. +// https://developer.apple.com/library/ios/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html#//apple_ref/doc/uid/TP40014214-CH21-SW6 +@interface GTMSessionFetcherUserDefaultsFactory : NSObject + ++ (NSUserDefaults *)fetcherUserDefaults; + +@end + +#ifdef __cplusplus +} +#endif + +typedef NS_ENUM(NSInteger, GTMSessionFetcherError) { + GTMSessionFetcherErrorDownloadFailed = -1, + GTMSessionFetcherErrorUploadChunkUnavailable = -2, + GTMSessionFetcherErrorBackgroundExpiration = -3, + GTMSessionFetcherErrorBackgroundFetchFailed = -4, + GTMSessionFetcherErrorInsecureRequest = -5, + GTMSessionFetcherErrorTaskCreationFailed = -6, +}; + +typedef NS_ENUM(NSInteger, GTMSessionFetcherStatus) { + // Standard http status codes. + GTMSessionFetcherStatusNotModified = 304, + GTMSessionFetcherStatusBadRequest = 400, + GTMSessionFetcherStatusUnauthorized = 401, + GTMSessionFetcherStatusForbidden = 403, + GTMSessionFetcherStatusPreconditionFailed = 412 +}; + +#ifdef __cplusplus +extern "C" { +#endif + +@class GTMSessionCookieStorage; +@class GTMSessionFetcher; + +// The configuration block is for modifying the NSURLSessionConfiguration only. +// DO NOT change any fetcher properties in the configuration block. +typedef void (^GTMSessionFetcherConfigurationBlock)(GTMSessionFetcher *fetcher, + NSURLSessionConfiguration *configuration); +typedef void (^GTMSessionFetcherSystemCompletionHandler)(void); +typedef void (^GTMSessionFetcherCompletionHandler)(NSData * GTM_NULLABLE_TYPE data, + NSError * GTM_NULLABLE_TYPE error); +typedef void (^GTMSessionFetcherBodyStreamProviderResponse)(NSInputStream *bodyStream); +typedef void (^GTMSessionFetcherBodyStreamProvider)(GTMSessionFetcherBodyStreamProviderResponse response); +typedef void (^GTMSessionFetcherDidReceiveResponseDispositionBlock)(NSURLSessionResponseDisposition disposition); +typedef void (^GTMSessionFetcherDidReceiveResponseBlock)(NSURLResponse *response, + GTMSessionFetcherDidReceiveResponseDispositionBlock dispositionBlock); +typedef void (^GTMSessionFetcherChallengeDispositionBlock)(NSURLSessionAuthChallengeDisposition disposition, + NSURLCredential * GTM_NULLABLE_TYPE credential); +typedef void (^GTMSessionFetcherChallengeBlock)(GTMSessionFetcher *fetcher, + NSURLAuthenticationChallenge *challenge, + GTMSessionFetcherChallengeDispositionBlock dispositionBlock); +typedef void (^GTMSessionFetcherWillRedirectResponse)(NSURLRequest * GTM_NULLABLE_TYPE redirectedRequest); +typedef void (^GTMSessionFetcherWillRedirectBlock)(NSHTTPURLResponse *redirectResponse, + NSURLRequest *redirectRequest, + GTMSessionFetcherWillRedirectResponse response); +typedef void (^GTMSessionFetcherAccumulateDataBlock)(NSData * GTM_NULLABLE_TYPE buffer); +typedef void (^GTMSessionFetcherSimulateByteTransferBlock)(NSData * GTM_NULLABLE_TYPE buffer, + int64_t bytesWritten, + int64_t totalBytesWritten, + int64_t totalBytesExpectedToWrite); +typedef void (^GTMSessionFetcherReceivedProgressBlock)(int64_t bytesWritten, + int64_t totalBytesWritten); +typedef void (^GTMSessionFetcherDownloadProgressBlock)(int64_t bytesWritten, + int64_t totalBytesWritten, + int64_t totalBytesExpectedToWrite); +typedef void (^GTMSessionFetcherSendProgressBlock)(int64_t bytesSent, + int64_t totalBytesSent, + int64_t totalBytesExpectedToSend); +typedef void (^GTMSessionFetcherWillCacheURLResponseResponse)(NSCachedURLResponse * GTM_NULLABLE_TYPE cachedResponse); +typedef void (^GTMSessionFetcherWillCacheURLResponseBlock)(NSCachedURLResponse *proposedResponse, + GTMSessionFetcherWillCacheURLResponseResponse responseBlock); +typedef void (^GTMSessionFetcherRetryResponse)(BOOL shouldRetry); +typedef void (^GTMSessionFetcherRetryBlock)(BOOL suggestedWillRetry, + NSError * GTM_NULLABLE_TYPE error, + GTMSessionFetcherRetryResponse response); + +typedef void (^GTMSessionFetcherTestResponse)(NSHTTPURLResponse * GTM_NULLABLE_TYPE response, + NSData * GTM_NULLABLE_TYPE data, + NSError * GTM_NULLABLE_TYPE error); +typedef void (^GTMSessionFetcherTestBlock)(GTMSessionFetcher *fetcherToTest, + GTMSessionFetcherTestResponse testResponse); + +void GTMSessionFetcherAssertValidSelector(id GTM_NULLABLE_TYPE obj, SEL GTM_NULLABLE_TYPE sel, ...); + +// Utility functions for applications self-identifying to servers via a +// user-agent header + +// The "standard" user agent includes the application identifier, taken from the bundle, +// followed by a space and the system version string. Pass nil to use +mainBundle as the source +// of the bundle identifier. +// +// Applications may use this as a starting point for their own user agent strings, perhaps +// with additional sections appended. Use GTMFetcherCleanedUserAgentString() below to +// clean up any string being added to the user agent. +NSString *GTMFetcherStandardUserAgentString(NSBundle * GTM_NULLABLE_TYPE bundle); + +// Make a generic name and version for the current application, like +// com.example.MyApp/1.2.3 relying on the bundle identifier and the +// CFBundleShortVersionString or CFBundleVersion. +// +// The bundle ID may be overridden as the base identifier string by +// adding to the bundle's Info.plist a "GTMUserAgentID" key. +// +// If no bundle ID or override is available, the process name preceded +// by "proc_" is used. +NSString *GTMFetcherApplicationIdentifier(NSBundle * GTM_NULLABLE_TYPE bundle); + +// Make an identifier like "MacOSX/10.7.1" or "iPod_Touch/4.1 hw/iPod1_1" +NSString *GTMFetcherSystemVersionString(void); + +// Make a parseable user-agent identifier from the given string, replacing whitespace +// and commas with underscores, and removing other characters that may interfere +// with parsing of the full user-agent string. +// +// For example, @"[My App]" would become @"My_App" +NSString *GTMFetcherCleanedUserAgentString(NSString *str); + +// Grab the data from an input stream. Since streams cannot be assumed to be rewindable, +// this may be destructive; the caller can try to rewind the stream (by setting the +// NSStreamFileCurrentOffsetKey property) or can just use the NSData to make a new +// NSInputStream. This function is intended to facilitate testing rather than be used in +// production. +// +// This function operates synchronously on the current thread. Depending on how the +// input stream is implemented, it may be appropriate to dispatch to a different +// queue before calling this function. +// +// Failure is indicated by a returned data value of nil. +NSData * GTM_NULLABLE_TYPE GTMDataFromInputStream(NSInputStream *inputStream, NSError **outError); + +#ifdef __cplusplus +} // extern "C" +#endif + + +#if !GTM_USE_SESSION_FETCHER +@protocol GTMHTTPFetcherServiceProtocol; +#endif + +// This protocol allows abstract references to the fetcher service, primarily for +// fetchers (which may be compiled without the fetcher service class present.) +// +// Apps should not need to use this protocol. +@protocol GTMSessionFetcherServiceProtocol <NSObject> +// This protocol allows us to call into the service without requiring +// GTMSessionFetcherService sources in this project + +@property(atomic, strong) dispatch_queue_t callbackQueue; + +- (BOOL)fetcherShouldBeginFetching:(GTMSessionFetcher *)fetcher; +- (void)fetcherDidCreateSession:(GTMSessionFetcher *)fetcher; +- (void)fetcherDidBeginFetching:(GTMSessionFetcher *)fetcher; +- (void)fetcherDidStop:(GTMSessionFetcher *)fetcher; + +- (GTMSessionFetcher *)fetcherWithRequest:(NSURLRequest *)request; +- (BOOL)isDelayingFetcher:(GTMSessionFetcher *)fetcher; + +@property(atomic, assign) BOOL reuseSession; +- (GTM_NULLABLE NSURLSession *)session; +- (GTM_NULLABLE NSURLSession *)sessionForFetcherCreation; +- (GTM_NULLABLE id<NSURLSessionDelegate>)sessionDelegate; +- (GTM_NULLABLE NSDate *)stoppedAllFetchersDate; + +// Methods for compatibility with the old GTMHTTPFetcher. +@property(readonly, strong, GTM_NULLABLE) NSOperationQueue *delegateQueue; + +@end // @protocol GTMSessionFetcherServiceProtocol + +#ifndef GTM_FETCHER_AUTHORIZATION_PROTOCOL +#define GTM_FETCHER_AUTHORIZATION_PROTOCOL 1 +@protocol GTMFetcherAuthorizationProtocol <NSObject> +@required +// This protocol allows us to call the authorizer without requiring its sources +// in this project. +- (void)authorizeRequest:(GTM_NULLABLE NSMutableURLRequest *)request + delegate:(id)delegate + didFinishSelector:(SEL)sel; + +- (void)stopAuthorization; + +- (void)stopAuthorizationForRequest:(NSURLRequest *)request; + +- (BOOL)isAuthorizingRequest:(NSURLRequest *)request; + +- (BOOL)isAuthorizedRequest:(NSURLRequest *)request; + +@property(strong, readonly, GTM_NULLABLE) NSString *userEmail; + +@optional + +// Indicate if authorization may be attempted. Even if this succeeds, +// authorization may fail if the user's permissions have been revoked. +@property(readonly) BOOL canAuthorize; + +// For development only, allow authorization of non-SSL requests, allowing +// transmission of the bearer token unencrypted. +@property(assign) BOOL shouldAuthorizeAllRequests; + +- (void)authorizeRequest:(GTM_NULLABLE NSMutableURLRequest *)request + completionHandler:(void (^)(NSError * GTM_NULLABLE_TYPE error))handler; + +#if GTM_USE_SESSION_FETCHER +@property (weak, GTM_NULLABLE) id<GTMSessionFetcherServiceProtocol> fetcherService; +#else +@property (weak, GTM_NULLABLE) id<GTMHTTPFetcherServiceProtocol> fetcherService; +#endif + +- (BOOL)primeForRefresh; + +@end +#endif // GTM_FETCHER_AUTHORIZATION_PROTOCOL + +#if GTM_BACKGROUND_TASK_FETCHING +// A protocol for an alternative target for messages from GTMSessionFetcher to UIApplication. +// Set the target using +[GTMSessionFetcher setSubstituteUIApplication:] +@protocol GTMUIApplicationProtocol <NSObject> +- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithName:(nullable NSString *)taskName + expirationHandler:(void(^ __nullable)(void))handler; +- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)identifier; +@end +#endif + +#pragma mark - + +// GTMSessionFetcher objects are used for async retrieval of an http get or post +// +// See additional comments at the beginning of this file +@interface GTMSessionFetcher : NSObject <NSURLSessionDelegate> + +// Create a fetcher +// +// fetcherWithRequest will return an autoreleased fetcher, but if +// the connection is successfully created, the connection should retain the +// fetcher for the life of the connection as well. So the caller doesn't have +// to retain the fetcher explicitly unless they want to be able to cancel it. ++ (instancetype)fetcherWithRequest:(GTM_NULLABLE NSURLRequest *)request; + +// Convenience methods that make a request, like +fetcherWithRequest ++ (instancetype)fetcherWithURL:(NSURL *)requestURL; ++ (instancetype)fetcherWithURLString:(NSString *)requestURLString; + +// Methods for creating fetchers to continue previous fetches. ++ (instancetype)fetcherWithDownloadResumeData:(NSData *)resumeData; ++ (GTM_NULLABLE instancetype)fetcherWithSessionIdentifier:(NSString *)sessionIdentifier; + +// Returns an array of currently active fetchers for background sessions, +// both restarted and newly created ones. ++ (GTM_NSArrayOf(GTMSessionFetcher *) *)fetchersForBackgroundSessions; + +// Designated initializer. +// +// Applications should create fetchers with a "fetcherWith..." method on a fetcher +// service or a class method, not with this initializer. +// +// The configuration should typically be nil. Applications needing to customize +// the configuration may do so by setting the configurationBlock property. +- (instancetype)initWithRequest:(GTM_NULLABLE NSURLRequest *)request + configuration:(GTM_NULLABLE NSURLSessionConfiguration *)configuration; + +// The fetcher's request. This may not be set after beginFetch has been invoked. The request +// may change due to redirects. +@property(strong, GTM_NULLABLE) NSURLRequest *request; + +// Set a header field value on the request. Header field value changes will not +// affect a fetch after the fetch has begun. +- (void)setRequestValue:(GTM_NULLABLE NSString *)value forHTTPHeaderField:(NSString *)field; + +// Data used for resuming a download task. +@property(atomic, readonly, GTM_NULLABLE) NSData *downloadResumeData; + +// The configuration; this must be set before the fetch begins. If no configuration is +// set or inherited from the fetcher service, then the fetcher uses an ephemeral config. +// +// NOTE: This property should typically be nil. Applications needing to customize +// the configuration should do so by setting the configurationBlock property. +// That allows the fetcher to pick an appropriate base configuration, with the +// application setting only the configuration properties it needs to customize. +@property(atomic, strong, GTM_NULLABLE) NSURLSessionConfiguration *configuration; + +// A block the client may use to customize the configuration used to create the session. +// +// This is called synchronously, either on the thread that begins the fetch or, during a retry, +// on the main thread. The configuration block may be called repeatedly if multiple fetchers are +// created. +// +// The configuration block is for modifying the NSURLSessionConfiguration only. +// DO NOT change any fetcher properties in the configuration block. Fetcher properties +// may be set in the fetcher service prior to fetcher creation, or on the fetcher prior +// to invoking beginFetch. +@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherConfigurationBlock configurationBlock; + +// A session is created as needed by the fetcher. A fetcher service object +// may maintain sessions for multiple fetches to the same host. +@property(atomic, strong, GTM_NULLABLE) NSURLSession *session; + +// The task in flight. +@property(atomic, readonly, GTM_NULLABLE) NSURLSessionTask *sessionTask; + +// The background session identifier. +@property(atomic, readonly, GTM_NULLABLE) NSString *sessionIdentifier; + +// Indicates a fetcher created to finish a background session task. +@property(atomic, readonly) BOOL wasCreatedFromBackgroundSession; + +// Additional user-supplied data to encode into the session identifier. Since session identifier +// length limits are unspecified, this should be kept small. Key names beginning with an underscore +// are reserved for use by the fetcher. +@property(atomic, strong, GTM_NULLABLE) GTM_NSDictionaryOf(NSString *, NSString *) *sessionUserInfo; + +// The human-readable description to be assigned to the task. +@property(atomic, copy, GTM_NULLABLE) NSString *taskDescription; + +// The priority assigned to the task, if any. Use NSURLSessionTaskPriorityLow, +// NSURLSessionTaskPriorityDefault, or NSURLSessionTaskPriorityHigh. +@property(atomic, assign) float taskPriority; + +// The fetcher encodes information used to resume a session in the session identifier. +// This method, intended for internal use returns the encoded information. The sessionUserInfo +// dictionary is stored as identifier metadata. +- (GTM_NULLABLE GTM_NSDictionaryOf(NSString *, NSString *) *)sessionIdentifierMetadata; + +#if TARGET_OS_IPHONE && !TARGET_OS_WATCH +// The app should pass to this method the completion handler passed in the app delegate method +// application:handleEventsForBackgroundURLSession:completionHandler: ++ (void)application:(UIApplication *)application + handleEventsForBackgroundURLSession:(NSString *)identifier + completionHandler:(GTMSessionFetcherSystemCompletionHandler)completionHandler; +#endif + +// Indicate that a newly created session should be a background session. +// A new session identifier will be created by the fetcher. +// +// Warning: The only thing background sessions are for is rare download +// of huge, batched files of data. And even just for those, there's a lot +// of pain and hackery needed to get transfers to actually happen reliably +// with background sessions. +// +// Don't try to upload or download in many background sessions, since the system +// will impose an exponentially increasing time penalty to prevent the app from +// getting too much background execution time. +// +// References: +// +// "Moving to Fewer, Larger Transfers" +// https://forums.developer.apple.com/thread/14853 +// +// "NSURLSession’s Resume Rate Limiter" +// https://forums.developer.apple.com/thread/14854 +// +// "Background Session Task state persistence" +// https://forums.developer.apple.com/thread/11554 +// +@property(assign) BOOL useBackgroundSession; + +// Indicates if the fetcher was started using a background session. +@property(atomic, readonly, getter=isUsingBackgroundSession) BOOL usingBackgroundSession; + +// Indicates if uploads should use an upload task. This is always set for file or stream-provider +// bodies, but may be set explicitly for NSData bodies. +@property(atomic, assign) BOOL useUploadTask; + +// Indicates that the fetcher is using a session that may be shared with other fetchers. +@property(atomic, readonly) BOOL canShareSession; + +// By default, the fetcher allows only secure (https) schemes unless this +// property is set, or the GTM_ALLOW_INSECURE_REQUESTS build flag is set. +// +// For example, during debugging when fetching from a development server that lacks SSL support, +// this may be set to @[ @"http" ], or when the fetcher is used to retrieve local files, +// this may be set to @[ @"file" ]. +// +// This should be left as nil for release builds to avoid creating the opportunity for +// leaking private user behavior and data. If a server is providing insecure URLs +// for fetching by the client app, report the problem as server security & privacy bug. +// +// For builds with the iOS 9/OS X 10.11 and later SDKs, this property is required only when +// the app specifies NSAppTransportSecurity/NSAllowsArbitraryLoads in the main bundle's Info.plist. +@property(atomic, copy, GTM_NULLABLE) GTM_NSArrayOf(NSString *) *allowedInsecureSchemes; + +// By default, the fetcher prohibits localhost requests unless this property is set, +// or the GTM_ALLOW_INSECURE_REQUESTS build flag is set. +// +// For localhost requests, the URL scheme is not checked when this property is set. +// +// For builds with the iOS 9/OS X 10.11 and later SDKs, this property is required only when +// the app specifies NSAppTransportSecurity/NSAllowsArbitraryLoads in the main bundle's Info.plist. +@property(atomic, assign) BOOL allowLocalhostRequest; + +// By default, the fetcher requires valid server certs. This may be bypassed +// temporarily for development against a test server with an invalid cert. +@property(atomic, assign) BOOL allowInvalidServerCertificates; + +// Cookie storage object for this fetcher. If nil, the fetcher will use a static cookie +// storage instance shared among fetchers. If this fetcher was created by a fetcher service +// object, it will be set to use the service object's cookie storage. See Cookies section above for +// the full discussion. +// +// Because as of Jan 2014 standalone instances of NSHTTPCookieStorage do not actually +// store any cookies (Radar 15735276) we use our own subclass, GTMSessionCookieStorage, +// to hold cookies in memory. +@property(atomic, strong, GTM_NULLABLE) NSHTTPCookieStorage *cookieStorage; + +// Setting the credential is optional; it is used if the connection receives +// an authentication challenge. +@property(atomic, strong, GTM_NULLABLE) NSURLCredential *credential; + +// Setting the proxy credential is optional; it is used if the connection +// receives an authentication challenge from a proxy. +@property(atomic, strong, GTM_NULLABLE) NSURLCredential *proxyCredential; + +// If body data, body file URL, or body stream provider is not set, then a GET request +// method is assumed. +@property(atomic, strong, GTM_NULLABLE) NSData *bodyData; + +// File to use as the request body. This forces use of an upload task. +@property(atomic, strong, GTM_NULLABLE) NSURL *bodyFileURL; + +// Length of body to send, expected or actual. +@property(atomic, readonly) int64_t bodyLength; + +// The body stream provider may be called repeatedly to provide a body. +// Setting a body stream provider forces use of an upload task. +@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherBodyStreamProvider bodyStreamProvider; + +// Object to add authorization to the request, if needed. +// +// This may not be changed once beginFetch has been invoked. +@property(atomic, strong, GTM_NULLABLE) id<GTMFetcherAuthorizationProtocol> authorizer; + +// The service object that created and monitors this fetcher, if any. +@property(atomic, strong) id<GTMSessionFetcherServiceProtocol> service; + +// The host, if any, used to classify this fetcher in the fetcher service. +@property(atomic, copy, GTM_NULLABLE) NSString *serviceHost; + +// The priority, if any, used for starting fetchers in the fetcher service. +// +// Lower values are higher priority; the default is 0, and values may +// be negative or positive. This priority affects only the start order of +// fetchers that are being delayed by a fetcher service when the running fetchers +// exceeds the service's maxRunningFetchersPerHost. A priority of NSIntegerMin will +// exempt this fetcher from delay. +@property(atomic, assign) NSInteger servicePriority; + +// The delegate's optional didReceiveResponse block may be used to inspect or alter +// the session task response. +// +// This is called on the callback queue. +@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherDidReceiveResponseBlock didReceiveResponseBlock; + +// The delegate's optional challenge block may be used to inspect or alter +// the session task challenge. +// +// If this block is not set, the fetcher's default behavior for the NSURLSessionTask +// didReceiveChallenge: delegate method is to use the fetcher's respondToChallenge: method +// which relies on the fetcher's credential and proxyCredential properties. +// +// Warning: This may be called repeatedly if the challenge fails. Check +// challenge.previousFailureCount to identify repeated invocations. +// +// This is called on the callback queue. +@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherChallengeBlock challengeBlock; + +// The delegate's optional willRedirect block may be used to inspect or alter +// the redirection. +// +// This is called on the callback queue. +@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherWillRedirectBlock willRedirectBlock; + +// The optional send progress block reports body bytes uploaded. +// +// This is called on the callback queue. +@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherSendProgressBlock sendProgressBlock; + +// The optional accumulate block may be set by clients wishing to accumulate data +// themselves rather than let the fetcher append each buffer to an NSData. +// +// When this is called with nil data (such as on redirect) the client +// should empty its accumulation buffer. +// +// This is called on the callback queue. +@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherAccumulateDataBlock accumulateDataBlock; + +// The optional received progress block may be used to monitor data +// received from a data task. +// +// This is called on the callback queue. +@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherReceivedProgressBlock receivedProgressBlock; + +// The delegate's optional downloadProgress block may be used to monitor download +// progress in writing to disk. +// +// This is called on the callback queue. +@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherDownloadProgressBlock downloadProgressBlock; + +// The delegate's optional willCacheURLResponse block may be used to alter the cached +// NSURLResponse. The user may prevent caching by passing nil to the block's response. +// +// This is called on the callback queue. +@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherWillCacheURLResponseBlock willCacheURLResponseBlock; + +// Enable retrying; see comments at the top of this file. Setting +// retryEnabled=YES resets the min and max retry intervals. +@property(atomic, assign, getter=isRetryEnabled) BOOL retryEnabled; + +// Retry block is optional for retries. +// +// If present, this block should call the response block with YES to cause a retry or NO to end the +// fetch. +// See comments at the top of this file. +@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherRetryBlock retryBlock; + +// Retry intervals must be strictly less than maxRetryInterval, else +// they will be limited to maxRetryInterval and no further retries will +// be attempted. Setting maxRetryInterval to 0.0 will reset it to the +// default value, 60 seconds for downloads and 600 seconds for uploads. +@property(atomic, assign) NSTimeInterval maxRetryInterval; + +// Starting retry interval. Setting minRetryInterval to 0.0 will reset it +// to a random value between 1.0 and 2.0 seconds. Clients should normally not +// set this except for unit testing. +@property(atomic, assign) NSTimeInterval minRetryInterval; + +// Multiplier used to increase the interval between retries, typically 2.0. +// Clients should not need to set this. +@property(atomic, assign) double retryFactor; + +// Number of retries attempted. +@property(atomic, readonly) NSUInteger retryCount; + +// Interval delay to precede next retry. +@property(atomic, readonly) NSTimeInterval nextRetryInterval; + +#if GTM_BACKGROUND_TASK_FETCHING +// Skip use of a UIBackgroundTask, thus requiring fetches to complete when the app is in the +// foreground. +// +// Targets should define GTM_BACKGROUND_TASK_FETCHING to 0 to avoid use of a UIBackgroundTask +// on iOS to allow fetches to complete in the background. This property is available when +// it's not practical to set the preprocessor define. +@property(atomic, assign) BOOL skipBackgroundTask; +#endif // GTM_BACKGROUND_TASK_FETCHING + +// Begin fetching the request +// +// The delegate may optionally implement the callback or pass nil for the selector or handler. +// +// The delegate and all callback blocks are retained between the beginFetch call until after the +// finish callback, or until the fetch is stopped. +// +// An error is passed to the callback for server statuses 300 or +// higher, with the status stored as the error object's code. +// +// finishedSEL has a signature like: +// - (void)fetcher:(GTMSessionFetcher *)fetcher +// finishedWithData:(NSData *)data +// error:(NSError *)error; +// +// If the application has specified a destinationFileURL or an accumulateDataBlock +// for the fetcher, the data parameter passed to the callback will be nil. + +- (void)beginFetchWithDelegate:(GTM_NULLABLE id)delegate + didFinishSelector:(GTM_NULLABLE SEL)finishedSEL; + +- (void)beginFetchWithCompletionHandler:(GTM_NULLABLE GTMSessionFetcherCompletionHandler)handler; + +// Returns YES if this fetcher is in the process of fetching a URL. +@property(atomic, readonly, getter=isFetching) BOOL fetching; + +// Cancel the fetch of the request that's currently in progress. The completion handler +// will not be called. +- (void)stopFetching; + +// A block to be called when the fetch completes. +@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherCompletionHandler completionHandler; + +// A block to be called if download resume data becomes available. +@property(atomic, strong, GTM_NULLABLE) void (^resumeDataBlock)(NSData *); + +// Return the status code from the server response. +@property(atomic, readonly) NSInteger statusCode; + +// Return the http headers from the response. +@property(atomic, strong, readonly, GTM_NULLABLE) GTM_NSDictionaryOf(NSString *, NSString *) *responseHeaders; + +// The response, once it's been received. +@property(atomic, strong, readonly, GTM_NULLABLE) NSURLResponse *response; + +// Bytes downloaded so far. +@property(atomic, readonly) int64_t downloadedLength; + +// Buffer of currently-downloaded data, if available. +@property(atomic, readonly, strong, GTM_NULLABLE) NSData *downloadedData; + +// Local path to which the downloaded file will be moved. +// +// If a file already exists at the path, it will be overwritten. +// Will create the enclosing folders if they are not present. +@property(atomic, strong, GTM_NULLABLE) NSURL *destinationFileURL; + +// The time this fetcher originally began fetching. This is useful as a time +// barrier for ignoring irrelevant fetch notifications or callbacks. +@property(atomic, strong, readonly, GTM_NULLABLE) NSDate *initialBeginFetchDate; + +// userData is retained solely for the convenience of the client. +@property(atomic, strong, GTM_NULLABLE) id userData; + +// Stored property values are retained solely for the convenience of the client. +@property(atomic, copy, GTM_NULLABLE) GTM_NSDictionaryOf(NSString *, id) *properties; + +- (void)setProperty:(GTM_NULLABLE id)obj forKey:(NSString *)key; // Pass nil for obj to remove the property. +- (GTM_NULLABLE id)propertyForKey:(NSString *)key; + +- (void)addPropertiesFromDictionary:(GTM_NSDictionaryOf(NSString *, id) *)dict; + +// Comments are useful for logging, so are strongly recommended for each fetcher. +@property(atomic, copy, GTM_NULLABLE) NSString *comment; + +- (void)setCommentWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1, 2); + +// Log of request and response, if logging is enabled +@property(atomic, copy, GTM_NULLABLE) NSString *log; + +// Callbacks are run on this queue. If none is supplied, the main queue is used. +@property(atomic, strong, GTM_NULL_RESETTABLE) dispatch_queue_t callbackQueue; + +// The queue used internally by the session to invoke its delegate methods in the fetcher. +// +// Application callbacks are always called by the fetcher on the callbackQueue above, +// not on this queue. Apps should generally not change this queue. +// +// The default delegate queue is the main queue. +// +// This value is ignored after the session has been created, so this +// property should be set in the fetcher service rather in the fetcher as it applies +// to a shared session. +@property(atomic, strong, GTM_NULL_RESETTABLE) NSOperationQueue *sessionDelegateQueue; + +// Spin the run loop or sleep the thread, discarding events, until the fetch has completed. +// +// This is only for use in testing or in tools without a user interface. +// +// Note: Synchronous fetches should never be used by shipping apps; they are +// sufficient reason for rejection from the app store. +// +// Returns NO if timed out. +- (BOOL)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds; + +// Test block is optional for testing. +// +// If present, this block will cause the fetcher to skip starting the session, and instead +// use the test block response values when calling the completion handler and delegate code. +// +// Test code can set this on the fetcher or on the fetcher service. For testing libraries +// that use a fetcher without exposing either the fetcher or the fetcher service, the global +// method setGlobalTestBlock: will set the block for all fetchers that do not have a test +// block set. +// +// The test code can pass nil for all response parameters to indicate that the fetch +// should proceed. +// +// Applications can exclude test block support by setting GTM_DISABLE_FETCHER_TEST_BLOCK. +@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherTestBlock testBlock; + ++ (void)setGlobalTestBlock:(GTM_NULLABLE GTMSessionFetcherTestBlock)block; + +// When using the testBlock, |testBlockAccumulateDataChunkCount| is the desired number of chunks to +// divide the response data into if the client has streaming enabled. The data will be divided up to +// |testBlockAccumulateDataChunkCount| chunks; however, the exact amount may vary depending on the +// size of the response data (e.g. a 1-byte response can only be divided into one chunk). +@property(atomic, readwrite) NSUInteger testBlockAccumulateDataChunkCount; + +#if GTM_BACKGROUND_TASK_FETCHING +// For testing or to override UIApplication invocations, apps may specify an alternative +// target for messages to UIApplication. ++ (void)setSubstituteUIApplication:(nullable id<GTMUIApplicationProtocol>)substituteUIApplication; ++ (nullable id<GTMUIApplicationProtocol>)substituteUIApplication; +#endif // GTM_BACKGROUND_TASK_FETCHING + +// Exposed for testing. ++ (GTMSessionCookieStorage *)staticCookieStorage; ++ (BOOL)appAllowsInsecureRequests; + +#if STRIP_GTM_FETCH_LOGGING +// If logging is stripped, provide a stub for the main method +// for controlling logging. ++ (void)setLoggingEnabled:(BOOL)flag; ++ (BOOL)isLoggingEnabled; + +#else + +// These methods let an application log specific body text, such as the text description of a binary +// request or response. The application should set the fetcher to defer response body logging until +// the response has been received and the log response body has been set by the app. For example: +// +// fetcher.logRequestBody = [binaryObject stringDescription]; +// fetcher.deferResponseBodyLogging = YES; +// [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { +// if (error == nil) { +// fetcher.logResponseBody = [[[MyThing alloc] initWithData:data] stringDescription]; +// } +// fetcher.deferResponseBodyLogging = NO; +// }]; + +@property(atomic, copy, GTM_NULLABLE) NSString *logRequestBody; +@property(atomic, assign) BOOL deferResponseBodyLogging; +@property(atomic, copy, GTM_NULLABLE) NSString *logResponseBody; + +// Internal logging support. +@property(atomic, readonly) NSData *loggedStreamData; +@property(atomic, assign) BOOL hasLoggedError; +@property(atomic, strong, GTM_NULLABLE) NSURL *redirectedFromURL; +- (void)appendLoggedStreamData:(NSData *)dataToAdd; +- (void)clearLoggedStreamData; + +#endif // STRIP_GTM_FETCH_LOGGING + +@end + +@interface GTMSessionFetcher (BackwardsCompatibilityOnly) +// Clients using GTMSessionFetcher should set the cookie storage explicitly themselves. +// This method is just for compatibility with the old GTMHTTPFetcher class. +- (void)setCookieStorageMethod:(NSInteger)method; +@end + +// Until we can just instantiate NSHTTPCookieStorage for local use, we'll +// implement all the public methods ourselves. This stores cookies only in +// memory. Additional methods are provided for testing. +// +// iOS 9/OS X 10.11 added +[NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:] +// which may also be used to create cookie storage. +@interface GTMSessionCookieStorage : NSHTTPCookieStorage + +// Add the array off cookies to the storage, replacing duplicates. +// Also removes expired cookies from the storage. +- (void)setCookies:(GTM_NULLABLE GTM_NSArrayOf(NSHTTPCookie *) *)cookies; + +- (void)removeAllCookies; + +@end + +// Macros to monitor synchronization blocks in debug builds. +// These report problems using GTMSessionCheckDebug. +// +// GTMSessionMonitorSynchronized Start monitoring a top-level-only +// @sync scope. +// GTMSessionMonitorRecursiveSynchronized Start monitoring a top-level or +// recursive @sync scope. +// GTMSessionCheckSynchronized Verify that the current execution +// is inside a @sync scope. +// GTMSessionCheckNotSynchronized Verify that the current execution +// is not inside a @sync scope. +// +// Example usage: +// +// - (void)myExternalMethod { +// @synchronized(self) { +// GTMSessionMonitorSynchronized(self) +// +// - (void)myInternalMethod { +// GTMSessionCheckSynchronized(self); +// +// - (void)callMyCallbacks { +// GTMSessionCheckNotSynchronized(self); +// +// GTMSessionCheckNotSynchronized is available for verifying the code isn't +// in a deadlockable @sync state when posting notifications and invoking +// callbacks. Don't use GTMSessionCheckNotSynchronized immediately before a +// @sync scope; the normal recursiveness check of GTMSessionMonitorSynchronized +// can catch those. + +#ifdef __OBJC__ +// If asserts are entirely no-ops, the synchronization monitor is just a bunch +// of counting code that doesn't report exceptional circumstances in any way. +// Only build the synchronization monitor code if NS_BLOCK_ASSERTIONS is not +// defined or asserts are being logged instead. +#if DEBUG && (!defined(NS_BLOCK_ASSERTIONS) || GTMSESSION_ASSERT_AS_LOG) + #define __GTMSessionMonitorSynchronizedVariableInner(varname, counter) \ + varname ## counter + #define __GTMSessionMonitorSynchronizedVariable(varname, counter) \ + __GTMSessionMonitorSynchronizedVariableInner(varname, counter) + + #define GTMSessionMonitorSynchronized(obj) \ + NS_VALID_UNTIL_END_OF_SCOPE id \ + __GTMSessionMonitorSynchronizedVariable(__monitor, __COUNTER__) = \ + [[GTMSessionSyncMonitorInternal alloc] initWithSynchronizationObject:obj \ + allowRecursive:NO \ + functionName:__func__] + + #define GTMSessionMonitorRecursiveSynchronized(obj) \ + NS_VALID_UNTIL_END_OF_SCOPE id \ + __GTMSessionMonitorSynchronizedVariable(__monitor, __COUNTER__) = \ + [[GTMSessionSyncMonitorInternal alloc] initWithSynchronizationObject:obj \ + allowRecursive:YES \ + functionName:__func__] + + #define GTMSessionCheckSynchronized(obj) { \ + GTMSESSION_ASSERT_DEBUG( \ + [GTMSessionSyncMonitorInternal functionsHoldingSynchronizationOnObject:obj], \ + @"GTMSessionCheckSynchronized(" #obj ") failed: not sync'd" \ + @" on " #obj " in %s. Call stack:\n%@", \ + __func__, [NSThread callStackSymbols]); \ + } + + #define GTMSessionCheckNotSynchronized(obj) { \ + GTMSESSION_ASSERT_DEBUG( \ + ![GTMSessionSyncMonitorInternal functionsHoldingSynchronizationOnObject:obj], \ + @"GTMSessionCheckNotSynchronized(" #obj ") failed: was sync'd" \ + @" on " #obj " in %s by %@. Call stack:\n%@", __func__, \ + [GTMSessionSyncMonitorInternal functionsHoldingSynchronizationOnObject:obj], \ + [NSThread callStackSymbols]); \ + } + +// GTMSessionSyncMonitorInternal is a private class that keeps track of the +// beginning and end of synchronized scopes. +// +// This class should not be used directly, but only via the +// GTMSessionMonitorSynchronized macro. +@interface GTMSessionSyncMonitorInternal : NSObject +- (instancetype)initWithSynchronizationObject:(id)object + allowRecursive:(BOOL)allowRecursive + functionName:(const char *)functionName; +// Return the names of the functions that hold sync on the object, or nil if none. ++ (NSArray *)functionsHoldingSynchronizationOnObject:(id)object; +@end + +#else + #define GTMSessionMonitorSynchronized(obj) do { } while (0) + #define GTMSessionMonitorRecursiveSynchronized(obj) do { } while (0) + #define GTMSessionCheckSynchronized(obj) do { } while (0) + #define GTMSessionCheckNotSynchronized(obj) do { } while (0) +#endif // !DEBUG +#endif // __OBJC__ + + +GTM_ASSUME_NONNULL_END diff --git a/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.m b/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.m @@ -0,0 +1,4579 @@ +/* Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "GTMSessionFetcher.h" + +#import <sys/utsname.h> + +#ifndef STRIP_GTM_FETCH_LOGGING + #error GTMSessionFetcher headers should have defaulted this if it wasn't already defined. +#endif + +GTM_ASSUME_NONNULL_BEGIN + +NSString *const kGTMSessionFetcherStartedNotification = @"kGTMSessionFetcherStartedNotification"; +NSString *const kGTMSessionFetcherStoppedNotification = @"kGTMSessionFetcherStoppedNotification"; +NSString *const kGTMSessionFetcherRetryDelayStartedNotification = @"kGTMSessionFetcherRetryDelayStartedNotification"; +NSString *const kGTMSessionFetcherRetryDelayStoppedNotification = @"kGTMSessionFetcherRetryDelayStoppedNotification"; + +NSString *const kGTMSessionFetcherCompletionInvokedNotification = @"kGTMSessionFetcherCompletionInvokedNotification"; +NSString *const kGTMSessionFetcherCompletionDataKey = @"data"; +NSString *const kGTMSessionFetcherCompletionErrorKey = @"error"; + +NSString *const kGTMSessionFetcherErrorDomain = @"com.google.GTMSessionFetcher"; +NSString *const kGTMSessionFetcherStatusDomain = @"com.google.HTTPStatus"; +NSString *const kGTMSessionFetcherStatusDataKey = @"data"; // data returned with a kGTMSessionFetcherStatusDomain error +NSString *const kGTMSessionFetcherStatusDataContentTypeKey = @"data_content_type"; + +NSString *const kGTMSessionFetcherNumberOfRetriesDoneKey = @"kGTMSessionFetcherNumberOfRetriesDoneKey"; +NSString *const kGTMSessionFetcherElapsedIntervalWithRetriesKey = @"kGTMSessionFetcherElapsedIntervalWithRetriesKey"; + +static NSString *const kGTMSessionIdentifierPrefix = @"com.google.GTMSessionFetcher"; +static NSString *const kGTMSessionIdentifierDestinationFileURLMetadataKey = @"_destURL"; +static NSString *const kGTMSessionIdentifierBodyFileURLMetadataKey = @"_bodyURL"; + +// The default max retry interview is 10 minutes for uploads (POST/PUT/PATCH), +// 1 minute for downloads. +static const NSTimeInterval kUnsetMaxRetryInterval = -1.0; +static const NSTimeInterval kDefaultMaxDownloadRetryInterval = 60.0; +static const NSTimeInterval kDefaultMaxUploadRetryInterval = 60.0 * 10.; + +// The maximum data length that can be loaded to the error userInfo +static const int64_t kMaximumDownloadErrorDataLength = 20000; + +#ifdef GTMSESSION_PERSISTED_DESTINATION_KEY +// Projects using unique class names should also define a unique persisted destination key. +static NSString * const kGTMSessionFetcherPersistedDestinationKey = + GTMSESSION_PERSISTED_DESTINATION_KEY; +#else +static NSString * const kGTMSessionFetcherPersistedDestinationKey = + @"com.google.GTMSessionFetcher.downloads"; +#endif + +GTM_ASSUME_NONNULL_END + +// +// GTMSessionFetcher +// + +#if 0 +#define GTM_LOG_BACKGROUND_SESSION(...) GTMSESSION_LOG_DEBUG(__VA_ARGS__) +#else +#define GTM_LOG_BACKGROUND_SESSION(...) +#endif + +#ifndef GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY + #if (TARGET_OS_TV \ + || TARGET_OS_WATCH \ + || (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_11) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_11) \ + || (TARGET_OS_IPHONE && defined(__IPHONE_9_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0)) + #define GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY 1 + #endif +#endif + +@interface GTMSessionFetcher () + +@property(atomic, strong, readwrite, GTM_NULLABLE) NSData *downloadedData; +@property(atomic, strong, readwrite, GTM_NULLABLE) NSData *downloadResumeData; + +#if GTM_BACKGROUND_TASK_FETCHING +// Should always be accessed within an @synchronized(self). +@property(assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskIdentifier; +#endif + +@property(atomic, readwrite, getter=isUsingBackgroundSession) BOOL usingBackgroundSession; + +@end + +#if !GTMSESSION_BUILD_COMBINED_SOURCES +@interface GTMSessionFetcher (GTMSessionFetcherLoggingInternal) +- (void)logFetchWithError:(NSError *)error; +- (void)logNowWithError:(GTM_NULLABLE NSError *)error; +- (NSInputStream *)loggedInputStreamForInputStream:(NSInputStream *)inputStream; +- (GTMSessionFetcherBodyStreamProvider)loggedStreamProviderForStreamProvider: + (GTMSessionFetcherBodyStreamProvider)streamProvider; +@end +#endif // !GTMSESSION_BUILD_COMBINED_SOURCES + +GTM_ASSUME_NONNULL_BEGIN + +static NSTimeInterval InitialMinRetryInterval(void) { + return 1.0 + ((double)(arc4random_uniform(0x0FFFF)) / (double) 0x0FFFF); +} + +static BOOL IsLocalhost(NSString * GTM_NULLABLE_TYPE host) { + // We check if there's host, and then make the comparisons. + if (host == nil) return NO; + return ([host caseInsensitiveCompare:@"localhost"] == NSOrderedSame + || [host isEqual:@"::1"] + || [host isEqual:@"127.0.0.1"]); +} + +static NSDictionary *GTM_NULLABLE_TYPE GTMErrorUserInfoForData( + NSData *GTM_NULLABLE_TYPE data, NSDictionary *GTM_NULLABLE_TYPE responseHeaders) { + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + + if (data.length > 0) { + userInfo[kGTMSessionFetcherStatusDataKey] = data; + + NSString *contentType = responseHeaders[@"Content-Type"]; + if (contentType) { + userInfo[kGTMSessionFetcherStatusDataContentTypeKey] = contentType; + } + } + + return userInfo.count > 0 ? userInfo : nil; +} + +static GTMSessionFetcherTestBlock GTM_NULLABLE_TYPE gGlobalTestBlock; + +@implementation GTMSessionFetcher { + NSMutableURLRequest *_request; // after beginFetch, changed only in delegate callbacks + BOOL _useUploadTask; // immutable after beginFetch + NSURL *_bodyFileURL; // immutable after beginFetch + GTMSessionFetcherBodyStreamProvider _bodyStreamProvider; // immutable after beginFetch + NSURLSession *_session; + BOOL _shouldInvalidateSession; // immutable after beginFetch + NSURLSession *_sessionNeedingInvalidation; + NSURLSessionConfiguration *_configuration; + NSURLSessionTask *_sessionTask; + NSString *_taskDescription; + float _taskPriority; + NSURLResponse *_response; + NSString *_sessionIdentifier; + BOOL _wasCreatedFromBackgroundSession; + BOOL _didCreateSessionIdentifier; + NSString *_sessionIdentifierUUID; + BOOL _userRequestedBackgroundSession; + BOOL _usingBackgroundSession; + NSMutableData * GTM_NULLABLE_TYPE _downloadedData; + NSError *_downloadFinishedError; + NSData *_downloadResumeData; // immutable after construction + NSData * GTM_NULLABLE_TYPE _downloadTaskErrorData; // Data for when download task fails + NSURL *_destinationFileURL; + int64_t _downloadedLength; + NSURLCredential *_credential; // username & password + NSURLCredential *_proxyCredential; // credential supplied to proxy servers + BOOL _isStopNotificationNeeded; // set when start notification has been sent + BOOL _isUsingTestBlock; // set when a test block was provided (remains set when the block is released) + id _userData; // retained, if set by caller + NSMutableDictionary *_properties; // more data retained for caller + dispatch_queue_t _callbackQueue; + dispatch_group_t _callbackGroup; // read-only after creation + NSOperationQueue *_delegateQueue; // immutable after beginFetch + + id<GTMFetcherAuthorizationProtocol> _authorizer; // immutable after beginFetch + + // The service object that created and monitors this fetcher, if any. + id<GTMSessionFetcherServiceProtocol> _service; // immutable; set by the fetcher service upon creation + NSString *_serviceHost; + NSInteger _servicePriority; // immutable after beginFetch + BOOL _hasStoppedFetching; // counterpart to _initialBeginFetchDate + BOOL _userStoppedFetching; + + BOOL _isRetryEnabled; // user wants auto-retry + NSTimer *_retryTimer; + NSUInteger _retryCount; + NSTimeInterval _maxRetryInterval; // default 60 (download) or 600 (upload) seconds + NSTimeInterval _minRetryInterval; // random between 1 and 2 seconds + NSTimeInterval _retryFactor; // default interval multiplier is 2 + NSTimeInterval _lastRetryInterval; + NSDate *_initialBeginFetchDate; // date that beginFetch was first invoked; immutable after initial beginFetch + NSDate *_initialRequestDate; // date of first request to the target server (ignoring auth) + BOOL _hasAttemptedAuthRefresh; // accessed only in shouldRetryNowForStatus: + + NSString *_comment; // comment for log + NSString *_log; +#if !STRIP_GTM_FETCH_LOGGING + NSMutableData *_loggedStreamData; + NSURL *_redirectedFromURL; + NSString *_logRequestBody; + NSString *_logResponseBody; + BOOL _hasLoggedError; + BOOL _deferResponseBodyLogging; +#endif +} + +#if !GTMSESSION_UNIT_TESTING ++ (void)load { + [self fetchersForBackgroundSessions]; +} +#endif + ++ (instancetype)fetcherWithRequest:(GTM_NULLABLE NSURLRequest *)request { + return [[self alloc] initWithRequest:request configuration:nil]; +} + ++ (instancetype)fetcherWithURL:(NSURL *)requestURL { + return [self fetcherWithRequest:[NSURLRequest requestWithURL:requestURL]]; +} + ++ (instancetype)fetcherWithURLString:(NSString *)requestURLString { + return [self fetcherWithURL:(NSURL *)[NSURL URLWithString:requestURLString]]; +} + ++ (instancetype)fetcherWithDownloadResumeData:(NSData *)resumeData { + GTMSessionFetcher *fetcher = [self fetcherWithRequest:nil]; + fetcher.comment = @"Resuming download"; + fetcher.downloadResumeData = resumeData; + return fetcher; +} + ++ (GTM_NULLABLE instancetype)fetcherWithSessionIdentifier:(NSString *)sessionIdentifier { + GTMSESSION_ASSERT_DEBUG(sessionIdentifier != nil, @"Invalid session identifier"); + NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap]; + GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier]; + if (!fetcher && [sessionIdentifier hasPrefix:kGTMSessionIdentifierPrefix]) { + fetcher = [self fetcherWithRequest:nil]; + [fetcher setSessionIdentifier:sessionIdentifier]; + [sessionIdentifierToFetcherMap setObject:fetcher forKey:sessionIdentifier]; + fetcher->_wasCreatedFromBackgroundSession = YES; + [fetcher setCommentWithFormat:@"Resuming %@", + fetcher && fetcher->_sessionIdentifierUUID ? fetcher->_sessionIdentifierUUID : @"?"]; + } + return fetcher; +} + ++ (NSMapTable *)sessionIdentifierToFetcherMap { + // TODO: What if a service is involved in creating the fetcher? Currently, when re-creating + // fetchers, if a service was involved, it is not re-created. Should the service maintain a map? + static NSMapTable *gSessionIdentifierToFetcherMap = nil; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + gSessionIdentifierToFetcherMap = [NSMapTable strongToWeakObjectsMapTable]; + }); + return gSessionIdentifierToFetcherMap; +} + +#if !GTM_ALLOW_INSECURE_REQUESTS ++ (BOOL)appAllowsInsecureRequests { + // If the main bundle Info.plist key NSAppTransportSecurity is present, and it specifies + // NSAllowsArbitraryLoads, then we need to explicitly enforce secure schemes. +#if GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY + static BOOL allowsInsecureRequests; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSBundle *mainBundle = [NSBundle mainBundle]; + NSDictionary *appTransportSecurity = + [mainBundle objectForInfoDictionaryKey:@"NSAppTransportSecurity"]; + allowsInsecureRequests = + [[appTransportSecurity objectForKey:@"NSAllowsArbitraryLoads"] boolValue]; + }); + return allowsInsecureRequests; +#else + // For builds targeting iOS 8 or 10.10 and earlier, we want to require fetcher + // security checks. + return YES; +#endif // GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY +} +#else // GTM_ALLOW_INSECURE_REQUESTS ++ (BOOL)appAllowsInsecureRequests { + return YES; +} +#endif // !GTM_ALLOW_INSECURE_REQUESTS + + +- (instancetype)init { + return [self initWithRequest:nil configuration:nil]; +} + +- (instancetype)initWithRequest:(NSURLRequest *)request { + return [self initWithRequest:request configuration:nil]; +} + +- (instancetype)initWithRequest:(GTM_NULLABLE NSURLRequest *)request + configuration:(GTM_NULLABLE NSURLSessionConfiguration *)configuration { + self = [super init]; + if (self) { +#if GTM_BACKGROUND_TASK_FETCHING + _backgroundTaskIdentifier = UIBackgroundTaskInvalid; +#endif + _request = [request mutableCopy]; + _configuration = configuration; + + NSData *bodyData = request.HTTPBody; + if (bodyData) { + _bodyLength = (int64_t)bodyData.length; + } else { + _bodyLength = NSURLSessionTransferSizeUnknown; + } + + _callbackQueue = dispatch_get_main_queue(); + _callbackGroup = dispatch_group_create(); + _delegateQueue = [NSOperationQueue mainQueue]; + + _minRetryInterval = InitialMinRetryInterval(); + _maxRetryInterval = kUnsetMaxRetryInterval; + + _taskPriority = -1.0f; // Valid values if set are 0.0...1.0. + + _testBlockAccumulateDataChunkCount = 1; + +#if !STRIP_GTM_FETCH_LOGGING + // Encourage developers to set the comment property or use + // setCommentWithFormat: by providing a default string. + _comment = @"(No fetcher comment set)"; +#endif + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + // disallow use of fetchers in a copy property + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (NSString *)description { + NSString *requestStr = self.request.URL.description; + if (requestStr.length == 0) { + if (self.downloadResumeData.length > 0) { + requestStr = @"<download resume data>"; + } else if (_wasCreatedFromBackgroundSession) { + requestStr = @"<from bg session>"; + } else { + requestStr = @"<no request>"; + } + } + return [NSString stringWithFormat:@"%@ %p (%@)", [self class], self, requestStr]; +} + +- (void)dealloc { + GTMSESSION_ASSERT_DEBUG(!_isStopNotificationNeeded, + @"unbalanced fetcher notification for %@", _request.URL); + [self forgetSessionIdentifierForFetcherWithoutSyncCheck]; + + // Note: if a session task or a retry timer was pending, then this instance + // would be retained by those so it wouldn't be getting dealloc'd, + // hence we don't need to stopFetch here +} + +#pragma mark - + +// Begin fetching the URL (or begin a retry fetch). The delegate is retained +// for the duration of the fetch connection. + +- (void)beginFetchWithCompletionHandler:(GTM_NULLABLE GTMSessionFetcherCompletionHandler)handler { + GTMSessionCheckNotSynchronized(self); + + _completionHandler = [handler copy]; + + // The user may have called setDelegate: earlier if they want to use other + // delegate-style callbacks during the fetch; otherwise, the delegate is nil, + // which is fine. + [self beginFetchMayDelay:YES mayAuthorize:YES]; +} + +// Begin fetching the URL for a retry fetch. The delegate and completion handler +// are already provided, and do not need to be copied. +- (void)beginFetchForRetry { + GTMSessionCheckNotSynchronized(self); + + [self beginFetchMayDelay:YES mayAuthorize:YES]; +} + +- (GTMSessionFetcherCompletionHandler)completionHandlerWithTarget:(GTM_NULLABLE_TYPE id)target + didFinishSelector:(GTM_NULLABLE_TYPE SEL)finishedSelector { + GTMSessionFetcherAssertValidSelector(target, finishedSelector, @encode(GTMSessionFetcher *), + @encode(NSData *), @encode(NSError *), 0); + GTMSessionFetcherCompletionHandler completionHandler = ^(NSData *data, NSError *error) { + if (target && finishedSelector) { + id selfArg = self; // Placate ARC. + NSMethodSignature *sig = [target methodSignatureForSelector:finishedSelector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:(SEL)finishedSelector]; + [invocation setTarget:target]; + [invocation setArgument:&selfArg atIndex:2]; + [invocation setArgument:&data atIndex:3]; + [invocation setArgument:&error atIndex:4]; + [invocation invoke]; + } + }; + return completionHandler; +} + +- (void)beginFetchWithDelegate:(GTM_NULLABLE_TYPE id)target + didFinishSelector:(GTM_NULLABLE_TYPE SEL)finishedSelector { + GTMSessionCheckNotSynchronized(self); + + GTMSessionFetcherCompletionHandler handler = [self completionHandlerWithTarget:target + didFinishSelector:finishedSelector]; + [self beginFetchWithCompletionHandler:handler]; +} + +- (void)beginFetchMayDelay:(BOOL)mayDelay + mayAuthorize:(BOOL)mayAuthorize { + // This is the internal entry point for re-starting fetches. + GTMSessionCheckNotSynchronized(self); + + NSMutableURLRequest *fetchRequest = _request; // The request property is now externally immutable. + NSURL *fetchRequestURL = fetchRequest.URL; + NSString *priorSessionIdentifier = self.sessionIdentifier; + + // A utility block for creating error objects when we fail to start the fetch. + NSError *(^beginFailureError)(NSInteger) = ^(NSInteger code){ + NSString *urlString = fetchRequestURL.absoluteString; + NSDictionary *userInfo = @{ + NSURLErrorFailingURLStringErrorKey : (urlString ? urlString : @"(missing URL)") + }; + return [NSError errorWithDomain:kGTMSessionFetcherErrorDomain + code:code + userInfo:userInfo]; + }; + + // Catch delegate queue maxConcurrentOperationCount values other than 1, particularly + // NSOperationQueueDefaultMaxConcurrentOperationCount (-1), to avoid the additional complexity + // of simultaneous or out-of-order delegate callbacks. + GTMSESSION_ASSERT_DEBUG(_delegateQueue.maxConcurrentOperationCount == 1, + @"delegate queue %@ should support one concurrent operation, not %ld", + _delegateQueue.name, + (long)_delegateQueue.maxConcurrentOperationCount); + + if (!_initialBeginFetchDate) { + // This ivar is set only here on the initial beginFetch so need not be synchronized. + _initialBeginFetchDate = [[NSDate alloc] init]; + } + + if (self.sessionTask != nil) { + // If cached fetcher returned through fetcherWithSessionIdentifier:, then it's + // already begun, but don't consider this a failure, since the user need not know this. + if (self.sessionIdentifier != nil) { + return; + } + GTMSESSION_ASSERT_DEBUG(NO, @"Fetch object %@ being reused; this should never happen", self); + [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorDownloadFailed)]; + return; + } + + if (fetchRequestURL == nil && !_downloadResumeData && !priorSessionIdentifier) { + GTMSESSION_ASSERT_DEBUG(NO, @"Beginning a fetch requires a request with a URL"); + [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorDownloadFailed)]; + return; + } + + // We'll respect the user's request for a background session (unless this is + // an upload fetcher, which does its initial request foreground.) + self.usingBackgroundSession = self.useBackgroundSession && [self canFetchWithBackgroundSession]; + + NSURL *bodyFileURL = self.bodyFileURL; + if (bodyFileURL) { + NSError *fileCheckError; + if (![bodyFileURL checkResourceIsReachableAndReturnError:&fileCheckError]) { + // This assert fires when the file being uploaded no longer exists once + // the fetcher is ready to start the upload. + GTMSESSION_ASSERT_DEBUG_OR_LOG(0, @"Body file is unreachable: %@\n %@", + bodyFileURL.path, fileCheckError); + [self failToBeginFetchWithError:fileCheckError]; + return; + } + } + + NSString *requestScheme = fetchRequestURL.scheme; + BOOL isDataRequest = [requestScheme isEqual:@"data"]; + if (isDataRequest) { + // NSURLSession does not support data URLs in background sessions. +#if DEBUG + if (priorSessionIdentifier || self.sessionIdentifier) { + GTMSESSION_LOG_DEBUG(@"Converting background to foreground session for %@", + fetchRequest); + } +#endif + [self setSessionIdentifierInternal:nil]; + self.useBackgroundSession = NO; + } + +#if GTM_ALLOW_INSECURE_REQUESTS + BOOL shouldCheckSecurity = NO; +#else + BOOL shouldCheckSecurity = (fetchRequestURL != nil + && !isDataRequest + && [[self class] appAllowsInsecureRequests]); +#endif + + if (shouldCheckSecurity) { + // Allow https only for requests, unless overridden by the client. + // + // Non-https requests may too easily be snooped, so we disallow them by default. + // + // file: and data: schemes are usually safe if they are hardcoded in the client or provided + // by a trusted source, but since it's fairly rare to need them, it's safest to make clients + // explicitly whitelist them. + BOOL isSecure = + requestScheme != nil && [requestScheme caseInsensitiveCompare:@"https"] == NSOrderedSame; + if (!isSecure) { + BOOL allowRequest = NO; + NSString *host = fetchRequestURL.host; + + // Check schemes first. A file scheme request may be allowed here, or as a localhost request. + for (NSString *allowedScheme in _allowedInsecureSchemes) { + if (requestScheme != nil && + [requestScheme caseInsensitiveCompare:allowedScheme] == NSOrderedSame) { + allowRequest = YES; + break; + } + } + if (!allowRequest) { + // Check for localhost requests. Security checks only occur for non-https requests, so + // this check won't happen for an https request to localhost. + BOOL isLocalhostRequest = (host.length == 0 && [fetchRequestURL isFileURL]) || IsLocalhost(host); + if (isLocalhostRequest) { + if (self.allowLocalhostRequest) { + allowRequest = YES; + } else { + GTMSESSION_ASSERT_DEBUG(NO, @"Fetch request for localhost but fetcher" + @" allowLocalhostRequest is not set: %@", fetchRequestURL); + } + } else { + GTMSESSION_ASSERT_DEBUG(NO, @"Insecure fetch request has a scheme (%@)" + @" not found in fetcher allowedInsecureSchemes (%@): %@", + requestScheme, _allowedInsecureSchemes ?: @" @[] ", fetchRequestURL); + } + } + + if (!allowRequest) { +#if !DEBUG + NSLog(@"Insecure fetch disallowed for %@", fetchRequestURL.description ?: @"nil request URL"); +#endif + [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorInsecureRequest)]; + return; + } + } // !isSecure + } // (requestURL != nil) && !isDataRequest + + if (self.cookieStorage == nil) { + self.cookieStorage = [[self class] staticCookieStorage]; + } + + BOOL isRecreatingSession = (self.sessionIdentifier != nil) && (fetchRequest == nil); + + self.canShareSession = !isRecreatingSession && !self.usingBackgroundSession; + + if (!self.session && self.canShareSession) { + self.session = [_service sessionForFetcherCreation]; + // If _session is nil, then the service's session creation semaphore will block + // until this fetcher invokes fetcherDidCreateSession: below, so this *must* invoke + // that method, even if the session fails to be created. + } + + if (!self.session) { + // Create a session. + if (!_configuration) { + if (priorSessionIdentifier || self.usingBackgroundSession) { + NSString *sessionIdentifier = priorSessionIdentifier; + if (!sessionIdentifier) { + sessionIdentifier = [self createSessionIdentifierWithMetadata:nil]; + } + NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIdentifierToFetcherMap]; + [sessionIdentifierToFetcherMap setObject:self forKey:self.sessionIdentifier]; + +#if (TARGET_OS_TV \ + || TARGET_OS_WATCH \ + || (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_10) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_10) \ + || (TARGET_OS_IPHONE && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0)) + // iOS 8/10.10 builds require the new backgroundSessionConfiguration method name. + _configuration = + [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionIdentifier]; +#elif (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_10) && MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10) \ + || (TARGET_OS_IPHONE && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0) + // Do a runtime check to avoid a deprecation warning about using + // +backgroundSessionConfiguration: on iOS 8. + if ([NSURLSessionConfiguration respondsToSelector:@selector(backgroundSessionConfigurationWithIdentifier:)]) { + // Running on iOS 8+/OS X 10.10+. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability" +// Disable unguarded availability warning as we can't use the @availability macro until we require +// all clients to build with Xcode 9 or above. + _configuration = + [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionIdentifier]; +#pragma clang diagnostic pop + } else { + // Running on iOS 7/OS X 10.9. + _configuration = + [NSURLSessionConfiguration backgroundSessionConfiguration:sessionIdentifier]; + } +#else + // Building with an SDK earlier than iOS 8/OS X 10.10. + _configuration = + [NSURLSessionConfiguration backgroundSessionConfiguration:sessionIdentifier]; +#endif + self.usingBackgroundSession = YES; + self.canShareSession = NO; + } else { + _configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration]; + } +#if !GTM_ALLOW_INSECURE_REQUESTS + _configuration.TLSMinimumSupportedProtocol = kTLSProtocol12; +#endif + } // !_configuration + _configuration.HTTPCookieStorage = self.cookieStorage; + + if (_configurationBlock) { + _configurationBlock(self, _configuration); + } + + id<NSURLSessionDelegate> delegate = [_service sessionDelegate]; + if (!delegate || !self.canShareSession) { + delegate = self; + } + self.session = [NSURLSession sessionWithConfiguration:_configuration + delegate:delegate + delegateQueue:self.sessionDelegateQueue]; + GTMSESSION_ASSERT_DEBUG(self.session, @"Couldn't create session"); + + // Tell the service about the session created by this fetcher. This also signals the + // service's semaphore to allow other fetchers to request this session. + [_service fetcherDidCreateSession:self]; + + // If this assertion fires, the client probably tried to use a session identifier that was + // already used. The solution is to make the client use a unique identifier (or better yet let + // the session fetcher assign the identifier). + GTMSESSION_ASSERT_DEBUG(self.session.delegate == delegate, @"Couldn't assign delegate."); + + if (self.session) { + BOOL isUsingSharedDelegate = (delegate != self); + if (!isUsingSharedDelegate) { + _shouldInvalidateSession = YES; + } + } + } + + if (isRecreatingSession) { + _shouldInvalidateSession = YES; + + // Let's make sure there are tasks still running or if not that we get a callback from a + // completed one; otherwise, we assume the tasks failed. + // This is the observed behavior perhaps 25% of the time within the Simulator running 7.0.3 on + // exiting the app after starting an upload and relaunching the app if we manage to relaunch + // after the task has completed, but before the system relaunches us in the background. + [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, + NSArray *downloadTasks) { + if (dataTasks.count == 0 && uploadTasks.count == 0 && downloadTasks.count == 0) { + double const kDelayInSeconds = 1.0; // We should get progress indication or completion soon + dispatch_time_t checkForFeedbackDelay = + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kDelayInSeconds * NSEC_PER_SEC)); + dispatch_after(checkForFeedbackDelay, dispatch_get_main_queue(), ^{ + if (!self.sessionTask && !fetchRequest) { + // If our task and/or request haven't been restored, then we assume task feedback lost. + [self removePersistedBackgroundSessionFromDefaults]; + NSError *sessionError = + [NSError errorWithDomain:kGTMSessionFetcherErrorDomain + code:GTMSessionFetcherErrorBackgroundFetchFailed + userInfo:nil]; + [self failToBeginFetchWithError:sessionError]; + } + }); + } + }]; + return; + } + + self.downloadedData = nil; + self.downloadedLength = 0; + + if (_servicePriority == NSIntegerMin) { + mayDelay = NO; + } + if (mayDelay && _service) { + BOOL shouldFetchNow = [_service fetcherShouldBeginFetching:self]; + if (!shouldFetchNow) { + // The fetch is deferred, but will happen later. + // + // If this session is held by the fetcher service, clear the session now so that we don't + // assume it's still valid after the fetcher is restarted. + if (self.canShareSession) { + self.session = nil; + } + return; + } + } + + NSString *effectiveHTTPMethod = [fetchRequest valueForHTTPHeaderField:@"X-HTTP-Method-Override"]; + if (effectiveHTTPMethod == nil) { + effectiveHTTPMethod = fetchRequest.HTTPMethod; + } + BOOL isEffectiveHTTPGet = (effectiveHTTPMethod == nil + || [effectiveHTTPMethod isEqual:@"GET"]); + + BOOL needsUploadTask = (self.useUploadTask || self.bodyFileURL || self.bodyStreamProvider); + if (_bodyData || self.bodyStreamProvider || fetchRequest.HTTPBodyStream) { + if (isEffectiveHTTPGet) { + fetchRequest.HTTPMethod = @"POST"; + isEffectiveHTTPGet = NO; + } + + if (_bodyData) { + if (!needsUploadTask) { + fetchRequest.HTTPBody = _bodyData; + } +#if !STRIP_GTM_FETCH_LOGGING + } else if (fetchRequest.HTTPBodyStream) { + if ([self respondsToSelector:@selector(loggedInputStreamForInputStream:)]) { + fetchRequest.HTTPBodyStream = + [self performSelector:@selector(loggedInputStreamForInputStream:) + withObject:fetchRequest.HTTPBodyStream]; + } +#endif + } + } + + // We authorize after setting up the http method and body in the request + // because OAuth 1 may need to sign the request body + if (mayAuthorize && _authorizer && !isDataRequest) { + BOOL isAuthorized = [_authorizer isAuthorizedRequest:fetchRequest]; + if (!isAuthorized) { + // Authorization needed. + // + // If this session is held by the fetcher service, clear the session now so that we don't + // assume it's still valid after authorization completes. + if (self.canShareSession) { + self.session = nil; + } + + // Authorizing the request will recursively call this beginFetch:mayDelay: + // or failToBeginFetchWithError:. + [self authorizeRequest]; + return; + } + } + + // set the default upload or download retry interval, if necessary + if ([self isRetryEnabled] && self.maxRetryInterval <= 0) { + if (isEffectiveHTTPGet || [effectiveHTTPMethod isEqual:@"HEAD"]) { + [self setMaxRetryInterval:kDefaultMaxDownloadRetryInterval]; + } else { + [self setMaxRetryInterval:kDefaultMaxUploadRetryInterval]; + } + } + + // finally, start the connection + NSURLSessionTask *newSessionTask; + BOOL needsDataAccumulator = NO; + if (_downloadResumeData) { + newSessionTask = [_session downloadTaskWithResumeData:_downloadResumeData]; + GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, + @"Failed downloadTaskWithResumeData for %@, resume data %lu bytes", + _session, (unsigned long)_downloadResumeData.length); + } else if (_destinationFileURL && !isDataRequest) { + newSessionTask = [_session downloadTaskWithRequest:fetchRequest]; + GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, @"Failed downloadTaskWithRequest for %@, %@", + _session, fetchRequest); + } else if (needsUploadTask) { + if (bodyFileURL) { + newSessionTask = [_session uploadTaskWithRequest:fetchRequest + fromFile:bodyFileURL]; + GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, + @"Failed uploadTaskWithRequest for %@, %@, file %@", + _session, fetchRequest, bodyFileURL.path); + } else if (self.bodyStreamProvider) { + newSessionTask = [_session uploadTaskWithStreamedRequest:fetchRequest]; + GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, + @"Failed uploadTaskWithStreamedRequest for %@, %@", + _session, fetchRequest); + } else { + GTMSESSION_ASSERT_DEBUG_OR_LOG(_bodyData != nil, + @"Upload task needs body data, %@", fetchRequest); + newSessionTask = [_session uploadTaskWithRequest:fetchRequest + fromData:(NSData * GTM_NONNULL_TYPE)_bodyData]; + GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, + @"Failed uploadTaskWithRequest for %@, %@, body data %lu bytes", + _session, fetchRequest, (unsigned long)_bodyData.length); + } + needsDataAccumulator = YES; + } else { + newSessionTask = [_session dataTaskWithRequest:fetchRequest]; + needsDataAccumulator = YES; + GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, @"Failed dataTaskWithRequest for %@, %@", + _session, fetchRequest); + } + self.sessionTask = newSessionTask; + + if (!newSessionTask) { + // We shouldn't get here; if we're here, an earlier assertion should have fired to explain + // which session task creation failed. + [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorTaskCreationFailed)]; + return; + } + + if (needsDataAccumulator && _accumulateDataBlock == nil) { + self.downloadedData = [NSMutableData data]; + } + if (_taskDescription) { + newSessionTask.taskDescription = _taskDescription; + } + if (_taskPriority >= 0) { +#if TARGET_OS_TV || TARGET_OS_WATCH + BOOL hasTaskPriority = YES; +#elif (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_10) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_10) \ + || (TARGET_OS_IPHONE && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0) + BOOL hasTaskPriority = YES; +#else + BOOL hasTaskPriority = [newSessionTask respondsToSelector:@selector(setPriority:)]; +#endif + if (hasTaskPriority) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability" +// Disable unguarded availability warning as we can't use the @availability macro until we require +// all clients to build with Xcode 9 or above. + newSessionTask.priority = _taskPriority; +#pragma clang diagnostic pop + } + } + +#if GTM_DISABLE_FETCHER_TEST_BLOCK + GTMSESSION_ASSERT_DEBUG(_testBlock == nil && gGlobalTestBlock == nil, @"test blocks disabled"); + _testBlock = nil; +#else + if (!_testBlock) { + if (gGlobalTestBlock) { + // Note that the test block may pass nil for all of its response parameters, + // indicating that the fetch should actually proceed. This is useful when the + // global test block has been set, and the app is only testing a specific + // fetcher. The block simulation code will then resume the task. + _testBlock = gGlobalTestBlock; + } + } + _isUsingTestBlock = (_testBlock != nil); +#endif // GTM_DISABLE_FETCHER_TEST_BLOCK + +#if GTM_BACKGROUND_TASK_FETCHING + id<GTMUIApplicationProtocol> app = [[self class] fetcherUIApplication]; + // Background tasks seem to interfere with out-of-process uploads and downloads. + if (app && !self.skipBackgroundTask && !self.useBackgroundSession) { + // Tell UIApplication that we want to continue even when the app is in the + // background. +#if DEBUG + NSString *bgTaskName = [NSString stringWithFormat:@"%@-%@", + [self class], fetchRequest.URL.host]; +#else + NSString *bgTaskName = @"GTMSessionFetcher"; +#endif + __block UIBackgroundTaskIdentifier bgTaskID = [app beginBackgroundTaskWithName:bgTaskName + expirationHandler:^{ + // Background task expiration callback - this block is always invoked by + // UIApplication on the main thread. + if (bgTaskID != UIBackgroundTaskInvalid) { + @synchronized(self) { + if (bgTaskID == self.backgroundTaskIdentifier) { + self.backgroundTaskIdentifier = UIBackgroundTaskInvalid; + } + } + [app endBackgroundTask:bgTaskID]; + } + }]; + @synchronized(self) { + self.backgroundTaskIdentifier = bgTaskID; + } + } +#endif + + if (!_initialRequestDate) { + _initialRequestDate = [[NSDate alloc] init]; + } + + // We don't expect to reach here even on retry or auth until a stop notification has been sent + // for the previous task, but we should ensure that we don't unbalance that. + GTMSESSION_ASSERT_DEBUG(!_isStopNotificationNeeded, @"Start notification without a prior stop"); + [self sendStopNotificationIfNeeded]; + + [self addPersistedBackgroundSessionToDefaults]; + + [self setStopNotificationNeeded:YES]; + + [self postNotificationOnMainThreadWithName:kGTMSessionFetcherStartedNotification + userInfo:nil + requireAsync:NO]; + + // The service needs to know our task if it is serving as NSURLSession delegate. + [_service fetcherDidBeginFetching:self]; + + if (_testBlock) { +#if !GTM_DISABLE_FETCHER_TEST_BLOCK + [self simulateFetchForTestBlock]; +#endif + } else { + // We resume the session task after posting the notification since the + // delegate callbacks may happen immediately if the fetch is started off + // the main thread or the session delegate queue is on a background thread, + // and we don't want to post a start notification after a premature finish + // of the session task. + [newSessionTask resume]; + } +} + +NSData * GTM_NULLABLE_TYPE GTMDataFromInputStream(NSInputStream *inputStream, NSError **outError) { + NSMutableData *data = [NSMutableData data]; + + [inputStream open]; + NSInteger numberOfBytesRead = 0; + while ([inputStream hasBytesAvailable]) { + uint8_t buffer[512]; + numberOfBytesRead = [inputStream read:buffer maxLength:sizeof(buffer)]; + if (numberOfBytesRead > 0) { + [data appendBytes:buffer length:(NSUInteger)numberOfBytesRead]; + } else { + break; + } + } + [inputStream close]; + NSError *streamError = inputStream.streamError; + + if (streamError) { + data = nil; + } + if (outError) { + *outError = streamError; + } + return data; +} + +#if !GTM_DISABLE_FETCHER_TEST_BLOCK + +- (void)simulateFetchForTestBlock { + // This is invoked on the same thread as the beginFetch method was. + // + // Callbacks will all occur on the callback queue. + _testBlock(self, ^(NSURLResponse *response, NSData *responseData, NSError *error) { + // Callback from test block. + if (response == nil && responseData == nil && error == nil) { + // Assume the fetcher should execute rather than be tested. + self->_testBlock = nil; + self->_isUsingTestBlock = NO; + [self->_sessionTask resume]; + return; + } + + GTMSessionFetcherBodyStreamProvider bodyStreamProvider = self.bodyStreamProvider; + if (bodyStreamProvider) { + bodyStreamProvider(^(NSInputStream *bodyStream){ + // Read from the input stream into an NSData buffer. We'll drain the stream + // explicitly on a background queue. + [self invokeOnCallbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0) + afterUserStopped:NO + block:^{ + NSError *streamError; + NSData *streamedData = GTMDataFromInputStream(bodyStream, &streamError); + + dispatch_async(dispatch_get_main_queue(), ^{ + // Continue callbacks on the main thread, since serial behavior + // is more reliable for tests. + [self simulateDataCallbacksForTestBlockWithBodyData:streamedData + response:response + responseData:responseData + error:(error ?: streamError)]; + }); + }]; + }); + } else { + // No input stream; use the supplied data or file URL. + NSURL *bodyFileURL = self.bodyFileURL; + if (bodyFileURL) { + NSError *readError; + self->_bodyData = [NSData dataWithContentsOfURL:bodyFileURL + options:NSDataReadingMappedIfSafe + error:&readError]; + error = readError; + } + + // No stream provider. + + // In real fetches, nothing happens until the run loop spins, so apps have leeway to + // set callbacks after they call beginFetch. We'll mirror that fetcher behavior by + // delaying callbacks here at least to the next spin of the run loop. That keeps + // immediate, synchronous setting of callback blocks after beginFetch working in tests. + dispatch_async(dispatch_get_main_queue(), ^{ + [self simulateDataCallbacksForTestBlockWithBodyData:self->_bodyData + response:response + responseData:responseData + error:error]; + }); + } + }); +} + +- (void)simulateByteTransferReportWithDataLength:(int64_t)totalDataLength + block:(GTMSessionFetcherSendProgressBlock)block { + // This utility method simulates transfer progress with up to three callbacks. + // It is used to call back to any of the progress blocks. + int64_t sendReportSize = totalDataLength / 3 + 1; + int64_t totalSent = 0; + while (totalSent < totalDataLength) { + int64_t bytesRemaining = totalDataLength - totalSent; + sendReportSize = MIN(sendReportSize, bytesRemaining); + totalSent += sendReportSize; + [self invokeOnCallbackQueueUnlessStopped:^{ + block(sendReportSize, totalSent, totalDataLength); + }]; + } +} + +- (void)simulateDataCallbacksForTestBlockWithBodyData:(NSData * GTM_NULLABLE_TYPE)bodyData + response:(NSURLResponse *)response + responseData:(NSData *)suppliedData + error:(NSError *)suppliedError { + __block NSData *responseData = suppliedData; + __block NSError *responseError = suppliedError; + + // This method does the test simulation of callbacks once the upload + // and download data are known. + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + // Get copies of ivars we'll access in async invocations. This simulation assumes + // they won't change during fetcher execution. + NSURL *destinationFileURL = _destinationFileURL; + GTMSessionFetcherWillRedirectBlock willRedirectBlock = _willRedirectBlock; + GTMSessionFetcherDidReceiveResponseBlock didReceiveResponseBlock = _didReceiveResponseBlock; + GTMSessionFetcherSendProgressBlock sendProgressBlock = _sendProgressBlock; + GTMSessionFetcherDownloadProgressBlock downloadProgressBlock = _downloadProgressBlock; + GTMSessionFetcherAccumulateDataBlock accumulateDataBlock = _accumulateDataBlock; + GTMSessionFetcherReceivedProgressBlock receivedProgressBlock = _receivedProgressBlock; + GTMSessionFetcherWillCacheURLResponseBlock willCacheURLResponseBlock = + _willCacheURLResponseBlock; + + // Simulate receipt of redirection. + if (willRedirectBlock) { + [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES + block:^{ + willRedirectBlock((NSHTTPURLResponse *)response, self->_request, + ^(NSURLRequest *redirectRequest) { + // For simulation, we'll assume the app will just continue. + }); + }]; + } + + // If the fetcher has a challenge block, simulate a challenge. + // + // It might be nice to eventually let the user determine which testBlock + // fetches get challenged rather than always executing the supplied + // challenge block. + if (_challengeBlock) { + [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES + block:^{ + if (self->_challengeBlock) { + NSURL *requestURL = self->_request.URL; + NSString *host = requestURL.host; + NSURLProtectionSpace *pspace = + [[NSURLProtectionSpace alloc] initWithHost:host + port:requestURL.port.integerValue + protocol:requestURL.scheme + realm:nil + authenticationMethod:NSURLAuthenticationMethodHTTPBasic]; + id<NSURLAuthenticationChallengeSender> unusedSender = + (id<NSURLAuthenticationChallengeSender>)[NSNull null]; + NSURLAuthenticationChallenge *challenge = + [[NSURLAuthenticationChallenge alloc] initWithProtectionSpace:pspace + proposedCredential:nil + previousFailureCount:0 + failureResponse:nil + error:nil + sender:unusedSender]; + self->_challengeBlock(self, challenge, ^(NSURLSessionAuthChallengeDisposition disposition, + NSURLCredential * GTM_NULLABLE_TYPE credential){ + // We could change the responseData and responseError based on the disposition, + // but it's easier for apps to just supply the expected data and error + // directly to the test block. So this simulation ignores the disposition. + }); + } + }]; + } + + // Simulate receipt of an initial response. + if (response && didReceiveResponseBlock) { + [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES + block:^{ + didReceiveResponseBlock(response, ^(NSURLSessionResponseDisposition desiredDisposition) { + // For simulation, we'll assume the disposition is to continue. + }); + }]; + } + + // Simulate reporting send progress. + if (sendProgressBlock) { + [self simulateByteTransferReportWithDataLength:(int64_t)bodyData.length + block:^(int64_t bytesSent, + int64_t totalBytesSent, + int64_t totalBytesExpectedToSend) { + // This is invoked on the callback queue unless stopped. + sendProgressBlock(bytesSent, totalBytesSent, totalBytesExpectedToSend); + }]; + } + + if (destinationFileURL) { + // Simulate download to file progress. + if (downloadProgressBlock) { + [self simulateByteTransferReportWithDataLength:(int64_t)responseData.length + block:^(int64_t bytesDownloaded, + int64_t totalBytesDownloaded, + int64_t totalBytesExpectedToDownload) { + // This is invoked on the callback queue unless stopped. + downloadProgressBlock(bytesDownloaded, totalBytesDownloaded, + totalBytesExpectedToDownload); + }]; + } + + NSError *writeError; + [responseData writeToURL:destinationFileURL + options:NSDataWritingAtomic + error:&writeError]; + if (writeError) { + // Tell the test code that writing failed. + responseError = writeError; + } + } else { + // Simulate download to NSData progress. + if ((accumulateDataBlock || receivedProgressBlock) && responseData) { + [self simulateByteTransferWithData:responseData + block:^(NSData *data, + int64_t bytesReceived, + int64_t totalBytesReceived, + int64_t totalBytesExpectedToReceive) { + // This is invoked on the callback queue unless stopped. + if (accumulateDataBlock) { + accumulateDataBlock(data); + } + + if (receivedProgressBlock) { + receivedProgressBlock(bytesReceived, totalBytesReceived); + } + }]; + } + + if (!accumulateDataBlock) { + _downloadedData = [responseData mutableCopy]; + } + + if (willCacheURLResponseBlock) { + // Simulate letting the client inspect and alter the cached response. + NSData *cachedData = responseData ?: [[NSData alloc] init]; // Always have non-nil data. + NSCachedURLResponse *cachedResponse = + [[NSCachedURLResponse alloc] initWithResponse:response + data:cachedData]; + [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES + block:^{ + willCacheURLResponseBlock(cachedResponse, ^(NSCachedURLResponse *responseToCache){ + // The app may provide an alternative response, or nil to defeat caching. + }); + }]; + } + } + _response = response; + } // @synchronized(self) + + NSOperationQueue *queue = self.sessionDelegateQueue; + [queue addOperationWithBlock:^{ + // Rather than invoke failToBeginFetchWithError: we want to simulate completion of + // a connection that started and ended, so we'll call down to finishWithError: + NSInteger status = responseError ? responseError.code : 200; + if (status >= 200 && status <= 399) { + [self finishWithError:nil shouldRetry:NO]; + } else { + [self shouldRetryNowForStatus:status + error:responseError + forceAssumeRetry:NO + response:^(BOOL shouldRetry) { + [self finishWithError:responseError shouldRetry:shouldRetry]; + }]; + } + }]; +} + +- (void)simulateByteTransferWithData:(NSData *)responseData + block:(GTMSessionFetcherSimulateByteTransferBlock)transferBlock { + // This utility method simulates transfering data to the client. It divides the data into at most + // "chunkCount" chunks and then passes each chunk along with a progress update to transferBlock. + // This function can be used with accumulateDataBlock or receivedProgressBlock. + + NSUInteger chunkCount = MAX(self.testBlockAccumulateDataChunkCount, (NSUInteger) 1); + NSUInteger totalDataLength = responseData.length; + NSUInteger sendDataSize = totalDataLength / chunkCount + 1; + NSUInteger totalSent = 0; + while (totalSent < totalDataLength) { + NSUInteger bytesRemaining = totalDataLength - totalSent; + sendDataSize = MIN(sendDataSize, bytesRemaining); + NSData *chunkData = [responseData subdataWithRange:NSMakeRange(totalSent, sendDataSize)]; + totalSent += sendDataSize; + [self invokeOnCallbackQueueUnlessStopped:^{ + transferBlock(chunkData, + (int64_t)sendDataSize, + (int64_t)totalSent, + (int64_t)totalDataLength); + }]; + } +} + +#endif // !GTM_DISABLE_FETCHER_TEST_BLOCK + +- (void)setSessionTask:(NSURLSessionTask *)sessionTask { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (_sessionTask != sessionTask) { + _sessionTask = sessionTask; + if (_sessionTask) { + // Request could be nil on restoring this fetcher from a background session. + if (!_request) { + _request = [_sessionTask.originalRequest mutableCopy]; + } + } + } + } // @synchronized(self) +} + +- (NSURLSessionTask * GTM_NULLABLE_TYPE)sessionTask { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _sessionTask; + } // @synchronized(self) +} + ++ (NSUserDefaults *)fetcherUserDefaults { + static NSUserDefaults *gFetcherUserDefaults = nil; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + Class fetcherUserDefaultsClass = NSClassFromString(@"GTMSessionFetcherUserDefaultsFactory"); + if (fetcherUserDefaultsClass) { + gFetcherUserDefaults = [fetcherUserDefaultsClass fetcherUserDefaults]; + } else { + gFetcherUserDefaults = [NSUserDefaults standardUserDefaults]; + } + }); + return gFetcherUserDefaults; +} + +- (void)addPersistedBackgroundSessionToDefaults { + NSString *sessionIdentifier = self.sessionIdentifier; + if (!sessionIdentifier) { + return; + } + NSArray *oldBackgroundSessions = [[self class] activePersistedBackgroundSessions]; + if ([oldBackgroundSessions containsObject:_sessionIdentifier]) { + return; + } + NSMutableArray *newBackgroundSessions = + [NSMutableArray arrayWithArray:oldBackgroundSessions]; + [newBackgroundSessions addObject:sessionIdentifier]; + GTM_LOG_BACKGROUND_SESSION(@"Add to background sessions: %@", newBackgroundSessions); + + NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults]; + [userDefaults setObject:newBackgroundSessions + forKey:kGTMSessionFetcherPersistedDestinationKey]; + [userDefaults synchronize]; +} + +- (void)removePersistedBackgroundSessionFromDefaults { + NSString *sessionIdentifier = self.sessionIdentifier; + if (!sessionIdentifier) return; + + NSArray *oldBackgroundSessions = [[self class] activePersistedBackgroundSessions]; + if (!oldBackgroundSessions) { + return; + } + NSMutableArray *newBackgroundSessions = + [NSMutableArray arrayWithArray:oldBackgroundSessions]; + NSUInteger sessionIndex = [newBackgroundSessions indexOfObject:sessionIdentifier]; + if (sessionIndex == NSNotFound) { + return; + } + [newBackgroundSessions removeObjectAtIndex:sessionIndex]; + GTM_LOG_BACKGROUND_SESSION(@"Remove from background sessions: %@", newBackgroundSessions); + + NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults]; + if (newBackgroundSessions.count == 0) { + [userDefaults removeObjectForKey:kGTMSessionFetcherPersistedDestinationKey]; + } else { + [userDefaults setObject:newBackgroundSessions + forKey:kGTMSessionFetcherPersistedDestinationKey]; + } + [userDefaults synchronize]; +} + ++ (GTM_NULLABLE NSArray *)activePersistedBackgroundSessions { + NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults]; + NSArray *oldBackgroundSessions = + [userDefaults arrayForKey:kGTMSessionFetcherPersistedDestinationKey]; + if (oldBackgroundSessions.count == 0) { + return nil; + } + NSMutableArray *activeBackgroundSessions = nil; + NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap]; + for (NSString *sessionIdentifier in oldBackgroundSessions) { + GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier]; + if (fetcher) { + if (!activeBackgroundSessions) { + activeBackgroundSessions = [[NSMutableArray alloc] init]; + } + [activeBackgroundSessions addObject:sessionIdentifier]; + } + } + return activeBackgroundSessions; +} + ++ (NSArray *)fetchersForBackgroundSessions { + NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults]; + NSArray *backgroundSessions = + [userDefaults arrayForKey:kGTMSessionFetcherPersistedDestinationKey]; + NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap]; + NSMutableArray *fetchers = [NSMutableArray array]; + for (NSString *sessionIdentifier in backgroundSessions) { + GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier]; + if (!fetcher) { + fetcher = [self fetcherWithSessionIdentifier:sessionIdentifier]; + GTMSESSION_ASSERT_DEBUG(fetcher != nil, + @"Unexpected invalid session identifier: %@", sessionIdentifier); + [fetcher beginFetchWithCompletionHandler:nil]; + } + GTM_LOG_BACKGROUND_SESSION(@"%@ restoring session %@ by creating fetcher %@ %p", + [self class], sessionIdentifier, fetcher, fetcher); + if (fetcher != nil) { + [fetchers addObject:fetcher]; + } + } + return fetchers; +} + +#if TARGET_OS_IPHONE && !TARGET_OS_WATCH ++ (void)application:(UIApplication *)application + handleEventsForBackgroundURLSession:(NSString *)identifier + completionHandler:(GTMSessionFetcherSystemCompletionHandler)completionHandler { + GTMSessionFetcher *fetcher = [self fetcherWithSessionIdentifier:identifier]; + if (fetcher != nil) { + fetcher.systemCompletionHandler = completionHandler; + } else { + GTM_LOG_BACKGROUND_SESSION(@"%@ did not create background session identifier: %@", + [self class], identifier); + } +} +#endif + +- (NSString * GTM_NULLABLE_TYPE)sessionIdentifier { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _sessionIdentifier; + } // @synchronized(self) +} + +- (void)setSessionIdentifier:(NSString *)sessionIdentifier { + GTMSESSION_ASSERT_DEBUG(sessionIdentifier != nil, @"Invalid session identifier"); + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + GTMSESSION_ASSERT_DEBUG(!_session, @"Unable to set session identifier after session created"); + _sessionIdentifier = [sessionIdentifier copy]; + _usingBackgroundSession = YES; + _canShareSession = NO; + [self restoreDefaultStateForSessionIdentifierMetadata]; + } // @synchronized(self) +} + +- (void)setSessionIdentifierInternal:(GTM_NULLABLE NSString *)sessionIdentifier { + // This internal method only does a synchronized set of the session identifier. + // It does not have side effects on the background session, shared session, or + // session identifier metadata. + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _sessionIdentifier = [sessionIdentifier copy]; + } // @synchronized(self) +} + +- (NSDictionary * GTM_NULLABLE_TYPE)sessionUserInfo { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (_sessionUserInfo == nil) { + // We'll return the metadata dictionary with internal keys removed. This avoids the user + // re-using the userInfo dictionary later and accidentally including the internal keys. + NSMutableDictionary *metadata = [[self sessionIdentifierMetadataUnsynchronized] mutableCopy]; + NSSet *keysToRemove = [metadata keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) { + return [key hasPrefix:@"_"]; + }]; + [metadata removeObjectsForKeys:[keysToRemove allObjects]]; + if (metadata.count > 0) { + _sessionUserInfo = metadata; + } + } + return _sessionUserInfo; + } // @synchronized(self) +} + +- (void)setSessionUserInfo:(NSDictionary * GTM_NULLABLE_TYPE)dictionary { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + GTMSESSION_ASSERT_DEBUG(_sessionIdentifier == nil, @"Too late to assign userInfo"); + _sessionUserInfo = dictionary; + } // @synchronized(self) +} + +- (GTM_NULLABLE NSDictionary *)sessionIdentifierDefaultMetadata { + GTMSessionCheckSynchronized(self); + + NSMutableDictionary *defaultUserInfo = [[NSMutableDictionary alloc] init]; + if (_destinationFileURL) { + defaultUserInfo[kGTMSessionIdentifierDestinationFileURLMetadataKey] = + [_destinationFileURL absoluteString]; + } + if (_bodyFileURL) { + defaultUserInfo[kGTMSessionIdentifierBodyFileURLMetadataKey] = [_bodyFileURL absoluteString]; + } + return (defaultUserInfo.count > 0) ? defaultUserInfo : nil; +} + +- (void)restoreDefaultStateForSessionIdentifierMetadata { + GTMSessionCheckSynchronized(self); + + NSDictionary *metadata = [self sessionIdentifierMetadataUnsynchronized]; + NSString *destinationFileURLString = metadata[kGTMSessionIdentifierDestinationFileURLMetadataKey]; + if (destinationFileURLString) { + _destinationFileURL = [NSURL URLWithString:destinationFileURLString]; + GTM_LOG_BACKGROUND_SESSION(@"Restoring destination file URL: %@", _destinationFileURL); + } + NSString *bodyFileURLString = metadata[kGTMSessionIdentifierBodyFileURLMetadataKey]; + if (bodyFileURLString) { + _bodyFileURL = [NSURL URLWithString:bodyFileURLString]; + GTM_LOG_BACKGROUND_SESSION(@"Restoring body file URL: %@", _bodyFileURL); + } +} + +- (NSDictionary * GTM_NULLABLE_TYPE)sessionIdentifierMetadata { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return [self sessionIdentifierMetadataUnsynchronized]; + } +} + +- (NSDictionary * GTM_NULLABLE_TYPE)sessionIdentifierMetadataUnsynchronized { + GTMSessionCheckSynchronized(self); + + // Session Identifier format: "com.google.<ClassName>_<UUID>_<Metadata in JSON format> + if (!_sessionIdentifier) { + return nil; + } + NSScanner *metadataScanner = [NSScanner scannerWithString:_sessionIdentifier]; + [metadataScanner setCharactersToBeSkipped:nil]; + NSString *metadataString; + NSString *uuid; + if ([metadataScanner scanUpToString:@"_" intoString:NULL] && + [metadataScanner scanString:@"_" intoString:NULL] && + [metadataScanner scanUpToString:@"_" intoString:&uuid] && + [metadataScanner scanString:@"_" intoString:NULL] && + [metadataScanner scanUpToString:@"\n" intoString:&metadataString]) { + _sessionIdentifierUUID = uuid; + NSData *metadataData = [metadataString dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error; + NSDictionary *metadataDict = + [NSJSONSerialization JSONObjectWithData:metadataData + options:0 + error:&error]; + GTM_LOG_BACKGROUND_SESSION(@"User Info from session identifier: %@ %@", + metadataDict, error ? error : @""); + return metadataDict; + } + return nil; +} + +- (NSString *)createSessionIdentifierWithMetadata:(NSDictionary * GTM_NULLABLE_TYPE)metadataToInclude { + NSString *result; + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + // Session Identifier format: "com.google.<ClassName>_<UUID>_<Metadata in JSON format> + GTMSESSION_ASSERT_DEBUG(!_sessionIdentifier, @"Session identifier already created"); + _sessionIdentifierUUID = [[NSUUID UUID] UUIDString]; + _sessionIdentifier = + [NSString stringWithFormat:@"%@_%@", kGTMSessionIdentifierPrefix, _sessionIdentifierUUID]; + // Start with user-supplied keys so they cannot accidentally override the fetcher's keys. + NSMutableDictionary *metadataDict = + [NSMutableDictionary dictionaryWithDictionary:(NSDictionary * GTM_NONNULL_TYPE)_sessionUserInfo]; + + if (metadataToInclude) { + [metadataDict addEntriesFromDictionary:(NSDictionary *)metadataToInclude]; + } + NSDictionary *defaultMetadataDict = [self sessionIdentifierDefaultMetadata]; + if (defaultMetadataDict) { + [metadataDict addEntriesFromDictionary:defaultMetadataDict]; + } + if (metadataDict.count > 0) { + NSData *metadataData = [NSJSONSerialization dataWithJSONObject:metadataDict + options:0 + error:NULL]; + GTMSESSION_ASSERT_DEBUG(metadataData != nil, + @"Session identifier user info failed to convert to JSON"); + if (metadataData.length > 0) { + NSString *metadataString = [[NSString alloc] initWithData:metadataData + encoding:NSUTF8StringEncoding]; + _sessionIdentifier = + [_sessionIdentifier stringByAppendingFormat:@"_%@", metadataString]; + } + } + _didCreateSessionIdentifier = YES; + result = _sessionIdentifier; + } // @synchronized(self) + return result; +} + +- (void)failToBeginFetchWithError:(NSError *)error { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _hasStoppedFetching = YES; + } + + if (error == nil) { + error = [NSError errorWithDomain:kGTMSessionFetcherErrorDomain + code:GTMSessionFetcherErrorDownloadFailed + userInfo:nil]; + } + + [self invokeFetchCallbacksOnCallbackQueueWithData:nil + error:error]; + [self releaseCallbacks]; + + [_service fetcherDidStop:self]; + + self.authorizer = nil; +} + ++ (GTMSessionCookieStorage *)staticCookieStorage { + static GTMSessionCookieStorage *gCookieStorage = nil; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + gCookieStorage = [[GTMSessionCookieStorage alloc] init]; + }); + return gCookieStorage; +} + +#if GTM_BACKGROUND_TASK_FETCHING + +- (void)endBackgroundTask { + // Whenever the connection stops or background execution expires, + // we need to tell UIApplication we're done. + UIBackgroundTaskIdentifier bgTaskID; + @synchronized(self) { + bgTaskID = self.backgroundTaskIdentifier; + if (bgTaskID != UIBackgroundTaskInvalid) { + self.backgroundTaskIdentifier = UIBackgroundTaskInvalid; + } + } + + if (bgTaskID != UIBackgroundTaskInvalid) { + id<GTMUIApplicationProtocol> app = [[self class] fetcherUIApplication]; + [app endBackgroundTask:bgTaskID]; + } +} + +#endif // GTM_BACKGROUND_TASK_FETCHING + +- (void)authorizeRequest { + GTMSessionCheckNotSynchronized(self); + + id authorizer = self.authorizer; + SEL asyncAuthSel = @selector(authorizeRequest:delegate:didFinishSelector:); + if ([authorizer respondsToSelector:asyncAuthSel]) { + SEL callbackSel = @selector(authorizer:request:finishedWithError:); + NSMutableURLRequest *mutableRequest = [self.request mutableCopy]; + [authorizer authorizeRequest:mutableRequest + delegate:self + didFinishSelector:callbackSel]; + } else { + GTMSESSION_ASSERT_DEBUG(authorizer == nil, @"invalid authorizer for fetch"); + + // No authorizing possible, and authorizing happens only after any delay; + // just begin fetching + [self beginFetchMayDelay:NO + mayAuthorize:NO]; + } +} + +- (void)authorizer:(id<GTMFetcherAuthorizationProtocol>)auth + request:(NSMutableURLRequest *)authorizedRequest + finishedWithError:(NSError *)error { + GTMSessionCheckNotSynchronized(self); + + if (error != nil) { + // We can't fetch without authorization + [self failToBeginFetchWithError:error]; + } else { + @synchronized(self) { + _request = authorizedRequest; + } + [self beginFetchMayDelay:NO + mayAuthorize:NO]; + } +} + + +- (BOOL)canFetchWithBackgroundSession { + // Subclasses may override. + return YES; +} + +// Returns YES if the fetcher has been started and has not yet stopped. +// +// Fetching includes waiting for authorization or for retry, waiting to be allowed by the +// service object to start the request, and actually fetching the request. +- (BOOL)isFetching { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return [self isFetchingUnsynchronized]; + } +} + +- (BOOL)isFetchingUnsynchronized { + GTMSessionCheckSynchronized(self); + + BOOL hasBegun = (_initialBeginFetchDate != nil); + return hasBegun && !_hasStoppedFetching; +} + +- (NSURLResponse * GTM_NULLABLE_TYPE)response { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + NSURLResponse *response = [self responseUnsynchronized]; + return response; + } // @synchronized(self) +} + +- (NSURLResponse * GTM_NULLABLE_TYPE)responseUnsynchronized { + GTMSessionCheckSynchronized(self); + + NSURLResponse *response = _sessionTask.response; + if (!response) response = _response; + return response; +} + +- (NSInteger)statusCode { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + NSInteger statusCode = [self statusCodeUnsynchronized]; + return statusCode; + } // @synchronized(self) +} + +- (NSInteger)statusCodeUnsynchronized { + GTMSessionCheckSynchronized(self); + + NSURLResponse *response = [self responseUnsynchronized]; + NSInteger statusCode; + + if ([response respondsToSelector:@selector(statusCode)]) { + statusCode = [(NSHTTPURLResponse *)response statusCode]; + } else { + // Default to zero, in hopes of hinting "Unknown" (we can't be + // sure that things are OK enough to use 200). + statusCode = 0; + } + return statusCode; +} + +- (NSDictionary * GTM_NULLABLE_TYPE)responseHeaders { + GTMSessionCheckNotSynchronized(self); + + NSURLResponse *response = self.response; + if ([response respondsToSelector:@selector(allHeaderFields)]) { + NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields]; + return headers; + } + return nil; +} + +- (NSDictionary * GTM_NULLABLE_TYPE)responseHeadersUnsynchronized { + GTMSessionCheckSynchronized(self); + + NSURLResponse *response = [self responseUnsynchronized]; + if ([response respondsToSelector:@selector(allHeaderFields)]) { + NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields]; + return headers; + } + return nil; +} + +- (void)releaseCallbacks { + // Avoid releasing blocks in the sync section since objects dealloc'd by + // the blocks being released may call back into the fetcher or fetcher + // service. + dispatch_queue_t NS_VALID_UNTIL_END_OF_SCOPE holdCallbackQueue; + GTMSessionFetcherCompletionHandler NS_VALID_UNTIL_END_OF_SCOPE holdCompletionHandler; + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + holdCallbackQueue = _callbackQueue; + holdCompletionHandler = _completionHandler; + + _callbackQueue = nil; + _completionHandler = nil; // Setter overridden in upload. Setter assumed to be used externally. + } + + // Set local callback pointers to nil here rather than let them release at the end of the scope + // to make any problems due to the blocks being released be a bit more obvious in a stack trace. + holdCallbackQueue = nil; + holdCompletionHandler = nil; + + self.configurationBlock = nil; + self.didReceiveResponseBlock = nil; + self.challengeBlock = nil; + self.willRedirectBlock = nil; + self.sendProgressBlock = nil; + self.receivedProgressBlock = nil; + self.downloadProgressBlock = nil; + self.accumulateDataBlock = nil; + self.willCacheURLResponseBlock = nil; + self.retryBlock = nil; + self.testBlock = nil; + self.resumeDataBlock = nil; +} + +- (void)forgetSessionIdentifierForFetcher { + GTMSessionCheckSynchronized(self); + [self forgetSessionIdentifierForFetcherWithoutSyncCheck]; +} + +- (void)forgetSessionIdentifierForFetcherWithoutSyncCheck { + // This should be called inside a @synchronized block (except during dealloc.) + if (_sessionIdentifier) { + NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIdentifierToFetcherMap]; + [sessionIdentifierToFetcherMap removeObjectForKey:_sessionIdentifier]; + _sessionIdentifier = nil; + _didCreateSessionIdentifier = NO; + } +} + +// External stop method +- (void)stopFetching { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + // Prevent enqueued callbacks from executing. + _userStoppedFetching = YES; + } // @synchronized(self) + [self stopFetchReleasingCallbacks:YES]; +} + +// Cancel the fetch of the URL that's currently in progress. +// +// If shouldReleaseCallbacks is NO then the fetch will be retried so the callbacks +// need to still be retained. +- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks { + [self removePersistedBackgroundSessionFromDefaults]; + + id<GTMSessionFetcherServiceProtocol> service; + NSMutableURLRequest *request; + + // If the task or the retry timer is all that's retaining the fetcher, + // we want to be sure this instance survives stopping at least long enough for + // the stack to unwind. + __autoreleasing GTMSessionFetcher *holdSelf = self; + + BOOL hasCanceledTask = NO; + + [holdSelf destroyRetryTimer]; + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _hasStoppedFetching = YES; + + service = _service; + request = _request; + + if (_sessionTask) { + // In case cancelling the task or session calls this recursively, we want + // to ensure that we'll only release the task and delegate once, + // so first set _sessionTask to nil + // + // This may be called in a callback from the task, so use autorelease to avoid + // releasing the task in its own callback. + __autoreleasing NSURLSessionTask *oldTask = _sessionTask; + if (!_isUsingTestBlock) { + _response = _sessionTask.response; + } + _sessionTask = nil; + + if ([oldTask state] != NSURLSessionTaskStateCompleted) { + // For download tasks, when the fetch is stopped, we may provide resume data that can + // be used to create a new session. + BOOL mayResume = (_resumeDataBlock + && [oldTask respondsToSelector:@selector(cancelByProducingResumeData:)]); + if (!mayResume) { + [oldTask cancel]; + // A side effect of stopping the task is that URLSession:task:didCompleteWithError: + // will be invoked asynchronously on the delegate queue. + } else { + void (^resumeBlock)(NSData *) = _resumeDataBlock; + _resumeDataBlock = nil; + + // Save callbackQueue since releaseCallbacks clears it. + dispatch_queue_t callbackQueue = _callbackQueue; + dispatch_group_enter(_callbackGroup); + [(NSURLSessionDownloadTask *)oldTask cancelByProducingResumeData:^(NSData *resumeData) { + [self invokeOnCallbackQueue:callbackQueue + afterUserStopped:YES + block:^{ + resumeBlock(resumeData); + dispatch_group_leave(self->_callbackGroup); + }]; + }]; + } + hasCanceledTask = YES; + } + } + + // If the task was canceled, wait until the URLSession:task:didCompleteWithError: to call + // finishTasksAndInvalidate, since calling it immediately tends to crash, see radar 18471901. + if (_session) { + BOOL shouldInvalidate = _shouldInvalidateSession; +#if TARGET_OS_IPHONE + // Don't invalidate if we've got a systemCompletionHandler, since + // URLSessionDidFinishEventsForBackgroundURLSession: won't be called if invalidated. + shouldInvalidate = shouldInvalidate && !self.systemCompletionHandler; +#endif + if (shouldInvalidate) { + __autoreleasing NSURLSession *oldSession = _session; + _session = nil; + + if (!hasCanceledTask) { + [oldSession finishTasksAndInvalidate]; + } else { + _sessionNeedingInvalidation = oldSession; + } + } + } + } // @synchronized(self) + + // send the stopped notification + [self sendStopNotificationIfNeeded]; + + [_authorizer stopAuthorizationForRequest:request]; + + if (shouldReleaseCallbacks) { + [self releaseCallbacks]; + + self.authorizer = nil; + } + + [service fetcherDidStop:self]; + +#if GTM_BACKGROUND_TASK_FETCHING + [self endBackgroundTask]; +#endif +} + +- (void)setStopNotificationNeeded:(BOOL)flag { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _isStopNotificationNeeded = flag; + } // @synchronized(self) +} + +- (void)sendStopNotificationIfNeeded { + BOOL sendNow = NO; + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (_isStopNotificationNeeded) { + _isStopNotificationNeeded = NO; + sendNow = YES; + } + } // @synchronized(self) + + if (sendNow) { + [self postNotificationOnMainThreadWithName:kGTMSessionFetcherStoppedNotification + userInfo:nil + requireAsync:NO]; + } +} + +- (void)retryFetch { + [self stopFetchReleasingCallbacks:NO]; + + // A retry will need a configuration with a fresh session identifier. + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (_sessionIdentifier && _didCreateSessionIdentifier) { + [self forgetSessionIdentifierForFetcher]; + _configuration = nil; + } + + if (_canShareSession) { + // Force a grab of the current session from the fetcher service in case + // the service's old one has become invalid. + _session = nil; + } + } // @synchronized(self) + + [self beginFetchForRetry]; +} + +- (BOOL)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds { + // Uncovered in upload fetcher testing, because the chunk fetcher is being waited on, and gets + // released by the upload code. The uploader just holds onto it with an ivar, and that gets + // nilled in the chunk fetcher callback. + // Used once in while loop just to avoid unused variable compiler warning. + __autoreleasing GTMSessionFetcher *holdSelf = self; + + NSDate *giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds]; + + BOOL shouldSpinRunLoop = ([NSThread isMainThread] && + (!self.callbackQueue + || self.callbackQueue == dispatch_get_main_queue())); + BOOL expired = NO; + + // Loop until the callbacks have been called and released, and until + // the connection is no longer pending, until there are no callback dispatches + // in flight, or until the timeout has expired. + int64_t delta = (int64_t)(100 * NSEC_PER_MSEC); // 100 ms + while (1) { + BOOL isTaskInProgress = (holdSelf->_sessionTask + && [_sessionTask state] != NSURLSessionTaskStateCompleted); + BOOL needsToCallCompletion = (_completionHandler != nil); + BOOL isCallbackInProgress = (_callbackGroup + && dispatch_group_wait(_callbackGroup, dispatch_time(DISPATCH_TIME_NOW, delta))); + + if (!isTaskInProgress && !needsToCallCompletion && !isCallbackInProgress) break; + + expired = ([giveUpDate timeIntervalSinceNow] < 0); + if (expired) { + GTMSESSION_LOG_DEBUG(@"GTMSessionFetcher waitForCompletionWithTimeout:%0.1f expired -- " + @"%@%@%@", timeoutInSeconds, + isTaskInProgress ? @"taskInProgress " : @"", + needsToCallCompletion ? @"needsToCallCompletion " : @"", + isCallbackInProgress ? @"isCallbackInProgress" : @""); + break; + } + + // Run the current run loop 1/1000 of a second to give the networking + // code a chance to work + const NSTimeInterval kSpinInterval = 0.001; + if (shouldSpinRunLoop) { + NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:kSpinInterval]; + [[NSRunLoop currentRunLoop] runUntilDate:stopDate]; + } else { + [NSThread sleepForTimeInterval:kSpinInterval]; + } + } + return !expired; +} + ++ (void)setGlobalTestBlock:(GTMSessionFetcherTestBlock GTM_NULLABLE_TYPE)block { +#if GTM_DISABLE_FETCHER_TEST_BLOCK + GTMSESSION_ASSERT_DEBUG(block == nil, @"test blocks disabled"); +#endif + gGlobalTestBlock = [block copy]; +} + +#if GTM_BACKGROUND_TASK_FETCHING + +static GTM_NULLABLE_TYPE id<GTMUIApplicationProtocol> gSubstituteUIApp; + ++ (void)setSubstituteUIApplication:(nullable id<GTMUIApplicationProtocol>)app { + gSubstituteUIApp = app; +} + ++ (nullable id<GTMUIApplicationProtocol>)substituteUIApplication { + return gSubstituteUIApp; +} + ++ (nullable id<GTMUIApplicationProtocol>)fetcherUIApplication { + id<GTMUIApplicationProtocol> app = gSubstituteUIApp; + if (app) return app; + + // iOS App extensions should not call [UIApplication sharedApplication], even + // if UIApplication responds to it. + + static Class applicationClass = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + BOOL isAppExtension = [[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"]; + if (!isAppExtension) { + Class cls = NSClassFromString(@"UIApplication"); + if (cls && [cls respondsToSelector:NSSelectorFromString(@"sharedApplication")]) { + applicationClass = cls; + } + } + }); + + if (applicationClass) { + app = (id<GTMUIApplicationProtocol>)[applicationClass sharedApplication]; + } + return app; +} +#endif // GTM_BACKGROUND_TASK_FETCHING + +#pragma mark NSURLSession Delegate Methods + +// NSURLSession documentation indicates that redirectRequest can be passed to the handler +// but empirically redirectRequest lacks the HTTP body, so passing it will break POSTs. +// Instead, we construct a new request, a copy of the original, with overrides from the +// redirect. + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task +willPerformHTTPRedirection:(NSHTTPURLResponse *)redirectResponse + newRequest:(NSURLRequest *)redirectRequest + completionHandler:(void (^)(NSURLRequest * GTM_NULLABLE_TYPE))handler { + [self setSessionTask:task]; + GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ willPerformHTTPRedirection:%@ newRequest:%@", + [self class], self, session, task, redirectResponse, redirectRequest); + + if ([self userStoppedFetching]) { + handler(nil); + return; + } + if (redirectRequest && redirectResponse) { + // Copy the original request, including the body. + NSURLRequest *originalRequest = self.request; + NSMutableURLRequest *newRequest = [originalRequest mutableCopy]; + + // The new requests's URL overrides the original's URL. + [newRequest setURL:[GTMSessionFetcher redirectURLWithOriginalRequestURL:originalRequest.URL + redirectRequestURL:redirectRequest.URL]]; + + // Any headers in the redirect override headers in the original. + NSDictionary *redirectHeaders = redirectRequest.allHTTPHeaderFields; + for (NSString *key in redirectHeaders) { + NSString *value = [redirectHeaders objectForKey:key]; + [newRequest setValue:value forHTTPHeaderField:key]; + } + + redirectRequest = newRequest; + + // Log the response we just received + [self setResponse:redirectResponse]; + [self logNowWithError:nil]; + + GTMSessionFetcherWillRedirectBlock willRedirectBlock = self.willRedirectBlock; + if (willRedirectBlock) { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + [self invokeOnCallbackQueueAfterUserStopped:YES + block:^{ + willRedirectBlock(redirectResponse, redirectRequest, ^(NSURLRequest *clientRequest) { + + // Update the request for future logging. + [self updateMutableRequest:[clientRequest mutableCopy]]; + + handler(clientRequest); + }); + }]; + } // @synchronized(self) + return; + } + // Continues here if the client did not provide a redirect block. + + // Update the request for future logging. + [self updateMutableRequest:[redirectRequest mutableCopy]]; + } + handler(redirectRequest); +} + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask +didReceiveResponse:(NSURLResponse *)response + completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))handler { + [self setSessionTask:dataTask]; + GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ dataTask:%@ didReceiveResponse:%@", + [self class], self, session, dataTask, response); + void (^accumulateAndFinish)(NSURLSessionResponseDisposition) = + ^(NSURLSessionResponseDisposition dispositionValue) { + // This method is called when the server has determined that it + // has enough information to create the NSURLResponse + // it can be called multiple times, for example in the case of a + // redirect, so each time we reset the data. + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + BOOL hadPreviousData = self->_downloadedLength > 0; + + [self->_downloadedData setLength:0]; + self->_downloadedLength = 0; + + if (hadPreviousData && (dispositionValue != NSURLSessionResponseCancel)) { + // Tell the accumulate block to discard prior data. + GTMSessionFetcherAccumulateDataBlock accumulateBlock = self->_accumulateDataBlock; + if (accumulateBlock) { + [self invokeOnCallbackQueueUnlessStopped:^{ + accumulateBlock(nil); + }]; + } + } + } // @synchronized(self) + handler(dispositionValue); + }; + + GTMSessionFetcherDidReceiveResponseBlock receivedResponseBlock; + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + receivedResponseBlock = _didReceiveResponseBlock; + if (receivedResponseBlock) { + // We will ultimately need to call back to NSURLSession's handler with the disposition value + // for this delegate method even if the user has stopped the fetcher. + [self invokeOnCallbackQueueAfterUserStopped:YES + block:^{ + receivedResponseBlock(response, ^(NSURLSessionResponseDisposition desiredDisposition) { + accumulateAndFinish(desiredDisposition); + }); + }]; + } + } // @synchronized(self) + + if (receivedResponseBlock == nil) { + accumulateAndFinish(NSURLSessionResponseAllow); + } +} + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask +didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask { + GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ dataTask:%@ didBecomeDownloadTask:%@", + [self class], self, session, dataTask, downloadTask); + [self setSessionTask:downloadTask]; +} + + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task +didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge + completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, + NSURLCredential * GTM_NULLABLE_TYPE credential))handler { + [self setSessionTask:task]; + GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ didReceiveChallenge:%@", + [self class], self, session, task, challenge); + + GTMSessionFetcherChallengeBlock challengeBlock = self.challengeBlock; + if (challengeBlock) { + // The fetcher user has provided custom challenge handling. + // + // We will ultimately need to call back to NSURLSession's handler with the disposition value + // for this delegate method even if the user has stopped the fetcher. + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + [self invokeOnCallbackQueueAfterUserStopped:YES + block:^{ + challengeBlock(self, challenge, handler); + }]; + } + } else { + // No challenge block was provided by the client. + [self respondToChallenge:challenge + completionHandler:handler]; + } +} + +- (void)respondToChallenge:(NSURLAuthenticationChallenge *)challenge + completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, + NSURLCredential * GTM_NULLABLE_TYPE credential))handler { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + NSInteger previousFailureCount = [challenge previousFailureCount]; + if (previousFailureCount <= 2) { + NSURLProtectionSpace *protectionSpace = [challenge protectionSpace]; + NSString *authenticationMethod = [protectionSpace authenticationMethod]; + if ([authenticationMethod isEqual:NSURLAuthenticationMethodServerTrust]) { + // SSL. + // + // Background sessions seem to require an explicit check of the server trust object + // rather than default handling. + SecTrustRef serverTrust = challenge.protectionSpace.serverTrust; + if (serverTrust == NULL) { + // No server trust information is available. + handler(NSURLSessionAuthChallengePerformDefaultHandling, nil); + } else { + // Server trust information is available. + void (^callback)(SecTrustRef, BOOL) = ^(SecTrustRef trustRef, BOOL allow){ + if (allow) { + NSURLCredential *trustCredential = [NSURLCredential credentialForTrust:trustRef]; + handler(NSURLSessionAuthChallengeUseCredential, trustCredential); + } else { + GTMSESSION_LOG_DEBUG(@"Cancelling authentication challenge for %@", self->_request.URL); + handler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); + } + }; + if (_allowInvalidServerCertificates) { + callback(serverTrust, YES); + } else { + [[self class] evaluateServerTrust:serverTrust + forRequest:_request + completionHandler:callback]; + } + } + return; + } + + NSURLCredential *credential = _credential; + + if ([[challenge protectionSpace] isProxy] && _proxyCredential != nil) { + credential = _proxyCredential; + } + + if (credential) { + handler(NSURLSessionAuthChallengeUseCredential, credential); + } else { + // The credential is still nil; tell the OS to use the default handling. This is needed + // for things that can come out of the keychain (proxies, client certificates, etc.). + // + // Note: Looking up a credential with NSURLCredentialStorage's + // defaultCredentialForProtectionSpace: is *not* the same invoking the handler with + // NSURLSessionAuthChallengePerformDefaultHandling. In the case of + // NSURLAuthenticationMethodClientCertificate, you can get nil back from + // NSURLCredentialStorage, while using this code path instead works. + handler(NSURLSessionAuthChallengePerformDefaultHandling, nil); + } + + } else { + // We've failed auth 3 times. The completion handler will be called with code + // NSURLErrorCancelled. + handler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); + } + } // @synchronized(self) +} + +// Return redirect URL based on the original request URL and redirect request URL. +// +// Method disallows any scheme changes between the original request URL and redirect request URL +// aside from "http" to "https". If a change in scheme is detected the redirect URL inherits the +// scheme from the original request URL. ++ (GTM_NULLABLE NSURL *)redirectURLWithOriginalRequestURL:(GTM_NULLABLE NSURL *)originalRequestURL + redirectRequestURL:(GTM_NULLABLE NSURL *)redirectRequestURL { + // In the case of an NSURLSession redirect, neither URL should ever be nil; as a sanity check + // if either is nil return the other URL. + if (!redirectRequestURL) return originalRequestURL; + if (!originalRequestURL) return redirectRequestURL; + + NSString *originalScheme = originalRequestURL.scheme; + NSString *redirectScheme = redirectRequestURL.scheme; + BOOL insecureToSecureRedirect = + (originalScheme != nil && [originalScheme caseInsensitiveCompare:@"http"] == NSOrderedSame && + redirectScheme != nil && [redirectScheme caseInsensitiveCompare:@"https"] == NSOrderedSame); + + // Check for changes to the scheme and disallow any changes except for http to https. + if (!insecureToSecureRedirect && + (redirectScheme.length != originalScheme.length || + [redirectScheme caseInsensitiveCompare:originalScheme] != NSOrderedSame)) { + NSURLComponents *components = + [NSURLComponents componentsWithURL:(NSURL * _Nonnull)redirectRequestURL + resolvingAgainstBaseURL:NO]; + components.scheme = originalScheme; + return components.URL; + } + + return redirectRequestURL; +} + +// Validate the certificate chain. +// +// This may become a public method if it appears to be useful to users. ++ (void)evaluateServerTrust:(SecTrustRef)serverTrust + forRequest:(NSURLRequest *)request + completionHandler:(void (^)(SecTrustRef trustRef, BOOL allow))handler { + // Retain the trust object to avoid a SecTrustEvaluate() crash on iOS 7. + CFRetain(serverTrust); + + // Evaluate the certificate chain. + // + // The delegate queue may be the main thread. Trust evaluation could cause some + // blocking network activity, so we must evaluate async, as documented at + // https://developer.apple.com/library/ios/technotes/tn2232/ + // + // We must also avoid multiple uses of the trust object, per docs: + // "It is not safe to call this function concurrently with any other function that uses + // the same trust management object, or to re-enter this function for the same trust + // management object." + // + // SecTrustEvaluateAsync both does sync execution of Evaluate and calls back on the + // queue passed to it, according to at sources in + // http://www.opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-55050.9/lib/SecTrust.cpp + // It would require a global serial queue to ensure the evaluate happens only on a + // single thread at a time, so we'll stick with using SecTrustEvaluate on a background + // thread. + dispatch_queue_t evaluateBackgroundQueue = + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(evaluateBackgroundQueue, ^{ + // It looks like the implementation of SecTrustEvaluate() on Mac grabs a global lock, + // so it may be redundant for us to also lock, but it's easy to synchronize here + // anyway. + SecTrustResultType trustEval = kSecTrustResultInvalid; + BOOL shouldAllow; + OSStatus trustError; + @synchronized([GTMSessionFetcher class]) { + GTMSessionMonitorSynchronized([GTMSessionFetcher class]); + + trustError = SecTrustEvaluate(serverTrust, &trustEval); + } + if (trustError != errSecSuccess) { + GTMSESSION_LOG_DEBUG(@"Error %d evaluating trust for %@", + (int)trustError, request); + shouldAllow = NO; + } else { + // Having a trust level "unspecified" by the user is the usual result, described at + // https://developer.apple.com/library/mac/qa/qa1360 + if (trustEval == kSecTrustResultUnspecified + || trustEval == kSecTrustResultProceed) { + shouldAllow = YES; + } else { + shouldAllow = NO; + GTMSESSION_LOG_DEBUG(@"Challenge SecTrustResultType %u for %@, properties: %@", + trustEval, request.URL.host, + CFBridgingRelease(SecTrustCopyProperties(serverTrust))); + } + } + handler(serverTrust, shouldAllow); + + CFRelease(serverTrust); + }); +} + +- (void)invokeOnCallbackQueueUnlessStopped:(void (^)(void))block { + [self invokeOnCallbackQueueAfterUserStopped:NO + block:block]; +} + +- (void)invokeOnCallbackQueueAfterUserStopped:(BOOL)afterStopped + block:(void (^)(void))block { + GTMSessionCheckSynchronized(self); + + [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:afterStopped + block:block]; +} + +- (void)invokeOnCallbackUnsynchronizedQueueAfterUserStopped:(BOOL)afterStopped + block:(void (^)(void))block { + // testBlock simulation code may not be synchronizing when this is invoked. + [self invokeOnCallbackQueue:_callbackQueue + afterUserStopped:afterStopped + block:block]; +} + +- (void)invokeOnCallbackQueue:(dispatch_queue_t)callbackQueue + afterUserStopped:(BOOL)afterStopped + block:(void (^)(void))block { + if (callbackQueue) { + dispatch_group_async(_callbackGroup, callbackQueue, ^{ + if (!afterStopped) { + NSDate *serviceStoppedAllDate = [self->_service stoppedAllFetchersDate]; + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + // Avoid a race between stopFetching and the callback. + if (self->_userStoppedFetching) { + return; + } + + // Also avoid calling back if the service has stopped all fetchers + // since this one was created. The fetcher may have stopped before + // stopAllFetchers was invoked, so _userStoppedFetching wasn't set, + // but the app still won't expect the callback to fire after + // the service's stopAllFetchers was invoked. + if (serviceStoppedAllDate + && [self->_initialBeginFetchDate compare:serviceStoppedAllDate] != NSOrderedDescending) { + // stopAllFetchers was called after this fetcher began. + return; + } + } // @synchronized(self) + } + block(); + }); + } +} + +- (void)invokeFetchCallbacksOnCallbackQueueWithData:(GTM_NULLABLE NSData *)data + error:(GTM_NULLABLE NSError *)error { + // Callbacks will be released in the method stopFetchReleasingCallbacks: + GTMSessionFetcherCompletionHandler handler; + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + handler = _completionHandler; + + if (handler) { + [self invokeOnCallbackQueueUnlessStopped:^{ + handler(data, error); + + // Post a notification, primarily to allow code to collect responses for + // testing. + // + // The observing code is not likely on the fetcher's callback + // queue, so this posts explicitly to the main queue. + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + if (data) { + userInfo[kGTMSessionFetcherCompletionDataKey] = data; + } + if (error) { + userInfo[kGTMSessionFetcherCompletionErrorKey] = error; + } + [self postNotificationOnMainThreadWithName:kGTMSessionFetcherCompletionInvokedNotification + userInfo:userInfo + requireAsync:NO]; + }]; + } + } // @synchronized(self) +} + +- (void)postNotificationOnMainThreadWithName:(NSString *)noteName + userInfo:(GTM_NULLABLE NSDictionary *)userInfo + requireAsync:(BOOL)requireAsync { + dispatch_block_t postBlock = ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:noteName + object:self + userInfo:userInfo]; + }; + + if ([NSThread isMainThread] && !requireAsync) { + // Post synchronously for compatibility with older code using the fetcher. + + // Avoid calling out to other code from inside a sync block to avoid risk + // of a deadlock or of recursive sync. + GTMSessionCheckNotSynchronized(self); + + postBlock(); + } else { + dispatch_async(dispatch_get_main_queue(), postBlock); + } +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)uploadTask + needNewBodyStream:(void (^)(NSInputStream * GTM_NULLABLE_TYPE bodyStream))completionHandler { + [self setSessionTask:uploadTask]; + GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ needNewBodyStream:", + [self class], self, session, uploadTask); + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + GTMSessionFetcherBodyStreamProvider provider = _bodyStreamProvider; +#if !STRIP_GTM_FETCH_LOGGING + if ([self respondsToSelector:@selector(loggedStreamProviderForStreamProvider:)]) { + provider = [self performSelector:@selector(loggedStreamProviderForStreamProvider:) + withObject:provider]; + } +#endif + if (provider) { + [self invokeOnCallbackQueueUnlessStopped:^{ + provider(completionHandler); + }]; + } else { + GTMSESSION_ASSERT_DEBUG(NO, @"NSURLSession expects a stream provider"); + + completionHandler(nil); + } + } // @synchronized(self) +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + didSendBodyData:(int64_t)bytesSent + totalBytesSent:(int64_t)totalBytesSent +totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend { + [self setSessionTask:task]; + GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ didSendBodyData:%lld" + @" totalBytesSent:%lld totalBytesExpectedToSend:%lld", + [self class], self, session, task, bytesSent, totalBytesSent, + totalBytesExpectedToSend); + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (!_sendProgressBlock) { + return; + } + // We won't hold on to send progress block; it's ok to not send it if the upload finishes. + [self invokeOnCallbackQueueUnlessStopped:^{ + GTMSessionFetcherSendProgressBlock progressBlock; + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + progressBlock = self->_sendProgressBlock; + } + if (progressBlock) { + progressBlock(bytesSent, totalBytesSent, totalBytesExpectedToSend); + } + }]; + } // @synchronized(self) +} + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask + didReceiveData:(NSData *)data { + [self setSessionTask:dataTask]; + NSUInteger bufferLength = data.length; + GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ dataTask:%@ didReceiveData:%p (%llu bytes)", + [self class], self, session, dataTask, data, + (unsigned long long)bufferLength); + if (bufferLength == 0) { + // Observed on completing an out-of-process upload. + return; + } + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + GTMSessionFetcherAccumulateDataBlock accumulateBlock = _accumulateDataBlock; + if (accumulateBlock) { + // Let the client accumulate the data. + _downloadedLength += bufferLength; + [self invokeOnCallbackQueueUnlessStopped:^{ + accumulateBlock(data); + }]; + } else if (!_userStoppedFetching) { + // Append to the mutable data buffer unless the fetch has been cancelled. + + // Resumed upload tasks may not yet have a data buffer. + if (_downloadedData == nil) { + // Using NSClassFromString for iOS 6 compatibility. + GTMSESSION_ASSERT_DEBUG( + ![dataTask isKindOfClass:NSClassFromString(@"NSURLSessionDownloadTask")], + @"Resumed download tasks should not receive data bytes"); + _downloadedData = [[NSMutableData alloc] init]; + } + + [_downloadedData appendData:data]; + _downloadedLength = (int64_t)_downloadedData.length; + + // We won't hold on to receivedProgressBlock here; it's ok to not send + // it if the transfer finishes. + if (_receivedProgressBlock) { + [self invokeOnCallbackQueueUnlessStopped:^{ + GTMSessionFetcherReceivedProgressBlock progressBlock; + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + progressBlock = self->_receivedProgressBlock; + } + if (progressBlock) { + progressBlock((int64_t)bufferLength, self->_downloadedLength); + } + }]; + } + } + } // @synchronized(self) +} + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask + willCacheResponse:(NSCachedURLResponse *)proposedResponse + completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler { + GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ dataTask:%@ willCacheResponse:%@ %@", + [self class], self, session, dataTask, + proposedResponse, proposedResponse.response); + GTMSessionFetcherWillCacheURLResponseBlock callback; + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + callback = _willCacheURLResponseBlock; + + if (callback) { + [self invokeOnCallbackQueueAfterUserStopped:YES + block:^{ + callback(proposedResponse, completionHandler); + }]; + } + } // @synchronized(self) + if (!callback) { + completionHandler(proposedResponse); + } +} + + +- (void)URLSession:(NSURLSession *)session + downloadTask:(NSURLSessionDownloadTask *)downloadTask + didWriteData:(int64_t)bytesWritten + totalBytesWritten:(int64_t)totalBytesWritten +totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { + GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ downloadTask:%@ didWriteData:%lld" + @" bytesWritten:%lld totalBytesExpectedToWrite:%lld", + [self class], self, session, downloadTask, bytesWritten, + totalBytesWritten, totalBytesExpectedToWrite); + [self setSessionTask:downloadTask]; + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if ((totalBytesExpectedToWrite != NSURLSessionTransferSizeUnknown) && + (totalBytesExpectedToWrite < totalBytesWritten)) { + // Have observed cases were bytesWritten == totalBytesExpectedToWrite, + // but totalBytesWritten > totalBytesExpectedToWrite, so setting to unkown in these cases. + totalBytesExpectedToWrite = NSURLSessionTransferSizeUnknown; + } + // We won't hold on to download progress block during the enqueue; + // it's ok to not send it if the upload finishes. + + [self invokeOnCallbackQueueUnlessStopped:^{ + GTMSessionFetcherDownloadProgressBlock progressBlock; + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + progressBlock = self->_downloadProgressBlock; + } + if (progressBlock) { + progressBlock(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite); + } + }]; + } // @synchronized(self) +} + +- (void)URLSession:(NSURLSession *)session + downloadTask:(NSURLSessionDownloadTask *)downloadTask + didResumeAtOffset:(int64_t)fileOffset +expectedTotalBytes:(int64_t)expectedTotalBytes { + GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ downloadTask:%@ didResumeAtOffset:%lld" + @" expectedTotalBytes:%lld", + [self class], self, session, downloadTask, fileOffset, + expectedTotalBytes); + [self setSessionTask:downloadTask]; +} + +- (void)URLSession:(NSURLSession *)session + downloadTask:(NSURLSessionDownloadTask *)downloadTask +didFinishDownloadingToURL:(NSURL *)downloadLocationURL { + // Download may have relaunched app, so update _sessionTask. + [self setSessionTask:downloadTask]; + GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ downloadTask:%@ didFinishDownloadingToURL:%@", + [self class], self, session, downloadTask, downloadLocationURL); + NSNumber *fileSizeNum; + [downloadLocationURL getResourceValue:&fileSizeNum + forKey:NSURLFileSizeKey + error:NULL]; + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + NSURL *destinationURL = _destinationFileURL; + + _downloadedLength = fileSizeNum.longLongValue; + + // Overwrite any previous file at the destination URL. + NSFileManager *fileMgr = [NSFileManager defaultManager]; + NSError *removeError; + if (![fileMgr removeItemAtURL:destinationURL error:&removeError] + && removeError.code != NSFileNoSuchFileError) { + GTMSESSION_LOG_DEBUG(@"Could not remove previous file at %@ due to %@", + downloadLocationURL.path, removeError); + } + + NSInteger statusCode = [self statusCodeUnsynchronized]; + if (statusCode < 200 || statusCode > 399) { + // In OS X 10.11, the response body is written to a file even on a server + // status error. For convenience of the fetcher client, we'll skip saving the + // downloaded body to the destination URL so that clients do not need to know + // to delete the file following fetch errors. + GTMSESSION_LOG_DEBUG(@"Abandoning download due to status %ld, file %@", + (long)statusCode, downloadLocationURL.path); + + // On error code, add the contents of the temporary file to _downloadTaskErrorData + // This way fetcher clients have access to error details possibly passed by the server. + if (_downloadedLength > 0 && _downloadedLength <= kMaximumDownloadErrorDataLength) { + _downloadTaskErrorData = [NSData dataWithContentsOfURL:downloadLocationURL]; + } else if (_downloadedLength > kMaximumDownloadErrorDataLength) { + GTMSESSION_LOG_DEBUG(@"Download error data for file %@ not passed to userInfo due to size " + @"%lld", downloadLocationURL.path, _downloadedLength); + } + } else { + NSError *moveError; + NSURL *destinationFolderURL = [destinationURL URLByDeletingLastPathComponent]; + BOOL didMoveDownload = NO; + if ([fileMgr createDirectoryAtURL:destinationFolderURL + withIntermediateDirectories:YES + attributes:nil + error:&moveError]) { + didMoveDownload = [fileMgr moveItemAtURL:downloadLocationURL + toURL:destinationURL + error:&moveError]; + } + if (!didMoveDownload) { + _downloadFinishedError = moveError; + } + GTM_LOG_BACKGROUND_SESSION(@"%@ %p Moved download from \"%@\" to \"%@\" %@", + [self class], self, + downloadLocationURL.path, destinationURL.path, + error ? error : @""); + } + } // @synchronized(self) +} + +/* Sent as the last message related to a specific task. Error may be + * nil, which implies that no error occurred and this task is complete. + */ +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task +didCompleteWithError:(NSError *)error { + [self setSessionTask:task]; + GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ didCompleteWithError:%@", + [self class], self, session, task, error); + + NSInteger status = self.statusCode; + BOOL forceAssumeRetry = NO; + BOOL succeeded = NO; + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + +#if !GTM_DISABLE_FETCHER_TEST_BLOCK + // The task is never resumed when a testBlock is used. When the session is destroyed, + // we should ignore the callback, since the testBlock support code itself invokes + // shouldRetryNowForStatus: and finishWithError:shouldRetry: + if (_isUsingTestBlock) return; +#endif + + if (error == nil) { + error = _downloadFinishedError; + } + succeeded = (error == nil && status >= 0 && status < 300); + if (succeeded) { + // Succeeded. + _bodyLength = task.countOfBytesSent; + } + } // @synchronized(self) + + if (succeeded) { + [self finishWithError:nil shouldRetry:NO]; + return; + } + // For background redirects, no delegate method is called, so we cannot restore a stripped + // Authorization header, so if a 403 ("Forbidden") was generated due to a missing OAuth 2 header, + // set the current request's URL to the redirected URL, so we in effect restore the Authorization + // header. + if ((status == 403) && self.usingBackgroundSession) { + NSURL *redirectURL = self.response.URL; + NSURLRequest *request = self.request; + if (![request.URL isEqual:redirectURL]) { + NSString *authorizationHeader = [request.allHTTPHeaderFields objectForKey:@"Authorization"]; + if (authorizationHeader != nil) { + NSMutableURLRequest *mutableRequest = [request mutableCopy]; + mutableRequest.URL = redirectURL; + [self updateMutableRequest:mutableRequest]; + // Avoid assuming the session is still valid. + self.session = nil; + forceAssumeRetry = YES; + } + } + } + + // If invalidating the session was deferred in stopFetchReleasingCallbacks: then do it now. + NSURLSession *oldSession = self.sessionNeedingInvalidation; + if (oldSession) { + [self setSessionNeedingInvalidation:NULL]; + [oldSession finishTasksAndInvalidate]; + } + + // Failed. + [self shouldRetryNowForStatus:status + error:error + forceAssumeRetry:forceAssumeRetry + response:^(BOOL shouldRetry) { + [self finishWithError:error shouldRetry:shouldRetry]; + }]; +} + +#if TARGET_OS_IPHONE +- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { + GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSessionDidFinishEventsForBackgroundURLSession:%@", + [self class], self, session); + [self removePersistedBackgroundSessionFromDefaults]; + + GTMSessionFetcherSystemCompletionHandler handler; + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + handler = self.systemCompletionHandler; + self.systemCompletionHandler = nil; + } // @synchronized(self) + if (handler) { + GTM_LOG_BACKGROUND_SESSION(@"%@ %p Calling system completionHandler", [self class], self); + handler(); + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + NSURLSession *oldSession = _session; + _session = nil; + if (_shouldInvalidateSession) { + [oldSession finishTasksAndInvalidate]; + } + } // @synchronized(self) + } +} +#endif + +- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(GTM_NULLABLE NSError *)error { + // This may happen repeatedly for retries. On authentication callbacks, the retry + // may begin before the prior session sends the didBecomeInvalid delegate message. + GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ didBecomeInvalidWithError:%@", + [self class], self, session, error); + if (session == (NSURLSession *)self.session) { + GTM_LOG_SESSION_DELEGATE(@" Unexpected retained invalid session: %@", session); + self.session = nil; + } +} + +- (void)finishWithError:(GTM_NULLABLE NSError *)error shouldRetry:(BOOL)shouldRetry { + [self removePersistedBackgroundSessionFromDefaults]; + + BOOL shouldStopFetching = YES; + NSData *downloadedData = nil; +#if !STRIP_GTM_FETCH_LOGGING + BOOL shouldDeferLogging = NO; +#endif + BOOL shouldBeginRetryTimer = NO; + NSInteger status = [self statusCode]; + NSURL *destinationURL = self.destinationFileURL; + + BOOL fetchSucceeded = (error == nil && status >= 0 && status < 300); + +#if !STRIP_GTM_FETCH_LOGGING + if (!fetchSucceeded) { + if (!shouldDeferLogging && !self.hasLoggedError) { + [self logNowWithError:error]; + self.hasLoggedError = YES; + } + } +#endif // !STRIP_GTM_FETCH_LOGGING + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + +#if !STRIP_GTM_FETCH_LOGGING + shouldDeferLogging = _deferResponseBodyLogging; +#endif + if (fetchSucceeded) { + // Success + if ((_downloadedData.length > 0) && (destinationURL != nil)) { + // Overwrite any previous file at the destination URL. + NSFileManager *fileMgr = [NSFileManager defaultManager]; + [fileMgr removeItemAtURL:destinationURL + error:NULL]; + NSURL *destinationFolderURL = [destinationURL URLByDeletingLastPathComponent]; + BOOL didMoveDownload = NO; + if ([fileMgr createDirectoryAtURL:destinationFolderURL + withIntermediateDirectories:YES + attributes:nil + error:&error]) { + didMoveDownload = [_downloadedData writeToURL:destinationURL + options:NSDataWritingAtomic + error:&error]; + } + if (didMoveDownload) { + _downloadedData = nil; + } else { + _downloadFinishedError = error; + } + } + downloadedData = _downloadedData; + } else { + // Unsuccessful with error or status over 300. Retry or notify the delegate of failure + if (shouldRetry) { + // Retrying. + shouldBeginRetryTimer = YES; + shouldStopFetching = NO; + } else { + if (error == nil) { + // Create an error. + NSDictionary *userInfo = GTMErrorUserInfoForData( + _downloadedData.length > 0 ? _downloadedData : _downloadTaskErrorData, + [self responseHeadersUnsynchronized]); + + error = [NSError errorWithDomain:kGTMSessionFetcherStatusDomain + code:status + userInfo:userInfo]; + } else { + // If the error had resume data, and the client supplied a resume block, pass the + // data to the client. + void (^resumeBlock)(NSData *) = _resumeDataBlock; + _resumeDataBlock = nil; + if (resumeBlock) { + NSData *resumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData]; + if (resumeData) { + [self invokeOnCallbackQueueAfterUserStopped:YES block:^{ + resumeBlock(resumeData); + }]; + } + } + } + if (_downloadedData.length > 0) { + downloadedData = _downloadedData; + } + // If the error occurred after retries, report the number and duration of the + // retries. This provides a clue to a developer looking at the error description + // that the fetcher did retry before failing with this error. + if (_retryCount > 0) { + NSMutableDictionary *userInfoWithRetries = + [NSMutableDictionary dictionaryWithDictionary:(NSDictionary *)error.userInfo]; + NSTimeInterval timeSinceInitialRequest = -[_initialRequestDate timeIntervalSinceNow]; + [userInfoWithRetries setObject:@(timeSinceInitialRequest) + forKey:kGTMSessionFetcherElapsedIntervalWithRetriesKey]; + [userInfoWithRetries setObject:@(_retryCount) + forKey:kGTMSessionFetcherNumberOfRetriesDoneKey]; + error = [NSError errorWithDomain:(NSString *)error.domain + code:error.code + userInfo:userInfoWithRetries]; + } + } + } + } // @synchronized(self) + + if (shouldBeginRetryTimer) { + [self beginRetryTimer]; + } + + // We want to send the stop notification before calling the delegate's + // callback selector, since the callback selector may release all of + // the fetcher properties that the client is using to track the fetches. + // + // We'll also stop now so that, to any observers watching the notifications, + // it doesn't look like our wait for a retry (which may be long, + // 30 seconds or more) is part of the network activity. + [self sendStopNotificationIfNeeded]; + + if (shouldStopFetching) { + [self invokeFetchCallbacksOnCallbackQueueWithData:downloadedData + error:error]; + // The upload subclass doesn't want to release callbacks until upload chunks have completed. + BOOL shouldRelease = [self shouldReleaseCallbacksUponCompletion]; + [self stopFetchReleasingCallbacks:shouldRelease]; + } + +#if !STRIP_GTM_FETCH_LOGGING + // _hasLoggedError is only set by this method + if (!shouldDeferLogging && !_hasLoggedError) { + [self logNowWithError:error]; + } +#endif +} + +- (BOOL)shouldReleaseCallbacksUponCompletion { + // A subclass can override this to keep callbacks around after the + // connection has finished successfully + return YES; +} + +- (void)logNowWithError:(GTM_NULLABLE NSError *)error { + GTMSessionCheckNotSynchronized(self); + + // If the logging category is available, then log the current request, + // response, data, and error + if ([self respondsToSelector:@selector(logFetchWithError:)]) { + [self performSelector:@selector(logFetchWithError:) withObject:error]; + } +} + +#pragma mark Retries + +- (BOOL)isRetryError:(NSError *)error { + struct RetryRecord { + __unsafe_unretained NSString *const domain; + NSInteger code; + }; + + struct RetryRecord retries[] = { + { kGTMSessionFetcherStatusDomain, 408 }, // request timeout + { kGTMSessionFetcherStatusDomain, 502 }, // failure gatewaying to another server + { kGTMSessionFetcherStatusDomain, 503 }, // service unavailable + { kGTMSessionFetcherStatusDomain, 504 }, // request timeout + { NSURLErrorDomain, NSURLErrorTimedOut }, + { NSURLErrorDomain, NSURLErrorNetworkConnectionLost }, + { nil, 0 } + }; + + // NSError's isEqual always returns false for equal but distinct instances + // of NSError, so we have to compare the domain and code values explicitly + NSString *domain = error.domain; + NSInteger code = error.code; + for (int idx = 0; retries[idx].domain != nil; idx++) { + if (code == retries[idx].code && [domain isEqual:retries[idx].domain]) { + return YES; + } + } + return NO; +} + +// shouldRetryNowForStatus:error: responds with YES if the user has enabled retries +// and the status or error is one that is suitable for retrying. "Suitable" +// means either the isRetryError:'s list contains the status or error, or the +// user's retry block is present and returns YES when called, or the +// authorizer may be able to fix. +- (void)shouldRetryNowForStatus:(NSInteger)status + error:(NSError *)error + forceAssumeRetry:(BOOL)forceAssumeRetry + response:(GTMSessionFetcherRetryResponse)response { + // Determine if a refreshed authorizer may avoid an authorization error + BOOL willRetry = NO; + + // We assume _authorizer is immutable after beginFetch, and _hasAttemptedAuthRefresh is modified + // only in this method, and this method is invoked on the serial delegate queue. + // + // We want to avoid calling the authorizer from inside a sync block. + BOOL isFirstAuthError = (_authorizer != nil + && !_hasAttemptedAuthRefresh + && status == GTMSessionFetcherStatusUnauthorized); // 401 + + BOOL hasPrimed = NO; + if (isFirstAuthError) { + if ([_authorizer respondsToSelector:@selector(primeForRefresh)]) { + hasPrimed = [_authorizer primeForRefresh]; + } + } + + BOOL shouldRetryForAuthRefresh = NO; + if (hasPrimed) { + shouldRetryForAuthRefresh = YES; + _hasAttemptedAuthRefresh = YES; + [self updateRequestValue:nil forHTTPHeaderField:@"Authorization"]; + } + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + BOOL shouldDoRetry = [self isRetryEnabledUnsynchronized]; + if (shouldDoRetry && ![self hasRetryAfterInterval]) { + + // Determine if we're doing exponential backoff retries + shouldDoRetry = [self nextRetryIntervalUnsynchronized] < _maxRetryInterval; + + if (shouldDoRetry) { + // If an explicit max retry interval was set, we expect repeated backoffs to take + // up to roughly twice that for repeated fast failures. If the initial attempt is + // already more than 3 times the max retry interval, then failures have taken a long time + // (such as from network timeouts) so don't retry again to avoid the app becoming + // unexpectedly unresponsive. + if (_maxRetryInterval > 0) { + NSTimeInterval maxAllowedIntervalBeforeRetry = _maxRetryInterval * 3; + NSTimeInterval timeSinceInitialRequest = -[_initialRequestDate timeIntervalSinceNow]; + if (timeSinceInitialRequest > maxAllowedIntervalBeforeRetry) { + shouldDoRetry = NO; + } + } + } + } + BOOL canRetry = shouldRetryForAuthRefresh || forceAssumeRetry || shouldDoRetry; + if (canRetry) { + NSDictionary *userInfo = + GTMErrorUserInfoForData(_downloadedData, [self responseHeadersUnsynchronized]); + NSError *statusError = [NSError errorWithDomain:kGTMSessionFetcherStatusDomain + code:status + userInfo:userInfo]; + if (error == nil) { + error = statusError; + } + willRetry = shouldRetryForAuthRefresh || + forceAssumeRetry || + [self isRetryError:error] || + ((error != statusError) && [self isRetryError:statusError]); + + // If the user has installed a retry callback, consult that. + GTMSessionFetcherRetryBlock retryBlock = _retryBlock; + if (retryBlock) { + [self invokeOnCallbackQueueUnlessStopped:^{ + retryBlock(willRetry, error, response); + }]; + return; + } + } + } // @synchronized(self) + response(willRetry); +} + +- (BOOL)hasRetryAfterInterval { + GTMSessionCheckSynchronized(self); + + NSDictionary *responseHeaders = [self responseHeadersUnsynchronized]; + NSString *retryAfterValue = [responseHeaders valueForKey:@"Retry-After"]; + return (retryAfterValue != nil); +} + +- (NSTimeInterval)retryAfterInterval { + GTMSessionCheckSynchronized(self); + + NSDictionary *responseHeaders = [self responseHeadersUnsynchronized]; + NSString *retryAfterValue = [responseHeaders valueForKey:@"Retry-After"]; + if (retryAfterValue == nil) { + return 0; + } + // Retry-After formatted as HTTP-date | delta-seconds + // Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + NSDateFormatter *rfc1123DateFormatter = [[NSDateFormatter alloc] init]; + rfc1123DateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; + rfc1123DateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"]; + rfc1123DateFormatter.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss z"; + NSDate *retryAfterDate = [rfc1123DateFormatter dateFromString:retryAfterValue]; + NSTimeInterval retryAfterInterval = (retryAfterDate != nil) ? + retryAfterDate.timeIntervalSinceNow : retryAfterValue.intValue; + retryAfterInterval = MAX(0, retryAfterInterval); + return retryAfterInterval; +} + +- (void)beginRetryTimer { + if (![NSThread isMainThread]) { + // Defer creating and starting the timer until we're on the main thread to ensure it has + // a run loop. + dispatch_group_async(_callbackGroup, dispatch_get_main_queue(), ^{ + [self beginRetryTimer]; + }); + return; + } + + [self destroyRetryTimer]; + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + NSTimeInterval nextInterval = [self nextRetryIntervalUnsynchronized]; + NSTimeInterval maxInterval = _maxRetryInterval; + NSTimeInterval newInterval = MIN(nextInterval, (maxInterval > 0 ? maxInterval : DBL_MAX)); + NSTimeInterval newIntervalTolerance = (newInterval / 10) > 1.0 ?: 1.0; + + _lastRetryInterval = newInterval; + + _retryTimer = [NSTimer timerWithTimeInterval:newInterval + target:self + selector:@selector(retryTimerFired:) + userInfo:nil + repeats:NO]; + _retryTimer.tolerance = newIntervalTolerance; + [[NSRunLoop mainRunLoop] addTimer:_retryTimer + forMode:NSDefaultRunLoopMode]; + } // @synchronized(self) + + [self postNotificationOnMainThreadWithName:kGTMSessionFetcherRetryDelayStartedNotification + userInfo:nil + requireAsync:NO]; +} + +- (void)retryTimerFired:(NSTimer *)timer { + [self destroyRetryTimer]; + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _retryCount++; + } // @synchronized(self) + + NSOperationQueue *queue = self.sessionDelegateQueue; + [queue addOperationWithBlock:^{ + [self retryFetch]; + }]; +} + +- (void)destroyRetryTimer { + BOOL shouldNotify = NO; + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (_retryTimer) { + [_retryTimer invalidate]; + _retryTimer = nil; + shouldNotify = YES; + } + } + + if (shouldNotify) { + [self postNotificationOnMainThreadWithName:kGTMSessionFetcherRetryDelayStoppedNotification + userInfo:nil + requireAsync:NO]; + } +} + +- (NSUInteger)retryCount { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _retryCount; + } // @synchronized(self) +} + +- (NSTimeInterval)nextRetryInterval { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + NSTimeInterval interval = [self nextRetryIntervalUnsynchronized]; + return interval; + } // @synchronized(self) +} + +- (NSTimeInterval)nextRetryIntervalUnsynchronized { + GTMSessionCheckSynchronized(self); + + NSInteger statusCode = [self statusCodeUnsynchronized]; + if ((statusCode == 503) && [self hasRetryAfterInterval]) { + NSTimeInterval secs = [self retryAfterInterval]; + return secs; + } + // The next wait interval is the factor (2.0) times the last interval, + // but never less than the minimum interval. + NSTimeInterval secs = _lastRetryInterval * _retryFactor; + if (_maxRetryInterval > 0) { + secs = MIN(secs, _maxRetryInterval); + } + secs = MAX(secs, _minRetryInterval); + + return secs; +} + +- (NSTimer *)retryTimer { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _retryTimer; + } // @synchronized(self) +} + +- (BOOL)isRetryEnabled { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _isRetryEnabled; + } // @synchronized(self) +} + +- (BOOL)isRetryEnabledUnsynchronized { + GTMSessionCheckSynchronized(self); + + return _isRetryEnabled; +} + +- (void)setRetryEnabled:(BOOL)flag { + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (flag && !_isRetryEnabled) { + // We defer initializing these until the user calls setRetryEnabled + // to avoid using the random number generator if it's not needed. + // However, this means min and max intervals for this fetcher are reset + // as a side effect of calling setRetryEnabled. + // + // Make an initial retry interval random between 1.0 and 2.0 seconds + _minRetryInterval = InitialMinRetryInterval(); + _maxRetryInterval = kUnsetMaxRetryInterval; + _retryFactor = 2.0; + _lastRetryInterval = 0.0; + } + _isRetryEnabled = flag; + } // @synchronized(self) +}; + +- (NSTimeInterval)maxRetryInterval { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _maxRetryInterval; + } // @synchronized(self) +} + +- (void)setMaxRetryInterval:(NSTimeInterval)secs { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (secs > 0) { + _maxRetryInterval = secs; + } else { + _maxRetryInterval = kUnsetMaxRetryInterval; + } + } // @synchronized(self) +} + +- (double)minRetryInterval { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _minRetryInterval; + } // @synchronized(self) +} + +- (void)setMinRetryInterval:(NSTimeInterval)secs { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (secs > 0) { + _minRetryInterval = secs; + } else { + // Set min interval to a random value between 1.0 and 2.0 seconds + // so that if multiple clients start retrying at the same time, they'll + // repeat at different times and avoid overloading the server + _minRetryInterval = InitialMinRetryInterval(); + } + } // @synchronized(self) + +} + +#pragma mark iOS System Completion Handlers + +#if TARGET_OS_IPHONE +static NSMutableDictionary *gSystemCompletionHandlers = nil; + +- (GTM_NULLABLE GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler { + return [[self class] systemCompletionHandlerForSessionIdentifier:_sessionIdentifier]; +} + +- (void)setSystemCompletionHandler:(GTM_NULLABLE GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler { + [[self class] setSystemCompletionHandler:systemCompletionHandler + forSessionIdentifier:_sessionIdentifier]; +} + ++ (void)setSystemCompletionHandler:(GTM_NULLABLE GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler + forSessionIdentifier:(NSString *)sessionIdentifier { + if (!sessionIdentifier) { + NSLog(@"%s with nil identifier", __PRETTY_FUNCTION__); + return; + } + + @synchronized([GTMSessionFetcher class]) { + if (gSystemCompletionHandlers == nil && systemCompletionHandler != nil) { + gSystemCompletionHandlers = [[NSMutableDictionary alloc] init]; + } + // Use setValue: to remove the object if completionHandler is nil. + [gSystemCompletionHandlers setValue:systemCompletionHandler + forKey:sessionIdentifier]; + } +} + ++ (GTM_NULLABLE GTMSessionFetcherSystemCompletionHandler)systemCompletionHandlerForSessionIdentifier:(NSString *)sessionIdentifier { + if (!sessionIdentifier) { + return nil; + } + @synchronized([GTMSessionFetcher class]) { + return [gSystemCompletionHandlers objectForKey:sessionIdentifier]; + } +} +#endif // TARGET_OS_IPHONE + +#pragma mark Getters and Setters + +@synthesize downloadResumeData = _downloadResumeData, + configuration = _configuration, + configurationBlock = _configurationBlock, + sessionTask = _sessionTask, + wasCreatedFromBackgroundSession = _wasCreatedFromBackgroundSession, + sessionUserInfo = _sessionUserInfo, + taskDescription = _taskDescription, + taskPriority = _taskPriority, + usingBackgroundSession = _usingBackgroundSession, + canShareSession = _canShareSession, + completionHandler = _completionHandler, + credential = _credential, + proxyCredential = _proxyCredential, + bodyData = _bodyData, + bodyLength = _bodyLength, + service = _service, + serviceHost = _serviceHost, + accumulateDataBlock = _accumulateDataBlock, + receivedProgressBlock = _receivedProgressBlock, + downloadProgressBlock = _downloadProgressBlock, + resumeDataBlock = _resumeDataBlock, + didReceiveResponseBlock = _didReceiveResponseBlock, + challengeBlock = _challengeBlock, + willRedirectBlock = _willRedirectBlock, + sendProgressBlock = _sendProgressBlock, + willCacheURLResponseBlock = _willCacheURLResponseBlock, + retryBlock = _retryBlock, + retryFactor = _retryFactor, + allowedInsecureSchemes = _allowedInsecureSchemes, + allowLocalhostRequest = _allowLocalhostRequest, + allowInvalidServerCertificates = _allowInvalidServerCertificates, + cookieStorage = _cookieStorage, + callbackQueue = _callbackQueue, + initialBeginFetchDate = _initialBeginFetchDate, + testBlock = _testBlock, + testBlockAccumulateDataChunkCount = _testBlockAccumulateDataChunkCount, + comment = _comment, + log = _log; + +#if !STRIP_GTM_FETCH_LOGGING +@synthesize redirectedFromURL = _redirectedFromURL, + logRequestBody = _logRequestBody, + logResponseBody = _logResponseBody, + hasLoggedError = _hasLoggedError; +#endif + +#if GTM_BACKGROUND_TASK_FETCHING +@synthesize backgroundTaskIdentifier = _backgroundTaskIdentifier, + skipBackgroundTask = _skipBackgroundTask; +#endif + +- (GTM_NULLABLE NSURLRequest *)request { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return [_request copy]; + } // @synchronized(self) +} + +- (void)setRequest:(GTM_NULLABLE NSURLRequest *)request { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (![self isFetchingUnsynchronized]) { + _request = [request mutableCopy]; + } else { + GTMSESSION_ASSERT_DEBUG(0, @"request may not be set after beginFetch has been invoked"); + } + } // @synchronized(self) +} + +- (GTM_NULLABLE NSMutableURLRequest *)mutableRequestForTesting { + // Allow tests only to modify the request, useful during retries. + return _request; +} + +// Internal method for updating the request property such as on redirects. +- (void)updateMutableRequest:(GTM_NULLABLE NSMutableURLRequest *)request { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _request = request; + } // @synchronized(self) +} + +// Set a header field value on the request. Header field value changes will not +// affect a fetch after the fetch has begun. +- (void)setRequestValue:(GTM_NULLABLE NSString *)value forHTTPHeaderField:(NSString *)field { + if (![self isFetching]) { + [self updateRequestValue:value forHTTPHeaderField:field]; + } else { + GTMSESSION_ASSERT_DEBUG(0, @"request may not be set after beginFetch has been invoked"); + } +} + +// Internal method for updating request headers. +- (void)updateRequestValue:(GTM_NULLABLE NSString *)value forHTTPHeaderField:(NSString *)field { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + [_request setValue:value forHTTPHeaderField:field]; + } // @synchronized(self) +} + +- (void)setResponse:(GTM_NULLABLE NSURLResponse *)response { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _response = response; + } // @synchronized(self) +} + +- (int64_t)bodyLength { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (_bodyLength == NSURLSessionTransferSizeUnknown) { + if (_bodyData) { + _bodyLength = (int64_t)_bodyData.length; + } else if (_bodyFileURL) { + NSNumber *fileSizeNum = nil; + NSError *fileSizeError = nil; + if ([_bodyFileURL getResourceValue:&fileSizeNum + forKey:NSURLFileSizeKey + error:&fileSizeError]) { + _bodyLength = [fileSizeNum longLongValue]; + } + } + } + return _bodyLength; + } // @synchronized(self) +} + +- (BOOL)useUploadTask { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _useUploadTask; + } // @synchronized(self) +} + +- (void)setUseUploadTask:(BOOL)flag { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (flag != _useUploadTask) { + GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized], + @"useUploadTask should not change after beginFetch has been invoked"); + _useUploadTask = flag; + } + } // @synchronized(self) +} + +- (GTM_NULLABLE NSURL *)bodyFileURL { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _bodyFileURL; + } // @synchronized(self) +} + +- (void)setBodyFileURL:(GTM_NULLABLE NSURL *)fileURL { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + // The comparison here is a trivial optimization and forgiveness for any client that + // repeatedly sets the property, so it just uses pointer comparison rather than isEqual:. + if (fileURL != _bodyFileURL) { + GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized], + @"fileURL should not change after beginFetch has been invoked"); + + _bodyFileURL = fileURL; + } + } // @synchronized(self) +} + +- (GTM_NULLABLE GTMSessionFetcherBodyStreamProvider)bodyStreamProvider { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _bodyStreamProvider; + } // @synchronized(self) +} + +- (void)setBodyStreamProvider:(GTM_NULLABLE GTMSessionFetcherBodyStreamProvider)block { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized], + @"stream provider should not change after beginFetch has been invoked"); + + _bodyStreamProvider = [block copy]; + } // @synchronized(self) +} + +- (GTM_NULLABLE id<GTMFetcherAuthorizationProtocol>)authorizer { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _authorizer; + } // @synchronized(self) +} + +- (void)setAuthorizer:(GTM_NULLABLE id<GTMFetcherAuthorizationProtocol>)authorizer { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (authorizer != _authorizer) { + if ([self isFetchingUnsynchronized]) { + GTMSESSION_ASSERT_DEBUG(0, @"authorizer should not change after beginFetch has been invoked"); + } else { + _authorizer = authorizer; + } + } + } // @synchronized(self) +} + +- (GTM_NULLABLE NSData *)downloadedData { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _downloadedData; + } // @synchronized(self) +} + +- (void)setDownloadedData:(GTM_NULLABLE NSData *)data { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _downloadedData = [data mutableCopy]; + } // @synchronized(self) +} + +- (int64_t)downloadedLength { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _downloadedLength; + } // @synchronized(self) +} + +- (void)setDownloadedLength:(int64_t)length { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _downloadedLength = length; + } // @synchronized(self) +} + +- (dispatch_queue_t GTM_NONNULL_TYPE)callbackQueue { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _callbackQueue; + } // @synchronized(self) +} + +- (void)setCallbackQueue:(dispatch_queue_t GTM_NULLABLE_TYPE)queue { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _callbackQueue = queue ?: dispatch_get_main_queue(); + } // @synchronized(self) +} + +- (GTM_NULLABLE NSURLSession *)session { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _session; + } // @synchronized(self) +} + +- (NSInteger)servicePriority { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _servicePriority; + } // @synchronized(self) +} + +- (void)setServicePriority:(NSInteger)value { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (value != _servicePriority) { + GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized], + @"servicePriority should not change after beginFetch has been invoked"); + + _servicePriority = value; + } + } // @synchronized(self) +} + + +- (void)setSession:(GTM_NULLABLE NSURLSession *)session { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _session = session; + } // @synchronized(self) +} + +- (BOOL)canShareSession { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _canShareSession; + } // @synchronized(self) +} + +- (void)setCanShareSession:(BOOL)flag { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _canShareSession = flag; + } // @synchronized(self) +} + +- (BOOL)useBackgroundSession { + // This reflects if the user requested a background session, not necessarily + // if one was created. That is tracked with _usingBackgroundSession. + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _userRequestedBackgroundSession; + } // @synchronized(self) +} + +- (void)setUseBackgroundSession:(BOOL)flag { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (flag != _userRequestedBackgroundSession) { + GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized], + @"useBackgroundSession should not change after beginFetch has been invoked"); + + _userRequestedBackgroundSession = flag; + } + } // @synchronized(self) +} + +- (BOOL)isUsingBackgroundSession { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _usingBackgroundSession; + } // @synchronized(self) +} + +- (void)setUsingBackgroundSession:(BOOL)flag { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _usingBackgroundSession = flag; + } // @synchronized(self) +} + +- (GTM_NULLABLE NSURLSession *)sessionNeedingInvalidation { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _sessionNeedingInvalidation; + } // @synchronized(self) +} + +- (void)setSessionNeedingInvalidation:(GTM_NULLABLE NSURLSession *)session { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _sessionNeedingInvalidation = session; + } // @synchronized(self) +} + +- (NSOperationQueue * GTM_NONNULL_TYPE)sessionDelegateQueue { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _delegateQueue; + } // @synchronized(self) +} + +- (void)setSessionDelegateQueue:(NSOperationQueue * GTM_NULLABLE_TYPE)queue { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (queue != _delegateQueue) { + if ([self isFetchingUnsynchronized]) { + GTMSESSION_ASSERT_DEBUG(0, @"sessionDelegateQueue should not change after fetch begins"); + } else { + _delegateQueue = queue ?: [NSOperationQueue mainQueue]; + } + } + } // @synchronized(self) +} + +- (BOOL)userStoppedFetching { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _userStoppedFetching; + } // @synchronized(self) +} + +- (GTM_NULLABLE id)userData { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _userData; + } // @synchronized(self) +} + +- (void)setUserData:(GTM_NULLABLE id)theObj { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _userData = theObj; + } // @synchronized(self) +} + +- (GTM_NULLABLE NSURL *)destinationFileURL { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _destinationFileURL; + } // @synchronized(self) +} + +- (void)setDestinationFileURL:(GTM_NULLABLE NSURL *)destinationFileURL { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (((_destinationFileURL == nil) && (destinationFileURL == nil)) || + [_destinationFileURL isEqual:destinationFileURL]) { + return; + } + if (_sessionIdentifier) { + // This is something we don't expect to happen in production. + // However if it ever happen, leave a system log. + NSLog(@"%@: Destination File URL changed from (%@) to (%@) after session identifier has " + @"been created.", + [self class], _destinationFileURL, destinationFileURL); +#if DEBUG + // On both the simulator and devices, the path can change to the download file, but the name + // shouldn't change. Technically, this isn't supported in the fetcher, but the change of + // URL is expected to happen only across development runs through Xcode. + NSString *oldFilename = [_destinationFileURL lastPathComponent]; + NSString *newFilename = [destinationFileURL lastPathComponent]; + #pragma unused(oldFilename) + #pragma unused(newFilename) + GTMSESSION_ASSERT_DEBUG([oldFilename isEqualToString:newFilename], + @"Destination File URL cannot be changed after session identifier has been created"); +#endif + } + _destinationFileURL = destinationFileURL; + } // @synchronized(self) +} + +- (void)setProperties:(GTM_NULLABLE NSDictionary *)dict { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _properties = [dict mutableCopy]; + } // @synchronized(self) +} + +- (GTM_NULLABLE NSDictionary *)properties { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _properties; + } // @synchronized(self) +} + +- (void)setProperty:(GTM_NULLABLE id)obj forKey:(NSString *)key { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (_properties == nil && obj != nil) { + _properties = [[NSMutableDictionary alloc] init]; + } + [_properties setValue:obj forKey:key]; + } // @synchronized(self) +} + +- (GTM_NULLABLE id)propertyForKey:(NSString *)key { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return [_properties objectForKey:key]; + } // @synchronized(self) +} + +- (void)addPropertiesFromDictionary:(NSDictionary *)dict { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (_properties == nil && dict != nil) { + [self setProperties:[dict mutableCopy]]; + } else { + [_properties addEntriesFromDictionary:dict]; + } + } // @synchronized(self) +} + +- (void)setCommentWithFormat:(id)format, ... { +#if !STRIP_GTM_FETCH_LOGGING + NSString *result = format; + if (format) { + va_list argList; + va_start(argList, format); + + result = [[NSString alloc] initWithFormat:format + arguments:argList]; + va_end(argList); + } + [self setComment:result]; +#endif +} + +#if !STRIP_GTM_FETCH_LOGGING +- (NSData *)loggedStreamData { + return _loggedStreamData; +} + +- (void)appendLoggedStreamData:dataToAdd { + if (!_loggedStreamData) { + _loggedStreamData = [NSMutableData data]; + } + [_loggedStreamData appendData:dataToAdd]; +} + +- (void)clearLoggedStreamData { + _loggedStreamData = nil; +} + +- (void)setDeferResponseBodyLogging:(BOOL)deferResponseBodyLogging { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (deferResponseBodyLogging != _deferResponseBodyLogging) { + _deferResponseBodyLogging = deferResponseBodyLogging; + if (!deferResponseBodyLogging && !self.hasLoggedError) { + [_delegateQueue addOperationWithBlock:^{ + [self logNowWithError:nil]; + }]; + } + } + } // @synchronized(self) +} + +- (BOOL)deferResponseBodyLogging { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _deferResponseBodyLogging; + } // @synchronized(self) +} + +#else ++ (void)setLoggingEnabled:(BOOL)flag { +} + ++ (BOOL)isLoggingEnabled { + return NO; +} +#endif // STRIP_GTM_FETCH_LOGGING + +@end + +@implementation GTMSessionFetcher (BackwardsCompatibilityOnly) + +- (void)setCookieStorageMethod:(NSInteger)method { + // For backwards compatibility with the old fetcher, we'll support the old constants. + // + // Clients using the GTMSessionFetcher class should set the cookie storage explicitly + // themselves. + NSHTTPCookieStorage *storage = nil; + switch(method) { + case 0: // kGTMHTTPFetcherCookieStorageMethodStatic + // nil storage will use [[self class] staticCookieStorage] when the fetch begins. + break; + case 1: // kGTMHTTPFetcherCookieStorageMethodFetchHistory + // Do nothing; use whatever was set by the fetcher service. + return; + case 2: // kGTMHTTPFetcherCookieStorageMethodSystemDefault + storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + break; + case 3: // kGTMHTTPFetcherCookieStorageMethodNone + // Create temporary storage for this fetcher only. + storage = [[GTMSessionCookieStorage alloc] init]; + break; + default: + GTMSESSION_ASSERT_DEBUG(0, @"Invalid cookie storage method: %d", (int)method); + } + self.cookieStorage = storage; +} + +@end + +@implementation GTMSessionCookieStorage { + NSMutableArray *_cookies; + NSHTTPCookieAcceptPolicy _policy; +} + +- (id)init { + self = [super init]; + if (self != nil) { + _cookies = [[NSMutableArray alloc] init]; + } + return self; +} + +- (GTM_NULLABLE NSArray *)cookies { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return [_cookies copy]; + } // @synchronized(self) +} + +- (void)setCookie:(NSHTTPCookie *)cookie { + if (!cookie) return; + if (_policy == NSHTTPCookieAcceptPolicyNever) return; + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + [self internalSetCookie:cookie]; + } // @synchronized(self) +} + +// Note: this should only be called from inside a @synchronized(self) block. +- (void)internalSetCookie:(NSHTTPCookie *)newCookie { + GTMSessionCheckSynchronized(self); + + if (_policy == NSHTTPCookieAcceptPolicyNever) return; + + BOOL isValidCookie = (newCookie.name.length > 0 + && newCookie.domain.length > 0 + && newCookie.path.length > 0); + GTMSESSION_ASSERT_DEBUG(isValidCookie, @"invalid cookie: %@", newCookie); + + if (isValidCookie) { + // Remove the cookie if it's currently in the array. + NSHTTPCookie *oldCookie = [self cookieMatchingCookie:newCookie]; + if (oldCookie) { + [_cookies removeObjectIdenticalTo:oldCookie]; + } + + if (![[self class] hasCookieExpired:newCookie]) { + [_cookies addObject:newCookie]; + } + } +} + +// Add all cookies in the new cookie array to the storage, +// replacing stored cookies as appropriate. +// +// Side effect: removes expired cookies from the storage array. +- (void)setCookies:(GTM_NULLABLE NSArray *)newCookies { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + [self removeExpiredCookies]; + + for (NSHTTPCookie *newCookie in newCookies) { + [self internalSetCookie:newCookie]; + } + } // @synchronized(self) +} + +- (void)setCookies:(NSArray *)cookies forURL:(GTM_NULLABLE NSURL *)URL mainDocumentURL:(GTM_NULLABLE NSURL *)mainDocumentURL { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (_policy == NSHTTPCookieAcceptPolicyNever) { + return; + } + + if (_policy == NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain) { + NSString *mainHost = mainDocumentURL.host; + NSString *associatedHost = URL.host; + if (!mainHost || ![associatedHost hasSuffix:mainHost]) { + return; + } + } + } // @synchronized(self) + [self setCookies:cookies]; +} + +- (void)deleteCookie:(NSHTTPCookie *)cookie { + if (!cookie) return; + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + NSHTTPCookie *foundCookie = [self cookieMatchingCookie:cookie]; + if (foundCookie) { + [_cookies removeObjectIdenticalTo:foundCookie]; + } + } // @synchronized(self) +} + +// Retrieve all cookies appropriate for the given URL, considering +// domain, path, cookie name, expiration, security setting. +// Side effect: removed expired cookies from the storage array. +- (GTM_NULLABLE NSArray *)cookiesForURL:(NSURL *)theURL { + NSMutableArray *foundCookies = nil; + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + [self removeExpiredCookies]; + + // We'll prepend "." to the desired domain, since we want the + // actual domain "nytimes.com" to still match the cookie domain + // ".nytimes.com" when we check it below with hasSuffix. + NSString *host = theURL.host.lowercaseString; + NSString *path = theURL.path; + NSString *scheme = [theURL scheme]; + + NSString *requestingDomain = nil; + BOOL isLocalhostRetrieval = NO; + + if (IsLocalhost(host)) { + isLocalhostRetrieval = YES; + } else { + if (host.length > 0) { + requestingDomain = [@"." stringByAppendingString:host]; + } + } + + for (NSHTTPCookie *storedCookie in _cookies) { + NSString *cookieDomain = storedCookie.domain.lowercaseString; + NSString *cookiePath = storedCookie.path; + BOOL cookieIsSecure = [storedCookie isSecure]; + + BOOL isDomainOK; + + if (isLocalhostRetrieval) { + // Prior to 10.5.6, the domain stored into NSHTTPCookies for localhost + // is "localhost.local" + isDomainOK = (IsLocalhost(cookieDomain) + || [cookieDomain isEqual:@"localhost.local"]); + } else { + // Ensure we're matching exact domain names. We prepended a dot to the + // requesting domain, so we can also prepend one here if needed before + // checking if the request contains the cookie domain. + if (![cookieDomain hasPrefix:@"."]) { + cookieDomain = [@"." stringByAppendingString:cookieDomain]; + } + isDomainOK = [requestingDomain hasSuffix:cookieDomain]; + } + + BOOL isPathOK = [cookiePath isEqual:@"/"] || [path hasPrefix:cookiePath]; + BOOL isSecureOK = (!cookieIsSecure + || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame); + + if (isDomainOK && isPathOK && isSecureOK) { + if (foundCookies == nil) { + foundCookies = [NSMutableArray array]; + } + [foundCookies addObject:storedCookie]; + } + } + } // @synchronized(self) + return foundCookies; +} + +// Override methods from the NSHTTPCookieStorage (NSURLSessionTaskAdditions) category. +- (void)storeCookies:(NSArray *)cookies forTask:(NSURLSessionTask *)task { + NSURLRequest *currentRequest = task.currentRequest; + [self setCookies:cookies forURL:currentRequest.URL mainDocumentURL:nil]; +} + +- (void)getCookiesForTask:(NSURLSessionTask *)task + completionHandler:(void (^)(GTM_NSArrayOf(NSHTTPCookie *) *))completionHandler { + if (completionHandler) { + NSURLRequest *currentRequest = task.currentRequest; + NSURL *currentRequestURL = currentRequest.URL; + NSArray *cookies = [self cookiesForURL:currentRequestURL]; + completionHandler(cookies); + } +} + +// Return a cookie from the array with the same name, domain, and path as the +// given cookie, or else return nil if none found. +// +// Both the cookie being tested and all cookies in the storage array should +// be valid (non-nil name, domains, paths). +// +// Note: this should only be called from inside a @synchronized(self) block +- (GTM_NULLABLE NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie { + GTMSessionCheckSynchronized(self); + + NSString *name = cookie.name; + NSString *domain = cookie.domain; + NSString *path = cookie.path; + + GTMSESSION_ASSERT_DEBUG(name && domain && path, + @"Invalid stored cookie (name:%@ domain:%@ path:%@)", name, domain, path); + + for (NSHTTPCookie *storedCookie in _cookies) { + if ([storedCookie.name isEqual:name] + && [storedCookie.domain isEqual:domain] + && [storedCookie.path isEqual:path]) { + return storedCookie; + } + } + return nil; +} + +// Internal routine to remove any expired cookies from the array, excluding +// cookies with nil expirations. +// +// Note: this should only be called from inside a @synchronized(self) block +- (void)removeExpiredCookies { + GTMSessionCheckSynchronized(self); + + // Count backwards since we're deleting items from the array + for (NSInteger idx = (NSInteger)_cookies.count - 1; idx >= 0; idx--) { + NSHTTPCookie *storedCookie = [_cookies objectAtIndex:(NSUInteger)idx]; + if ([[self class] hasCookieExpired:storedCookie]) { + [_cookies removeObjectAtIndex:(NSUInteger)idx]; + } + } +} + ++ (BOOL)hasCookieExpired:(NSHTTPCookie *)cookie { + NSDate *expiresDate = [cookie expiresDate]; + if (expiresDate == nil) { + // Cookies seem to have a Expires property even when the expiresDate method returns nil. + id expiresVal = [[cookie properties] objectForKey:NSHTTPCookieExpires]; + if ([expiresVal isKindOfClass:[NSDate class]]) { + expiresDate = expiresVal; + } + } + BOOL hasExpired = (expiresDate != nil && [expiresDate timeIntervalSinceNow] < 0); + return hasExpired; +} + +- (void)removeAllCookies { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + [_cookies removeAllObjects]; + } // @synchronized(self) +} + +- (NSHTTPCookieAcceptPolicy)cookieAcceptPolicy { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _policy; + } // @synchronized(self) +} + +- (void)setCookieAcceptPolicy:(NSHTTPCookieAcceptPolicy)cookieAcceptPolicy { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _policy = cookieAcceptPolicy; + } // @synchronized(self) +} + +@end + +void GTMSessionFetcherAssertValidSelector(id GTM_NULLABLE_TYPE obj, SEL GTM_NULLABLE_TYPE sel, ...) { + // Verify that the object's selector is implemented with the proper + // number and type of arguments +#if DEBUG + va_list argList; + va_start(argList, sel); + + if (obj && sel) { + // Check that the selector is implemented + if (![obj respondsToSelector:sel]) { + NSLog(@"\"%@\" selector \"%@\" is unimplemented or misnamed", + NSStringFromClass([(id)obj class]), + NSStringFromSelector((SEL)sel)); + NSCAssert(0, @"callback selector unimplemented or misnamed"); + } else { + const char *expectedArgType; + unsigned int argCount = 2; // skip self and _cmd + NSMethodSignature *sig = [obj methodSignatureForSelector:sel]; + + // Check that each expected argument is present and of the correct type + while ((expectedArgType = va_arg(argList, const char*)) != 0) { + + if ([sig numberOfArguments] > argCount) { + const char *foundArgType = [sig getArgumentTypeAtIndex:argCount]; + + if (0 != strncmp(foundArgType, expectedArgType, strlen(expectedArgType))) { + NSLog(@"\"%@\" selector \"%@\" argument %d should be type %s", + NSStringFromClass([(id)obj class]), + NSStringFromSelector((SEL)sel), (argCount - 2), expectedArgType); + NSCAssert(0, @"callback selector argument type mistake"); + } + } + argCount++; + } + + // Check that the proper number of arguments are present in the selector + if (argCount != [sig numberOfArguments]) { + NSLog(@"\"%@\" selector \"%@\" should have %d arguments", + NSStringFromClass([(id)obj class]), + NSStringFromSelector((SEL)sel), (argCount - 2)); + NSCAssert(0, @"callback selector arguments incorrect"); + } + } + } + + va_end(argList); +#endif +} + +NSString *GTMFetcherCleanedUserAgentString(NSString *str) { + // Reference http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html + // and http://www-archive.mozilla.org/build/user-agent-strings.html + + if (str == nil) return @""; + + NSMutableString *result = [NSMutableString stringWithString:str]; + + // Replace spaces and commas with underscores + [result replaceOccurrencesOfString:@" " + withString:@"_" + options:0 + range:NSMakeRange(0, result.length)]; + [result replaceOccurrencesOfString:@"," + withString:@"_" + options:0 + range:NSMakeRange(0, result.length)]; + + // Delete http token separators and remaining whitespace + static NSCharacterSet *charsToDelete = nil; + if (charsToDelete == nil) { + // Make a set of unwanted characters + NSString *const kSeparators = @"()<>@;:\\\"/[]?={}"; + + NSMutableCharacterSet *mutableChars = + [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy]; + [mutableChars addCharactersInString:kSeparators]; + charsToDelete = [mutableChars copy]; // hang on to an immutable copy + } + + while (1) { + NSRange separatorRange = [result rangeOfCharacterFromSet:charsToDelete]; + if (separatorRange.location == NSNotFound) break; + + [result deleteCharactersInRange:separatorRange]; + }; + + return result; +} + +NSString *GTMFetcherSystemVersionString(void) { + static NSString *sSavedSystemString; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // The Xcode 8 SDKs finally cleaned up this mess by providing TARGET_OS_OSX + // and TARGET_OS_IOS, but to build with older SDKs, those don't exist and + // instead one has to rely on TARGET_OS_MAC (which is true for iOS, watchOS, + // and tvOS) and TARGET_OS_IPHONE (which is true for iOS, watchOS, tvOS). So + // one has to order these carefully so you pick off the specific things + // first. + // If the code can ever assume Xcode 8 or higher (even when building for + // older OSes), then + // TARGET_OS_MAC -> TARGET_OS_OSX + // TARGET_OS_IPHONE -> TARGET_OS_IOS + // TARGET_IPHONE_SIMULATOR -> TARGET_OS_SIMULATOR +#if TARGET_OS_WATCH + // watchOS - WKInterfaceDevice + + WKInterfaceDevice *currentDevice = [WKInterfaceDevice currentDevice]; + + NSString *rawModel = [currentDevice model]; + NSString *model = GTMFetcherCleanedUserAgentString(rawModel); + + NSString *systemVersion = [currentDevice systemVersion]; + +#if TARGET_OS_SIMULATOR + NSString *hardwareModel = @"sim"; +#else + NSString *hardwareModel; + struct utsname unameRecord; + if (uname(&unameRecord) == 0) { + NSString *machineName = @(unameRecord.machine); + hardwareModel = GTMFetcherCleanedUserAgentString(machineName); + } + if (hardwareModel.length == 0) { + hardwareModel = @"unk"; + } +#endif + + sSavedSystemString = [[NSString alloc] initWithFormat:@"%@/%@ hw/%@", + model, systemVersion, hardwareModel]; + // Example: Apple_Watch/3.0 hw/Watch1_2 +#elif TARGET_OS_TV || TARGET_OS_IPHONE + // iOS and tvOS have UIDevice, use that. + UIDevice *currentDevice = [UIDevice currentDevice]; + + NSString *rawModel = [currentDevice model]; + NSString *model = GTMFetcherCleanedUserAgentString(rawModel); + + NSString *systemVersion = [currentDevice systemVersion]; + +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_SIMULATOR + NSString *hardwareModel = @"sim"; +#else + NSString *hardwareModel; + struct utsname unameRecord; + if (uname(&unameRecord) == 0) { + NSString *machineName = @(unameRecord.machine); + hardwareModel = GTMFetcherCleanedUserAgentString(machineName); + } + if (hardwareModel.length == 0) { + hardwareModel = @"unk"; + } +#endif + + sSavedSystemString = [[NSString alloc] initWithFormat:@"%@/%@ hw/%@", + model, systemVersion, hardwareModel]; + // Example: iPod_Touch/2.2 hw/iPod1_1 + // Example: Apple_TV/9.2 hw/AppleTV5,3 +#elif TARGET_OS_MAC + // Mac build + NSProcessInfo *procInfo = [NSProcessInfo processInfo]; +#if !defined(MAC_OS_X_VERSION_10_10) + BOOL hasOperatingSystemVersion = NO; +#elif MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10 + BOOL hasOperatingSystemVersion = + [procInfo respondsToSelector:@selector(operatingSystemVersion)]; +#else + BOOL hasOperatingSystemVersion = YES; +#endif + NSString *versString; + if (hasOperatingSystemVersion) { +#if defined(MAC_OS_X_VERSION_10_10) + // A reference to NSOperatingSystemVersion requires the 10.10 SDK. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability" +// Disable unguarded availability warning as we can't use the @availability macro until we require +// all clients to build with Xcode 9 or above. + NSOperatingSystemVersion version = procInfo.operatingSystemVersion; +#pragma clang diagnostic pop + versString = [NSString stringWithFormat:@"%ld.%ld.%ld", + (long)version.majorVersion, (long)version.minorVersion, + (long)version.patchVersion]; +#else +#pragma unused(procInfo) +#endif + } else { + // With Gestalt inexplicably deprecated in 10.8, we're reduced to reading + // the system plist file. + NSString *const kPath = @"/System/Library/CoreServices/SystemVersion.plist"; + NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile:kPath]; + versString = [plist objectForKey:@"ProductVersion"]; + if (versString.length == 0) { + versString = @"10.?.?"; + } + } + + sSavedSystemString = [[NSString alloc] initWithFormat:@"MacOSX/%@", versString]; +#elif defined(_SYS_UTSNAME_H) + // Foundation-only build + struct utsname unameRecord; + uname(&unameRecord); + + sSavedSystemString = [NSString stringWithFormat:@"%s/%s", + unameRecord.sysname, unameRecord.release]; // "Darwin/8.11.1" +#else +#error No branch taken for a default user agent +#endif + }); + return sSavedSystemString; +} + +NSString *GTMFetcherStandardUserAgentString(NSBundle * GTM_NULLABLE_TYPE bundle) { + NSString *result = [NSString stringWithFormat:@"%@ %@", + GTMFetcherApplicationIdentifier(bundle), + GTMFetcherSystemVersionString()]; + return result; +} + +NSString *GTMFetcherApplicationIdentifier(NSBundle * GTM_NULLABLE_TYPE bundle) { + @synchronized([GTMSessionFetcher class]) { + static NSMutableDictionary *sAppIDMap = nil; + + // If there's a bundle ID, use that; otherwise, use the process name + if (bundle == nil) { + bundle = [NSBundle mainBundle]; + } + NSString *bundleID = [bundle bundleIdentifier]; + if (bundleID == nil) { + bundleID = @""; + } + + NSString *identifier = [sAppIDMap objectForKey:bundleID]; + if (identifier) return identifier; + + // Apps may add a string to the info.plist to uniquely identify different builds. + identifier = [bundle objectForInfoDictionaryKey:@"GTMUserAgentID"]; + if (identifier.length == 0) { + if (bundleID.length > 0) { + identifier = bundleID; + } else { + // Fall back on the procname, prefixed by "proc" to flag that it's + // autogenerated and perhaps unreliable + NSString *procName = [[NSProcessInfo processInfo] processName]; + identifier = [NSString stringWithFormat:@"proc_%@", procName]; + } + } + + // Clean up whitespace and special characters + identifier = GTMFetcherCleanedUserAgentString(identifier); + + // If there's a version number, append that + NSString *version = [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + if (version.length == 0) { + version = [bundle objectForInfoDictionaryKey:@"CFBundleVersion"]; + } + + // Clean up whitespace and special characters + version = GTMFetcherCleanedUserAgentString(version); + + // Glue the two together (cleanup done above or else cleanup would strip the + // slash) + if (version.length > 0) { + identifier = [identifier stringByAppendingFormat:@"/%@", version]; + } + + if (sAppIDMap == nil) { + sAppIDMap = [[NSMutableDictionary alloc] init]; + } + [sAppIDMap setObject:identifier forKey:bundleID]; + return identifier; + } +} + +#if DEBUG && (!defined(NS_BLOCK_ASSERTIONS) || GTMSESSION_ASSERT_AS_LOG) +@implementation GTMSessionSyncMonitorInternal { + NSValue *_objectKey; // The synchronize target object. + const char *_functionName; // The function containing the monitored sync block. +} + +- (instancetype)initWithSynchronizationObject:(id)object + allowRecursive:(BOOL)allowRecursive + functionName:(const char *)functionName { + self = [super init]; + if (self) { + Class threadKey = [GTMSessionSyncMonitorInternal class]; + _objectKey = [NSValue valueWithNonretainedObject:object]; + _functionName = functionName; + + NSMutableDictionary *threadDict = [NSThread currentThread].threadDictionary; + NSMutableDictionary *counters = threadDict[threadKey]; + if (counters == nil) { + counters = [NSMutableDictionary dictionary]; + threadDict[(id)threadKey] = counters; + } + NSCountedSet *functionNamesCounter = counters[_objectKey]; + NSUInteger numberOfSyncingFunctions = functionNamesCounter.count; + + if (!allowRecursive) { + BOOL isTopLevelSyncScope = (numberOfSyncingFunctions == 0); + NSArray *stack = [NSThread callStackSymbols]; + GTMSESSION_ASSERT_DEBUG(isTopLevelSyncScope, + @"*** Recursive sync on %@ at %s; previous sync at %@\n%@", + [object class], functionName, functionNamesCounter.allObjects, + [stack subarrayWithRange:NSMakeRange(1, stack.count - 1)]); + } + + if (!functionNamesCounter) { + functionNamesCounter = [NSCountedSet set]; + counters[_objectKey] = functionNamesCounter; + } + [functionNamesCounter addObject:(id _Nonnull)@(functionName)]; + } + return self; +} + +- (void)dealloc { + Class threadKey = [GTMSessionSyncMonitorInternal class]; + + NSMutableDictionary *threadDict = [NSThread currentThread].threadDictionary; + NSMutableDictionary *counters = threadDict[threadKey]; + NSCountedSet *functionNamesCounter = counters[_objectKey]; + NSString *functionNameStr = @(_functionName); + NSUInteger numberOfSyncsByThisFunction = [functionNamesCounter countForObject:functionNameStr]; + NSArray *stack = [NSThread callStackSymbols]; + GTMSESSION_ASSERT_DEBUG(numberOfSyncsByThisFunction > 0, @"Sync not found on %@ at %s\n%@", + [_objectKey.nonretainedObjectValue class], _functionName, + [stack subarrayWithRange:NSMakeRange(1, stack.count - 1)]); + [functionNamesCounter removeObject:functionNameStr]; + if (functionNamesCounter.count == 0) { + [counters removeObjectForKey:_objectKey]; + } +} + ++ (NSArray *)functionsHoldingSynchronizationOnObject:(id)object { + Class threadKey = [GTMSessionSyncMonitorInternal class]; + NSValue *localObjectKey = [NSValue valueWithNonretainedObject:object]; + + NSMutableDictionary *threadDict = [NSThread currentThread].threadDictionary; + NSMutableDictionary *counters = threadDict[threadKey]; + NSCountedSet *functionNamesCounter = counters[localObjectKey]; + return functionNamesCounter.count > 0 ? functionNamesCounter.allObjects : nil; +} +@end +#endif // DEBUG && (!defined(NS_BLOCK_ASSERTIONS) || GTMSESSION_ASSERT_AS_LOG) +GTM_ASSUME_NONNULL_END diff --git a/Pods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.h b/Pods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.h @@ -0,0 +1,112 @@ +/* Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GTMSessionFetcher.h" + +// GTM HTTP Logging +// +// All traffic using GTMSessionFetcher can be easily logged. Call +// +// [GTMSessionFetcher setLoggingEnabled:YES]; +// +// to begin generating log files. +// +// Unless explicitly set by the application using +setLoggingDirectory:, +// logs are put into a default directory, located at: +// * macOS: ~/Desktop/GTMHTTPDebugLogs +// * iOS simulator: ~/GTMHTTPDebugLogs (in application sandbox) +// * iOS device: ~/Documents/GTMHTTPDebugLogs (in application sandbox) +// +// Tip: use the Finder's "Sort By Date" to find the most recent logs. +// +// Each run of an application gets a separate set of log files. An html +// file is generated to simplify browsing the run's http transactions. +// The html file includes javascript links for inline viewing of uploaded +// and downloaded data. +// +// A symlink is created in the logs folder to simplify finding the html file +// for the latest run of the application; the symlink is called +// +// AppName_http_log_newest.html +// +// For better viewing of XML logs, use Camino or Firefox rather than Safari. +// +// Each fetcher may be given a comment to be inserted as a label in the logs, +// such as +// [fetcher setCommentWithFormat:@"retrieve item %@", itemName]; +// +// Projects may define STRIP_GTM_FETCH_LOGGING to remove logging code. + +#if !STRIP_GTM_FETCH_LOGGING + +@interface GTMSessionFetcher (GTMSessionFetcherLogging) + +// Note: on macOS the default logs directory is ~/Desktop/GTMHTTPDebugLogs; on +// iOS simulators it will be the ~/GTMHTTPDebugLogs (in the app sandbox); on +// iOS devices it will be in ~/Documents/GTMHTTPDebugLogs (in the app sandbox). +// These directories will be created as needed, and are excluded from backups +// to iCloud and iTunes. +// +// If a custom directory is set, the directory should already exist. It is +// the application's responsibility to exclude any custom directory from +// backups, if desired. ++ (void)setLoggingDirectory:(NSString *)path; ++ (NSString *)loggingDirectory; + +// client apps can turn logging on and off ++ (void)setLoggingEnabled:(BOOL)isLoggingEnabled; ++ (BOOL)isLoggingEnabled; + +// client apps can turn off logging to a file if they want to only check +// the fetcher's log property ++ (void)setLoggingToFileEnabled:(BOOL)isLoggingToFileEnabled; ++ (BOOL)isLoggingToFileEnabled; + +// client apps can optionally specify process name and date string used in +// log file names ++ (void)setLoggingProcessName:(NSString *)processName; ++ (NSString *)loggingProcessName; + ++ (void)setLoggingDateStamp:(NSString *)dateStamp; ++ (NSString *)loggingDateStamp; + +// client apps can specify the directory for the log for this specific run, +// typically to match the directory used by another fetcher class, like: +// +// [GTMSessionFetcher setLogDirectoryForCurrentRun:[GTMHTTPFetcher logDirectoryForCurrentRun]]; +// +// Setting this overrides the logging directory, process name, and date stamp when writing +// the log file. ++ (void)setLogDirectoryForCurrentRun:(NSString *)logDirectoryForCurrentRun; ++ (NSString *)logDirectoryForCurrentRun; + +// Prunes old log directories that have not been modified since the provided date. +// This will not delete the current run's log directory. ++ (void)deleteLogDirectoriesOlderThanDate:(NSDate *)date; + +// internal; called by fetcher +- (void)logFetchWithError:(NSError *)error; +- (NSInputStream *)loggedInputStreamForInputStream:(NSInputStream *)inputStream; +- (GTMSessionFetcherBodyStreamProvider)loggedStreamProviderForStreamProvider: + (GTMSessionFetcherBodyStreamProvider)streamProvider; + +// internal; accessors useful for viewing logs ++ (NSString *)processNameLogPrefix; ++ (NSString *)symlinkNameSuffix; ++ (NSString *)htmlFileName; + +@end + +#endif // !STRIP_GTM_FETCH_LOGGING diff --git a/Pods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.m b/Pods/GTMSessionFetcher/Source/GTMSessionFetcherLogging.m @@ -0,0 +1,982 @@ +/* Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#include <sys/stat.h> +#include <unistd.h> + +#import "GTMSessionFetcherLogging.h" + +#ifndef STRIP_GTM_FETCH_LOGGING + #error GTMSessionFetcher headers should have defaulted this if it wasn't already defined. +#endif + +#if !STRIP_GTM_FETCH_LOGGING + +// Sensitive credential strings are replaced in logs with _snip_ +// +// Apps that must see the contents of sensitive tokens can set this to 1 +#ifndef SKIP_GTM_FETCH_LOGGING_SNIPPING +#define SKIP_GTM_FETCH_LOGGING_SNIPPING 0 +#endif + +// If GTMReadMonitorInputStream is available, it can be used for +// capturing uploaded streams of data +// +// We locally declare methods of GTMReadMonitorInputStream so we +// do not need to import the header, as some projects may not have it available +#if !GTMSESSION_BUILD_COMBINED_SOURCES +@interface GTMReadMonitorInputStream : NSInputStream + ++ (instancetype)inputStreamWithStream:(NSInputStream *)input; + +@property (assign) id readDelegate; +@property (assign) SEL readSelector; + +@end +#else +@class GTMReadMonitorInputStream; +#endif // !GTMSESSION_BUILD_COMBINED_SOURCES + +@interface GTMSessionFetcher (GTMHTTPFetcherLoggingUtilities) + ++ (NSString *)headersStringForDictionary:(NSDictionary *)dict; ++ (NSString *)snipSubstringOfString:(NSString *)originalStr + betweenStartString:(NSString *)startStr + endString:(NSString *)endStr; +- (void)inputStream:(GTMReadMonitorInputStream *)stream + readIntoBuffer:(void *)buffer + length:(int64_t)length; + +@end + +@implementation GTMSessionFetcher (GTMSessionFetcherLogging) + +// fetchers come and fetchers go, but statics are forever +static BOOL gIsLoggingEnabled = NO; +static BOOL gIsLoggingToFile = YES; +static NSString *gLoggingDirectoryPath = nil; +static NSString *gLogDirectoryForCurrentRun = nil; +static NSString *gLoggingDateStamp = nil; +static NSString *gLoggingProcessName = nil; + ++ (void)setLoggingDirectory:(NSString *)path { + gLoggingDirectoryPath = [path copy]; +} + ++ (NSString *)loggingDirectory { + if (!gLoggingDirectoryPath) { + NSArray *paths = nil; +#if TARGET_IPHONE_SIMULATOR + // default to a directory called GTMHTTPDebugLogs into a sandbox-safe + // directory that a developer can find easily, the application home + paths = @[ NSHomeDirectory() ]; +#elif TARGET_OS_IPHONE + // Neither ~/Desktop nor ~/Home is writable on an actual iOS, watchOS, or tvOS device. + // Put it in ~/Documents. + paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); +#else + // default to a directory called GTMHTTPDebugLogs in the desktop folder + paths = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES); +#endif + + NSString *desktopPath = paths.firstObject; + if (desktopPath) { + NSString *const kGTMLogFolderName = @"GTMHTTPDebugLogs"; + NSString *logsFolderPath = [desktopPath stringByAppendingPathComponent:kGTMLogFolderName]; + + NSFileManager *fileMgr = [NSFileManager defaultManager]; + BOOL isDir; + BOOL doesFolderExist = [fileMgr fileExistsAtPath:logsFolderPath isDirectory:&isDir]; + if (!doesFolderExist) { + // make the directory + doesFolderExist = [fileMgr createDirectoryAtPath:logsFolderPath + withIntermediateDirectories:YES + attributes:nil + error:NULL]; + if (doesFolderExist) { + // The directory has been created. Exclude it from backups. + NSURL *pathURL = [NSURL fileURLWithPath:logsFolderPath isDirectory:YES]; + [pathURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:NULL]; + } + } + + if (doesFolderExist) { + // it's there; store it in the global + gLoggingDirectoryPath = [logsFolderPath copy]; + } + } + } + return gLoggingDirectoryPath; +} + ++ (void)setLogDirectoryForCurrentRun:(NSString *)logDirectoryForCurrentRun { + // Set the path for this run's logs. + gLogDirectoryForCurrentRun = [logDirectoryForCurrentRun copy]; +} + ++ (NSString *)logDirectoryForCurrentRun { + // make a directory for this run's logs, like SyncProto_logs_10-16_01-56-58PM + if (gLogDirectoryForCurrentRun) return gLogDirectoryForCurrentRun; + + NSString *parentDir = [self loggingDirectory]; + NSString *logNamePrefix = [self processNameLogPrefix]; + NSString *dateStamp = [self loggingDateStamp]; + NSString *dirName = [NSString stringWithFormat:@"%@%@", logNamePrefix, dateStamp]; + NSString *logDirectory = [parentDir stringByAppendingPathComponent:dirName]; + + if (gIsLoggingToFile) { + NSFileManager *fileMgr = [NSFileManager defaultManager]; + // Be sure that the first time this app runs, it's not writing to a preexisting folder + static BOOL gShouldReuseFolder = NO; + if (!gShouldReuseFolder) { + gShouldReuseFolder = YES; + NSString *origLogDir = logDirectory; + for (int ctr = 2; ctr < 20; ++ctr) { + if (![fileMgr fileExistsAtPath:logDirectory]) break; + + // append a digit + logDirectory = [origLogDir stringByAppendingFormat:@"_%d", ctr]; + } + } + if (![fileMgr createDirectoryAtPath:logDirectory + withIntermediateDirectories:YES + attributes:nil + error:NULL]) return nil; + } + gLogDirectoryForCurrentRun = logDirectory; + + return gLogDirectoryForCurrentRun; +} + ++ (void)setLoggingEnabled:(BOOL)isLoggingEnabled { + gIsLoggingEnabled = isLoggingEnabled; +} + ++ (BOOL)isLoggingEnabled { + return gIsLoggingEnabled; +} + ++ (void)setLoggingToFileEnabled:(BOOL)isLoggingToFileEnabled { + gIsLoggingToFile = isLoggingToFileEnabled; +} + ++ (BOOL)isLoggingToFileEnabled { + return gIsLoggingToFile; +} + ++ (void)setLoggingProcessName:(NSString *)processName { + gLoggingProcessName = [processName copy]; +} + ++ (NSString *)loggingProcessName { + // get the process name (once per run) replacing spaces with underscores + if (!gLoggingProcessName) { + NSString *procName = [[NSProcessInfo processInfo] processName]; + gLoggingProcessName = [procName stringByReplacingOccurrencesOfString:@" " withString:@"_"]; + } + return gLoggingProcessName; +} + ++ (void)setLoggingDateStamp:(NSString *)dateStamp { + gLoggingDateStamp = [dateStamp copy]; +} + ++ (NSString *)loggingDateStamp { + // We'll pick one date stamp per run, so a run that starts at a later second + // will get a unique results html file + if (!gLoggingDateStamp) { + // produce a string like 08-21_01-41-23PM + + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + [formatter setFormatterBehavior:NSDateFormatterBehavior10_4]; + [formatter setDateFormat:@"M-dd_hh-mm-ssa"]; + + gLoggingDateStamp = [formatter stringFromDate:[NSDate date]]; + } + return gLoggingDateStamp; +} + ++ (NSString *)processNameLogPrefix { + static NSString *gPrefix = nil; + if (!gPrefix) { + NSString *processName = [self loggingProcessName]; + gPrefix = [[NSString alloc] initWithFormat:@"%@_log_", processName]; + } + return gPrefix; +} + ++ (NSString *)symlinkNameSuffix { + return @"_log_newest.html"; +} + ++ (NSString *)htmlFileName { + return @"aperçu_http_log.html"; +} + ++ (void)deleteLogDirectoriesOlderThanDate:(NSDate *)cutoffDate { + NSFileManager *fileMgr = [NSFileManager defaultManager]; + NSURL *parentDir = [NSURL fileURLWithPath:[[self class] loggingDirectory]]; + NSURL *logDirectoryForCurrentRun = + [NSURL fileURLWithPath:[[self class] logDirectoryForCurrentRun]]; + NSError *error; + NSArray *contents = [fileMgr contentsOfDirectoryAtURL:parentDir + includingPropertiesForKeys:@[ NSURLContentModificationDateKey ] + options:0 + error:&error]; + for (NSURL *itemURL in contents) { + if ([itemURL isEqual:logDirectoryForCurrentRun]) continue; + + NSDate *modDate; + if ([itemURL getResourceValue:&modDate + forKey:NSURLContentModificationDateKey + error:&error]) { + if ([modDate compare:cutoffDate] == NSOrderedAscending) { + if (![fileMgr removeItemAtURL:itemURL error:&error]) { + NSLog(@"deleteLogDirectoriesOlderThanDate failed to delete %@: %@", + itemURL.path, error); + } + } + } else { + NSLog(@"deleteLogDirectoriesOlderThanDate failed to get mod date of %@: %@", + itemURL.path, error); + } + } +} + +// formattedStringFromData returns a prettyprinted string for XML or JSON input, +// and a plain string for other input data +- (NSString *)formattedStringFromData:(NSData *)inputData + contentType:(NSString *)contentType + JSON:(NSDictionary **)outJSON { + if (!inputData) return nil; + + // if the content type is JSON and we have the parsing class available, use that + if ([contentType hasPrefix:@"application/json"] && inputData.length > 5) { + // convert from JSON string to NSObjects and back to a formatted string + NSMutableDictionary *obj = [NSJSONSerialization JSONObjectWithData:inputData + options:NSJSONReadingMutableContainers + error:NULL]; + if (obj) { + if (outJSON) *outJSON = obj; + if ([obj isKindOfClass:[NSMutableDictionary class]]) { + // for security and privacy, omit OAuth 2 response access and refresh tokens + if ([obj valueForKey:@"refresh_token"] != nil) { + [obj setObject:@"_snip_" forKey:@"refresh_token"]; + } + if ([obj valueForKey:@"access_token"] != nil) { + [obj setObject:@"_snip_" forKey:@"access_token"]; + } + } + NSData *data = [NSJSONSerialization dataWithJSONObject:obj + options:NSJSONWritingPrettyPrinted + error:NULL]; + if (data) { + NSString *jsonStr = [[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding]; + return jsonStr; + } + } + } + +#if !TARGET_OS_IPHONE && !GTM_SKIP_LOG_XMLFORMAT + // verify that this data starts with the bytes indicating XML + + NSString *const kXMLLintPath = @"/usr/bin/xmllint"; + static BOOL gHasCheckedAvailability = NO; + static BOOL gIsXMLLintAvailable = NO; + + if (!gHasCheckedAvailability) { + gIsXMLLintAvailable = [[NSFileManager defaultManager] fileExistsAtPath:kXMLLintPath]; + gHasCheckedAvailability = YES; + } + if (gIsXMLLintAvailable + && inputData.length > 5 + && strncmp(inputData.bytes, "<?xml", 5) == 0) { + + // call xmllint to format the data + NSTask *task = [[NSTask alloc] init]; + [task setLaunchPath:kXMLLintPath]; + + // use the dash argument to specify stdin as the source file + [task setArguments:@[ @"--format", @"-" ]]; + [task setEnvironment:@{}]; + + NSPipe *inputPipe = [NSPipe pipe]; + NSPipe *outputPipe = [NSPipe pipe]; + [task setStandardInput:inputPipe]; + [task setStandardOutput:outputPipe]; + + [task launch]; + + [[inputPipe fileHandleForWriting] writeData:inputData]; + [[inputPipe fileHandleForWriting] closeFile]; + + // drain the stdout before waiting for the task to exit + NSData *formattedData = [[outputPipe fileHandleForReading] readDataToEndOfFile]; + + [task waitUntilExit]; + + int status = [task terminationStatus]; + if (status == 0 && formattedData.length > 0) { + // success + inputData = formattedData; + } + } +#else + // we can't call external tasks on the iPhone; leave the XML unformatted +#endif + + NSString *dataStr = [[NSString alloc] initWithData:inputData + encoding:NSUTF8StringEncoding]; + return dataStr; +} + +// stringFromStreamData creates a string given the supplied data +// +// If NSString can create a UTF-8 string from the data, then that is returned. +// +// Otherwise, this routine tries to find a MIME boundary at the beginning of the data block, and +// uses that to break up the data into parts. Each part will be used to try to make a UTF-8 string. +// For parts that fail, a replacement string showing the part header and <<n bytes>> is supplied +// in place of the binary data. + +- (NSString *)stringFromStreamData:(NSData *)data + contentType:(NSString *)contentType { + + if (!data) return nil; + + // optimistically, see if the whole data block is UTF-8 + NSString *streamDataStr = [self formattedStringFromData:data + contentType:contentType + JSON:NULL]; + if (streamDataStr) return streamDataStr; + + // Munge a buffer by replacing non-ASCII bytes with underscores, and turn that munged buffer an + // NSString. That gives us a string we can use with NSScanner. + NSMutableData *mutableData = [NSMutableData dataWithData:data]; + unsigned char *bytes = (unsigned char *)mutableData.mutableBytes; + + for (unsigned int idx = 0; idx < mutableData.length; ++idx) { + if (bytes[idx] > 0x7F || bytes[idx] == 0) { + bytes[idx] = '_'; + } + } + + NSString *mungedStr = [[NSString alloc] initWithData:mutableData + encoding:NSUTF8StringEncoding]; + if (mungedStr) { + + // scan for the boundary string + NSString *boundary = nil; + NSScanner *scanner = [NSScanner scannerWithString:mungedStr]; + + if ([scanner scanUpToString:@"\r\n" intoString:&boundary] + && [boundary hasPrefix:@"--"]) { + + // we found a boundary string; use it to divide the string into parts + NSArray *mungedParts = [mungedStr componentsSeparatedByString:boundary]; + + // look at each munged part in the original string, and try to convert those into UTF-8 + NSMutableArray *origParts = [NSMutableArray array]; + NSUInteger offset = 0; + for (NSString *mungedPart in mungedParts) { + NSUInteger partSize = mungedPart.length; + NSData *origPartData = [data subdataWithRange:NSMakeRange(offset, partSize)]; + NSString *origPartStr = [[NSString alloc] initWithData:origPartData + encoding:NSUTF8StringEncoding]; + if (origPartStr) { + // we could make this original part into UTF-8; use the string + [origParts addObject:origPartStr]; + } else { + // this part can't be made into UTF-8; scan the header, if we can + NSString *header = nil; + NSScanner *headerScanner = [NSScanner scannerWithString:mungedPart]; + if (![headerScanner scanUpToString:@"\r\n\r\n" intoString:&header]) { + // we couldn't find a header + header = @""; + } + // make a part string with the header and <<n bytes>> + NSString *binStr = [NSString stringWithFormat:@"\r%@\r<<%lu bytes>>\r", + header, (long)(partSize - header.length)]; + [origParts addObject:binStr]; + } + offset += partSize + boundary.length; + } + // rejoin the original parts + streamDataStr = [origParts componentsJoinedByString:boundary]; + } + } + if (!streamDataStr) { + // give up; just make a string showing the uploaded bytes + streamDataStr = [NSString stringWithFormat:@"<<%u bytes>>", (unsigned int)data.length]; + } + return streamDataStr; +} + +// logFetchWithError is called following a successful or failed fetch attempt +// +// This method does all the work for appending to and creating log files + +- (void)logFetchWithError:(NSError *)error { + if (![[self class] isLoggingEnabled]) return; + NSString *logDirectory = [[self class] logDirectoryForCurrentRun]; + if (!logDirectory) return; + NSString *processName = [[self class] loggingProcessName]; + + // TODO: add Javascript to display response data formatted in hex + + // each response's NSData goes into its own xml or txt file, though all responses for this run of + // the app share a main html file. This counter tracks all fetch responses for this app run. + // + // we'll use a local variable since this routine may be reentered while waiting for XML formatting + // to be completed by an external task + static int gResponseCounter = 0; + int responseCounter = ++gResponseCounter; + + NSURLResponse *response = [self response]; + NSDictionary *responseHeaders = [self responseHeaders]; + NSString *responseDataStr = nil; + NSDictionary *responseJSON = nil; + + // if there's response data, decide what kind of file to put it in based on the first bytes of the + // file or on the mime type supplied by the server + NSString *responseMIMEType = [response MIMEType]; + BOOL isResponseImage = NO; + + // file name for an image data file + NSString *responseDataFileName = nil; + + int64_t responseDataLength = self.downloadedLength; + if (responseDataLength > 0) { + NSData *downloadedData = self.downloadedData; + if (downloadedData == nil + && responseDataLength > 0 + && responseDataLength < 20000 + && self.destinationFileURL) { + // There's a download file that's not too big, so get the data to display from the downloaded + // file. + NSURL *destinationURL = self.destinationFileURL; + downloadedData = [NSData dataWithContentsOfURL:destinationURL]; + } + NSString *responseType = [responseHeaders valueForKey:@"Content-Type"]; + responseDataStr = [self formattedStringFromData:downloadedData + contentType:responseType + JSON:&responseJSON]; + NSString *responseDataExtn = nil; + NSData *dataToWrite = nil; + if (responseDataStr) { + // we were able to make a UTF-8 string from the response data + if ([responseMIMEType isEqual:@"application/atom+xml"] + || [responseMIMEType hasSuffix:@"/xml"]) { + responseDataExtn = @"xml"; + dataToWrite = [responseDataStr dataUsingEncoding:NSUTF8StringEncoding]; + } + } else if ([responseMIMEType isEqual:@"image/jpeg"]) { + responseDataExtn = @"jpg"; + dataToWrite = downloadedData; + isResponseImage = YES; + } else if ([responseMIMEType isEqual:@"image/gif"]) { + responseDataExtn = @"gif"; + dataToWrite = downloadedData; + isResponseImage = YES; + } else if ([responseMIMEType isEqual:@"image/png"]) { + responseDataExtn = @"png"; + dataToWrite = downloadedData; + isResponseImage = YES; + } else { + // add more non-text types here + } + // if we have an extension, save the raw data in a file with that extension + if (responseDataExtn && dataToWrite) { + // generate a response file base name like + NSString *responseBaseName = [NSString stringWithFormat:@"fetch_%d_response", responseCounter]; + responseDataFileName = [responseBaseName stringByAppendingPathExtension:responseDataExtn]; + NSString *responseDataFilePath = [logDirectory stringByAppendingPathComponent:responseDataFileName]; + + NSError *downloadedError = nil; + if (gIsLoggingToFile && ![dataToWrite writeToFile:responseDataFilePath + options:0 + error:&downloadedError]) { + NSLog(@"%@ logging write error:%@ (%@)", [self class], downloadedError, responseDataFileName); + } + } + } + // we'll have one main html file per run of the app + NSString *htmlName = [[self class] htmlFileName]; + NSString *htmlPath =[logDirectory stringByAppendingPathComponent:htmlName]; + + // if the html file exists (from logging previous fetches) we don't need + // to re-write the header or the scripts + NSFileManager *fileMgr = [NSFileManager defaultManager]; + BOOL didFileExist = [fileMgr fileExistsAtPath:htmlPath]; + + NSMutableString* outputHTML = [NSMutableString string]; + + // we need a header to say we'll have UTF-8 text + if (!didFileExist) { + [outputHTML appendFormat:@"<html><head><meta http-equiv=\"content-type\" " + "content=\"text/html; charset=UTF-8\"><title>%@ HTTP fetch log %@</title>", + processName, [[self class] loggingDateStamp]]; + } + // now write the visible html elements + NSString *copyableFileName = [NSString stringWithFormat:@"fetch_%d.txt", responseCounter]; + + NSDate *now = [NSDate date]; + // write the date & time, the comment, and the link to the plain-text (copyable) log + [outputHTML appendFormat:@"<b>%@ &nbsp;&nbsp;&nbsp;&nbsp; ", now]; + + NSString *comment = [self comment]; + if (comment.length > 0) { + [outputHTML appendFormat:@"%@ &nbsp;&nbsp;&nbsp;&nbsp; ", comment]; + } + [outputHTML appendFormat:@"</b><a href='%@'><i>request/response log</i></a><br>", copyableFileName]; + NSTimeInterval elapsed = -self.initialBeginFetchDate.timeIntervalSinceNow; + [outputHTML appendFormat:@"elapsed: %5.3fsec<br>", elapsed]; + + // write the request URL + NSURLRequest *request = self.request; + NSString *requestMethod = request.HTTPMethod; + NSURL *requestURL = request.URL; + + // Save the request URL for next time in case this redirects. + NSString *redirectedFromURLString = [self.redirectedFromURL absoluteString]; + self.redirectedFromURL = [requestURL copy]; + if (redirectedFromURLString) { + [outputHTML appendFormat:@"<FONT COLOR='#990066'><i>redirected from %@</i></FONT><br>", + redirectedFromURLString]; + } + [outputHTML appendFormat:@"<b>request:</b> %@ <code>%@</code><br>\n", requestMethod, requestURL]; + + // write the request headers + NSDictionary *requestHeaders = request.allHTTPHeaderFields; + NSUInteger numberOfRequestHeaders = requestHeaders.count; + if (numberOfRequestHeaders > 0) { + // Indicate if the request is authorized; warn if the request is authorized but non-SSL + NSString *auth = [requestHeaders objectForKey:@"Authorization"]; + NSString *headerDetails = @""; + if (auth) { + BOOL isInsecure = [[requestURL scheme] isEqual:@"http"]; + if (isInsecure) { + // 26A0 = ⚠ + headerDetails = + @"&nbsp;&nbsp;&nbsp;<i>authorized, non-SSL</i><FONT COLOR='#FF00FF'> &#x26A0;</FONT> "; + } else { + headerDetails = @"&nbsp;&nbsp;&nbsp;<i>authorized</i>"; + } + } + NSString *cookiesHdr = [requestHeaders objectForKey:@"Cookie"]; + if (cookiesHdr) { + headerDetails = [headerDetails stringByAppendingString:@"&nbsp;&nbsp;&nbsp;<i>cookies</i>"]; + } + NSString *matchHdr = [requestHeaders objectForKey:@"If-Match"]; + if (matchHdr) { + headerDetails = [headerDetails stringByAppendingString:@"&nbsp;&nbsp;&nbsp;<i>if-match</i>"]; + } + matchHdr = [requestHeaders objectForKey:@"If-None-Match"]; + if (matchHdr) { + headerDetails = [headerDetails stringByAppendingString:@"&nbsp;&nbsp;&nbsp;<i>if-none-match</i>"]; + } + [outputHTML appendFormat:@"&nbsp;&nbsp; headers: %d %@<br>", + (int)numberOfRequestHeaders, headerDetails]; + } else { + [outputHTML appendFormat:@"&nbsp;&nbsp; headers: none<br>"]; + } + // write the request post data + NSData *bodyData = nil; + NSData *loggedStreamData = self.loggedStreamData; + if (loggedStreamData) { + bodyData = loggedStreamData; + } else { + bodyData = self.bodyData; + if (bodyData == nil) { + bodyData = self.request.HTTPBody; + } + } + uint64_t bodyDataLength = bodyData.length; + + if (bodyData.length == 0) { + // If the data is in a body upload file URL, read that in if it's not huge. + NSURL *bodyFileURL = self.bodyFileURL; + if (bodyFileURL) { + NSNumber *fileSizeNum = nil; + NSError *fileSizeError = nil; + if ([bodyFileURL getResourceValue:&fileSizeNum + forKey:NSURLFileSizeKey + error:&fileSizeError]) { + bodyDataLength = [fileSizeNum unsignedLongLongValue]; + if (bodyDataLength > 0 && bodyDataLength < 50000) { + bodyData = [NSData dataWithContentsOfURL:bodyFileURL + options:NSDataReadingUncached + error:&fileSizeError]; + } + } + } + } + NSString *bodyDataStr = nil; + NSString *postType = [requestHeaders valueForKey:@"Content-Type"]; + + if (bodyDataLength > 0) { + [outputHTML appendFormat:@"&nbsp;&nbsp; data: %llu bytes, <code>%@</code><br>\n", + bodyDataLength, postType ? postType : @"(no type)"]; + NSString *logRequestBody = self.logRequestBody; + if (logRequestBody) { + bodyDataStr = [logRequestBody copy]; + self.logRequestBody = nil; + } else { + bodyDataStr = [self stringFromStreamData:bodyData + contentType:postType]; + if (bodyDataStr) { + // remove OAuth 2 client secret and refresh token + bodyDataStr = [[self class] snipSubstringOfString:bodyDataStr + betweenStartString:@"client_secret=" + endString:@"&"]; + bodyDataStr = [[self class] snipSubstringOfString:bodyDataStr + betweenStartString:@"refresh_token=" + endString:@"&"]; + // remove ClientLogin password + bodyDataStr = [[self class] snipSubstringOfString:bodyDataStr + betweenStartString:@"&Passwd=" + endString:@"&"]; + } + } + } else { + // no post data + } + // write the response status, MIME type, URL + NSInteger status = [self statusCode]; + if (response) { + NSString *statusString = @""; + if (status != 0) { + if (status == 200 || status == 201) { + statusString = [NSString stringWithFormat:@"%ld", (long)status]; + + // report any JSON-RPC error + if ([responseJSON isKindOfClass:[NSDictionary class]]) { + NSDictionary *jsonError = [responseJSON objectForKey:@"error"]; + if ([jsonError isKindOfClass:[NSDictionary class]]) { + NSString *jsonCode = [[jsonError valueForKey:@"code"] description]; + NSString *jsonMessage = [jsonError valueForKey:@"message"]; + if (jsonCode || jsonMessage) { + // 2691 = ⚑ + NSString *const jsonErrFmt = + @"&nbsp;&nbsp;&nbsp;<i>JSON error:</i> <FONT COLOR='#FF00FF'>%@ %@ &nbsp;&#x2691;</FONT>"; + statusString = [statusString stringByAppendingFormat:jsonErrFmt, + jsonCode ? jsonCode : @"", + jsonMessage ? jsonMessage : @""]; + } + } + } + } else { + // purple for anything other than 200 or 201 + NSString *flag = status >= 400 ? @"&nbsp;&#x2691;" : @""; // 2691 = ⚑ + NSString *explanation = [NSHTTPURLResponse localizedStringForStatusCode:status]; + NSString *const statusFormat = @"<FONT COLOR='#FF00FF'>%ld %@ %@</FONT>"; + statusString = [NSString stringWithFormat:statusFormat, (long)status, explanation, flag]; + } + } + // show the response URL only if it's different from the request URL + NSString *responseURLStr = @""; + NSURL *responseURL = response.URL; + + if (responseURL && ![responseURL isEqual:request.URL]) { + NSString *const responseURLFormat = + @"<FONT COLOR='#FF00FF'>response URL:</FONT> <code>%@</code><br>\n"; + responseURLStr = [NSString stringWithFormat:responseURLFormat, [responseURL absoluteString]]; + } + [outputHTML appendFormat:@"<b>response:</b>&nbsp;&nbsp;status %@<br>\n%@", + statusString, responseURLStr]; + // Write the response headers + NSUInteger numberOfResponseHeaders = responseHeaders.count; + if (numberOfResponseHeaders > 0) { + // Indicate if the server is setting cookies + NSString *cookiesSet = [responseHeaders valueForKey:@"Set-Cookie"]; + NSString *cookiesStr = + cookiesSet ? @"&nbsp;&nbsp;<FONT COLOR='#990066'><i>sets cookies</i></FONT>" : @""; + // Indicate if the server is redirecting + NSString *location = [responseHeaders valueForKey:@"Location"]; + BOOL isRedirect = status >= 300 && status <= 399 && location != nil; + NSString *redirectsStr = + isRedirect ? @"&nbsp;&nbsp;<FONT COLOR='#990066'><i>redirects</i></FONT>" : @""; + [outputHTML appendFormat:@"&nbsp;&nbsp; headers: %d %@ %@<br>\n", + (int)numberOfResponseHeaders, cookiesStr, redirectsStr]; + } else { + [outputHTML appendString:@"&nbsp;&nbsp; headers: none<br>\n"]; + } + } + // error + if (error) { + [outputHTML appendFormat:@"<b>Error:</b> %@ <br>\n", error.description]; + } + // Write the response data + if (responseDataFileName) { + if (isResponseImage) { + // Make a small inline image that links to the full image file + [outputHTML appendFormat:@"&nbsp;&nbsp; data: %lld bytes, <code>%@</code><br>", + responseDataLength, responseMIMEType]; + NSString *const fmt = + @"<a href=\"%@\"><img src='%@' alt='image' style='border:solid thin;max-height:32'></a>\n"; + [outputHTML appendFormat:fmt, responseDataFileName, responseDataFileName]; + } else { + // The response data was XML; link to the xml file + NSString *const fmt = + @"&nbsp;&nbsp; data: %lld bytes, <code>%@</code>&nbsp;&nbsp;&nbsp;<i><a href=\"%@\">%@</a></i>\n"; + [outputHTML appendFormat:fmt, responseDataLength, responseMIMEType, + responseDataFileName, [responseDataFileName pathExtension]]; + } + } else { + // The response data was not an image; just show the length and MIME type + [outputHTML appendFormat:@"&nbsp;&nbsp; data: %lld bytes, <code>%@</code>\n", + responseDataLength, responseMIMEType ? responseMIMEType : @"(no response type)"]; + } + // Make a single string of the request and response, suitable for copying + // to the clipboard and pasting into a bug report + NSMutableString *copyable = [NSMutableString string]; + if (comment) { + [copyable appendFormat:@"%@\n\n", comment]; + } + [copyable appendFormat:@"%@ elapsed: %5.3fsec\n", now, elapsed]; + if (redirectedFromURLString) { + [copyable appendFormat:@"Redirected from %@\n", redirectedFromURLString]; + } + [copyable appendFormat:@"Request: %@ %@\n", requestMethod, requestURL]; + if (requestHeaders.count > 0) { + [copyable appendFormat:@"Request headers:\n%@\n", + [[self class] headersStringForDictionary:requestHeaders]]; + } + if (bodyDataLength > 0) { + [copyable appendFormat:@"Request body: (%llu bytes)\n", bodyDataLength]; + if (bodyDataStr) { + [copyable appendFormat:@"%@\n", bodyDataStr]; + } + [copyable appendString:@"\n"]; + } + if (response) { + [copyable appendFormat:@"Response: status %d\n", (int) status]; + [copyable appendFormat:@"Response headers:\n%@\n", + [[self class] headersStringForDictionary:responseHeaders]]; + [copyable appendFormat:@"Response body: (%lld bytes)\n", responseDataLength]; + if (responseDataLength > 0) { + NSString *logResponseBody = self.logResponseBody; + if (logResponseBody) { + // The user has provided the response body text. + responseDataStr = [logResponseBody copy]; + self.logResponseBody = nil; + } + if (responseDataStr != nil) { + [copyable appendFormat:@"%@\n", responseDataStr]; + } else { + // Even though it's redundant, we'll put in text to indicate that all the bytes are binary. + if (self.destinationFileURL) { + [copyable appendFormat:@"<<%lld bytes>> to file %@\n", + responseDataLength, self.destinationFileURL.path]; + } else { + [copyable appendFormat:@"<<%lld bytes>>\n", responseDataLength]; + } + } + } + } + if (error) { + [copyable appendFormat:@"Error: %@\n", error]; + } + // Save to log property before adding the separator + self.log = copyable; + + [copyable appendString:@"-----------------------------------------------------------\n"]; + + // Write the copyable version to another file (linked to at the top of the html file, above) + // + // Ideally, something to just copy this to the clipboard like + // <span onCopy='window.event.clipboardData.setData(\"Text\", + // \"copyable stuff\");return false;'>Copy here.</span>" + // would work everywhere, but it only works in Safari as of 8/2010 + if (gIsLoggingToFile) { + NSString *parentDir = [[self class] loggingDirectory]; + NSString *copyablePath = [logDirectory stringByAppendingPathComponent:copyableFileName]; + NSError *copyableError = nil; + if (![copyable writeToFile:copyablePath + atomically:NO + encoding:NSUTF8StringEncoding + error:&copyableError]) { + // Error writing to file + NSLog(@"%@ logging write error:%@ (%@)", [self class], copyableError, copyablePath); + } + [outputHTML appendString:@"<br><hr><p>"]; + + // Append the HTML to the main output file + const char* htmlBytes = outputHTML.UTF8String; + NSOutputStream *stream = [NSOutputStream outputStreamToFileAtPath:htmlPath + append:YES]; + [stream open]; + [stream write:(const uint8_t *) htmlBytes maxLength:strlen(htmlBytes)]; + [stream close]; + + // Make a symlink to the latest html + NSString *const symlinkNameSuffix = [[self class] symlinkNameSuffix]; + NSString *symlinkName = [processName stringByAppendingString:symlinkNameSuffix]; + NSString *symlinkPath = [parentDir stringByAppendingPathComponent:symlinkName]; + + [fileMgr removeItemAtPath:symlinkPath error:NULL]; + [fileMgr createSymbolicLinkAtPath:symlinkPath + withDestinationPath:htmlPath + error:NULL]; +#if TARGET_OS_IPHONE + static BOOL gReportedLoggingPath = NO; + if (!gReportedLoggingPath) { + gReportedLoggingPath = YES; + NSLog(@"GTMSessionFetcher logging to \"%@\"", parentDir); + } +#endif + } +} + +- (NSInputStream *)loggedInputStreamForInputStream:(NSInputStream *)inputStream { + if (!inputStream) return nil; + if (![GTMSessionFetcher isLoggingEnabled]) return inputStream; + + [self clearLoggedStreamData]; // Clear any previous data. + Class monitorClass = NSClassFromString(@"GTMReadMonitorInputStream"); + if (!monitorClass) { + NSString const *str = @"<<Uploaded stream log unavailable without GTMReadMonitorInputStream>>"; + NSData *stringData = [str dataUsingEncoding:NSUTF8StringEncoding]; + [self appendLoggedStreamData:stringData]; + return inputStream; + } + inputStream = [monitorClass inputStreamWithStream:inputStream]; + + GTMReadMonitorInputStream *readMonitorInputStream = (GTMReadMonitorInputStream *)inputStream; + [readMonitorInputStream setReadDelegate:self]; + SEL readSel = @selector(inputStream:readIntoBuffer:length:); + [readMonitorInputStream setReadSelector:readSel]; + + return inputStream; +} + +- (GTMSessionFetcherBodyStreamProvider)loggedStreamProviderForStreamProvider: + (GTMSessionFetcherBodyStreamProvider)streamProvider { + if (!streamProvider) return nil; + if (![GTMSessionFetcher isLoggingEnabled]) return streamProvider; + + [self clearLoggedStreamData]; // Clear any previous data. + Class monitorClass = NSClassFromString(@"GTMReadMonitorInputStream"); + if (!monitorClass) { + NSString const *str = @"<<Uploaded stream log unavailable without GTMReadMonitorInputStream>>"; + NSData *stringData = [str dataUsingEncoding:NSUTF8StringEncoding]; + [self appendLoggedStreamData:stringData]; + return streamProvider; + } + GTMSessionFetcherBodyStreamProvider loggedStreamProvider = + ^(GTMSessionFetcherBodyStreamProviderResponse response) { + streamProvider(^(NSInputStream *bodyStream) { + bodyStream = [self loggedInputStreamForInputStream:bodyStream]; + response(bodyStream); + }); + }; + return loggedStreamProvider; +} + +@end + +@implementation GTMSessionFetcher (GTMSessionFetcherLoggingUtilities) + +- (void)inputStream:(GTMReadMonitorInputStream *)stream + readIntoBuffer:(void *)buffer + length:(int64_t)length { + // append the captured data + NSData *data = [NSData dataWithBytesNoCopy:buffer + length:(NSUInteger)length + freeWhenDone:NO]; + [self appendLoggedStreamData:data]; +} + +#pragma mark Fomatting Utilities + ++ (NSString *)snipSubstringOfString:(NSString *)originalStr + betweenStartString:(NSString *)startStr + endString:(NSString *)endStr { +#if SKIP_GTM_FETCH_LOGGING_SNIPPING + return originalStr; +#else + if (!originalStr) return nil; + + // Find the start string, and replace everything between it + // and the end string (or the end of the original string) with "_snip_" + NSRange startRange = [originalStr rangeOfString:startStr]; + if (startRange.location == NSNotFound) return originalStr; + + // We found the start string + NSUInteger originalLength = originalStr.length; + NSUInteger startOfTarget = NSMaxRange(startRange); + NSRange targetAndRest = NSMakeRange(startOfTarget, originalLength - startOfTarget); + NSRange endRange = [originalStr rangeOfString:endStr + options:0 + range:targetAndRest]; + NSRange replaceRange; + if (endRange.location == NSNotFound) { + // Found no end marker so replace to end of string + replaceRange = targetAndRest; + } else { + // Replace up to the endStr + replaceRange = NSMakeRange(startOfTarget, endRange.location - startOfTarget); + } + NSString *result = [originalStr stringByReplacingCharactersInRange:replaceRange + withString:@"_snip_"]; + return result; +#endif // SKIP_GTM_FETCH_LOGGING_SNIPPING +} + ++ (NSString *)headersStringForDictionary:(NSDictionary *)dict { + // Format the dictionary in http header style, like + // Accept: application/json + // Cache-Control: no-cache + // Content-Type: application/json; charset=utf-8 + // + // Pad the key names, but not beyond 16 chars, since long custom header + // keys just create too much whitespace + NSArray *keys = [dict.allKeys sortedArrayUsingSelector:@selector(compare:)]; + + NSMutableString *str = [NSMutableString string]; + for (NSString *key in keys) { + NSString *value = [dict valueForKey:key]; + if ([key isEqual:@"Authorization"]) { + // Remove OAuth 1 token + value = [[self class] snipSubstringOfString:value + betweenStartString:@"oauth_token=\"" + endString:@"\""]; + + // Remove OAuth 2 bearer token (draft 16, and older form) + value = [[self class] snipSubstringOfString:value + betweenStartString:@"Bearer " + endString:@"\n"]; + value = [[self class] snipSubstringOfString:value + betweenStartString:@"OAuth " + endString:@"\n"]; + + // Remove Google ClientLogin + value = [[self class] snipSubstringOfString:value + betweenStartString:@"GoogleLogin auth=" + endString:@"\n"]; + } + [str appendFormat:@" %@: %@\n", key, value]; + } + return str; +} + +@end + +#endif // !STRIP_GTM_FETCH_LOGGING diff --git a/Pods/GTMSessionFetcher/Source/GTMSessionFetcherService.h b/Pods/GTMSessionFetcher/Source/GTMSessionFetcherService.h @@ -0,0 +1,193 @@ +/* Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// For best performance and convenient usage, fetchers should be generated by a common +// GTMSessionFetcherService instance, like +// +// _fetcherService = [[GTMSessionFetcherService alloc] init]; +// GTMSessionFetcher* myFirstFetcher = [_fetcherService fetcherWithRequest:request1]; +// GTMSessionFetcher* mySecondFetcher = [_fetcherService fetcherWithRequest:request2]; + +#import "GTMSessionFetcher.h" + +GTM_ASSUME_NONNULL_BEGIN + +// Notifications. + +// This notification indicates a reusable session has become invalid. It is intended mainly for the +// service's unit tests. +// +// The notification object is the fetcher service. +// The invalid session is provided via the userInfo kGTMSessionFetcherServiceSessionKey key. +extern NSString *const kGTMSessionFetcherServiceSessionBecameInvalidNotification; +extern NSString *const kGTMSessionFetcherServiceSessionKey; + +@interface GTMSessionFetcherService : NSObject<GTMSessionFetcherServiceProtocol> + +// Queues of delayed and running fetchers. Each dictionary contains arrays +// of GTMSessionFetcher *fetchers, keyed by NSString *host +@property(atomic, strong, readonly, GTM_NULLABLE) GTM_NSDictionaryOf(NSString *, NSArray *) *delayedFetchersByHost; +@property(atomic, strong, readonly, GTM_NULLABLE) GTM_NSDictionaryOf(NSString *, NSArray *) *runningFetchersByHost; + +// A max value of 0 means no fetchers should be delayed. +// The default limit is 10 simultaneous fetchers targeting each host. +// This does not apply to fetchers whose useBackgroundSession property is YES. Since services are +// not resurrected on an app relaunch, delayed fetchers would effectively be abandoned. +@property(atomic, assign) NSUInteger maxRunningFetchersPerHost; + +// Properties to be applied to each fetcher; see GTMSessionFetcher.h for descriptions +@property(atomic, strong, GTM_NULLABLE) NSURLSessionConfiguration *configuration; +@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherConfigurationBlock configurationBlock; +@property(atomic, strong, GTM_NULLABLE) NSHTTPCookieStorage *cookieStorage; +@property(atomic, strong, GTM_NULL_RESETTABLE) dispatch_queue_t callbackQueue; +@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherChallengeBlock challengeBlock; +@property(atomic, strong, GTM_NULLABLE) NSURLCredential *credential; +@property(atomic, strong) NSURLCredential *proxyCredential; +@property(atomic, copy, GTM_NULLABLE) GTM_NSArrayOf(NSString *) *allowedInsecureSchemes; +@property(atomic, assign) BOOL allowLocalhostRequest; +@property(atomic, assign) BOOL allowInvalidServerCertificates; +@property(atomic, assign, getter=isRetryEnabled) BOOL retryEnabled; +@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherRetryBlock retryBlock; +@property(atomic, assign) NSTimeInterval maxRetryInterval; +@property(atomic, assign) NSTimeInterval minRetryInterval; +@property(atomic, copy, GTM_NULLABLE) GTM_NSDictionaryOf(NSString *, id) *properties; + +#if GTM_BACKGROUND_TASK_FETCHING +@property(atomic, assign) BOOL skipBackgroundTask; +#endif + +// A default useragent of GTMFetcherStandardUserAgentString(nil) will be given to each fetcher +// created by this service unless the request already has a user-agent header set. +// This default will be added starting with builds with the SDKs for OS X 10.11 and iOS 9. +// +// To use the configuration's default user agent, set this property to nil. +@property(atomic, copy, GTM_NULLABLE) NSString *userAgent; + +// The authorizer to attach to the created fetchers. If a specific fetcher should +// not authorize its requests, the fetcher's authorizer property may be set to nil +// before the fetch begins. +@property(atomic, strong, GTM_NULLABLE) id<GTMFetcherAuthorizationProtocol> authorizer; + +// Delegate queue used by the session when calling back to the fetcher. The default +// is the main queue. Changing this does not affect the queue used to call back to the +// application; that is specified by the callbackQueue property above. +@property(atomic, strong, GTM_NULL_RESETTABLE) NSOperationQueue *sessionDelegateQueue; + +// When enabled, indicates the same session should be used by subsequent fetchers. +// +// This is enabled by default. +@property(atomic, assign) BOOL reuseSession; + +// Sets the delay until an unused session is invalidated. +// The default interval is 60 seconds. +// +// If the interval is set to 0, then any reused session is not invalidated except by +// explicitly invoking -resetSession. Be aware that setting the interval to 0 thus +// causes the session's delegate to be retained until the session is explicitly reset. +@property(atomic, assign) NSTimeInterval unusedSessionTimeout; + +// If shouldReuseSession is enabled, this will force creation of a new session when future +// fetchers begin. +- (void)resetSession; + +// Create a fetcher +// +// These methods will return a fetcher. If successfully created, the connection +// will hold a strong reference to it for the life of the connection as well. +// So the caller doesn't have to hold onto the fetcher explicitly unless they +// want to be able to monitor or cancel it. +- (GTMSessionFetcher *)fetcherWithRequest:(NSURLRequest *)request; +- (GTMSessionFetcher *)fetcherWithURL:(NSURL *)requestURL; +- (GTMSessionFetcher *)fetcherWithURLString:(NSString *)requestURLString; + +// Common method for fetcher creation. +// +// -fetcherWithRequest:fetcherClass: may be overridden to customize creation of +// fetchers. This is the ONLY method in the GTMSessionFetcher library intended to +// be overridden. +- (id)fetcherWithRequest:(NSURLRequest *)request + fetcherClass:(Class)fetcherClass; + +- (BOOL)isDelayingFetcher:(GTMSessionFetcher *)fetcher; + +- (NSUInteger)numberOfFetchers; // running + delayed fetchers +- (NSUInteger)numberOfRunningFetchers; +- (NSUInteger)numberOfDelayedFetchers; + +// Return a list of all running or delayed fetchers. This includes fetchers created +// by the service which have been started and have not yet stopped. +// +// Returns an array of fetcher objects, or nil if none. +- (GTM_NULLABLE GTM_NSArrayOf(GTMSessionFetcher *) *)issuedFetchers; + +// Search for running or delayed fetchers with the specified URL. +// +// Returns an array of fetcher objects found, or nil if none found. +- (GTM_NULLABLE GTM_NSArrayOf(GTMSessionFetcher *) *)issuedFetchersWithRequestURL:(NSURL *)requestURL; + +- (void)stopAllFetchers; + +// Methods for use by the fetcher class only. +- (GTM_NULLABLE NSURLSession *)session; +- (GTM_NULLABLE NSURLSession *)sessionForFetcherCreation; +- (GTM_NULLABLE id<NSURLSessionDelegate>)sessionDelegate; +- (GTM_NULLABLE NSDate *)stoppedAllFetchersDate; + +// The testBlock can inspect its fetcher parameter's request property to +// determine which fetcher is being faked. +@property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherTestBlock testBlock; + +@end + +@interface GTMSessionFetcherService (TestingSupport) + +// Convenience methods to create a fetcher service for testing. +// +// Fetchers generated by this mock fetcher service will not perform any +// network operation, but will invoke callbacks and provide the supplied data +// or error to the completion handler. +// +// You can make more customized mocks by setting the test block property of the service +// or fetcher; the test block can inspect the fetcher's request or other properties. +// +// See the description of the testBlock property below. ++ (instancetype)mockFetcherServiceWithFakedData:(GTM_NULLABLE NSData *)fakedDataOrNil + fakedError:(GTM_NULLABLE NSError *)fakedErrorOrNil; ++ (instancetype)mockFetcherServiceWithFakedData:(GTM_NULLABLE NSData *)fakedDataOrNil + fakedResponse:(NSHTTPURLResponse *)fakedResponse + fakedError:(GTM_NULLABLE NSError *)fakedErrorOrNil; + +// Spin the run loop and discard events (or, if not on the main thread, just sleep the thread) +// until all running and delayed fetchers have completed. +// +// This is only for use in testing or in tools without a user interface. +// +// Synchronous fetches should never be done by shipping apps; they are +// sufficient reason for rejection from the app store. +// +// Returns NO if timed out. +- (BOOL)waitForCompletionOfAllFetchersWithTimeout:(NSTimeInterval)timeoutInSeconds; + +@end + +@interface GTMSessionFetcherService (BackwardsCompatibilityOnly) + +// Clients using GTMSessionFetcher should set the cookie storage explicitly themselves. +// This method is just for compatibility with the old fetcher. +@property(atomic, assign) NSInteger cookieStorageMethod; + +@end + +GTM_ASSUME_NONNULL_END diff --git a/Pods/GTMSessionFetcher/Source/GTMSessionFetcherService.m b/Pods/GTMSessionFetcher/Source/GTMSessionFetcherService.m @@ -0,0 +1,1365 @@ +/* Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "GTMSessionFetcherService.h" + +NSString *const kGTMSessionFetcherServiceSessionBecameInvalidNotification + = @"kGTMSessionFetcherServiceSessionBecameInvalidNotification"; +NSString *const kGTMSessionFetcherServiceSessionKey + = @"kGTMSessionFetcherServiceSessionKey"; + +#if !GTMSESSION_BUILD_COMBINED_SOURCES +@interface GTMSessionFetcher (ServiceMethods) +- (BOOL)beginFetchMayDelay:(BOOL)mayDelay + mayAuthorize:(BOOL)mayAuthorize; +@end +#endif // !GTMSESSION_BUILD_COMBINED_SOURCES + +@interface GTMSessionFetcherService () + +@property(atomic, strong, readwrite) NSDictionary *delayedFetchersByHost; +@property(atomic, strong, readwrite) NSDictionary *runningFetchersByHost; + +@end + +// Since NSURLSession doesn't support a separate delegate per task (!), instances of this +// class serve as a session delegate trampoline. +// +// This class maps a session's tasks to fetchers, and resends delegate messages to the task's +// fetcher. +@interface GTMSessionFetcherSessionDelegateDispatcher : NSObject<NSURLSessionDelegate> + +// The session for the tasks in this dispatcher's task-to-fetcher map. +@property(atomic) NSURLSession *session; + +// The timer interval for invalidating a session that has no active tasks. +@property(atomic) NSTimeInterval discardInterval; + +// The current discard timer. +@property(atomic, readonly) NSTimer *discardTimer; + + +- (instancetype)initWithParentService:(GTMSessionFetcherService *)parentService + sessionDiscardInterval:(NSTimeInterval)discardInterval; + +- (void)setFetcher:(GTMSessionFetcher *)fetcher + forTask:(NSURLSessionTask *)task; +- (void)removeFetcher:(GTMSessionFetcher *)fetcher; + +// Before using a session, tells the delegate dispatcher to stop the discard timer. +- (void)startSessionUsage; + +// When abandoning a delegate dispatcher, we want to avoid the session retaining +// the delegate after tasks complete. +- (void)abandon; + +@end + + +@implementation GTMSessionFetcherService { + NSMutableDictionary *_delayedFetchersByHost; + NSMutableDictionary *_runningFetchersByHost; + NSUInteger _maxRunningFetchersPerHost; + + // When this ivar is nil, the service will not reuse sessions. + GTMSessionFetcherSessionDelegateDispatcher *_delegateDispatcher; + + // Fetchers will wait on this if another fetcher is creating the shared NSURLSession. + dispatch_semaphore_t _sessionCreationSemaphore; + + dispatch_queue_t _callbackQueue; + NSOperationQueue *_delegateQueue; + NSHTTPCookieStorage *_cookieStorage; + NSString *_userAgent; + NSTimeInterval _timeout; + + NSURLCredential *_credential; // Username & password. + NSURLCredential *_proxyCredential; // Credential supplied to proxy servers. + + NSInteger _cookieStorageMethod; + + id<GTMFetcherAuthorizationProtocol> _authorizer; + + // For waitForCompletionOfAllFetchersWithTimeout: we need to wait on stopped fetchers since + // they've not yet finished invoking their queued callbacks. This array is nil except when + // waiting on fetchers. + NSMutableArray *_stoppedFetchersToWaitFor; + + // For fetchers that enqueued their callbacks before stopAllFetchers was called on the service, + // set a barrier so the callbacks know to bail out. + NSDate *_stoppedAllFetchersDate; +} + +@synthesize maxRunningFetchersPerHost = _maxRunningFetchersPerHost, + configuration = _configuration, + configurationBlock = _configurationBlock, + cookieStorage = _cookieStorage, + userAgent = _userAgent, + challengeBlock = _challengeBlock, + credential = _credential, + proxyCredential = _proxyCredential, + allowedInsecureSchemes = _allowedInsecureSchemes, + allowLocalhostRequest = _allowLocalhostRequest, + allowInvalidServerCertificates = _allowInvalidServerCertificates, + retryEnabled = _retryEnabled, + retryBlock = _retryBlock, + maxRetryInterval = _maxRetryInterval, + minRetryInterval = _minRetryInterval, + properties = _properties, + unusedSessionTimeout = _unusedSessionTimeout, + testBlock = _testBlock; + +#if GTM_BACKGROUND_TASK_FETCHING +@synthesize skipBackgroundTask = _skipBackgroundTask; +#endif + +- (instancetype)init { + self = [super init]; + if (self) { + _delayedFetchersByHost = [[NSMutableDictionary alloc] init]; + _runningFetchersByHost = [[NSMutableDictionary alloc] init]; + _maxRunningFetchersPerHost = 10; + _cookieStorageMethod = -1; + _unusedSessionTimeout = 60.0; + _delegateDispatcher = + [[GTMSessionFetcherSessionDelegateDispatcher alloc] initWithParentService:self + sessionDiscardInterval:_unusedSessionTimeout]; + _callbackQueue = dispatch_get_main_queue(); + + _delegateQueue = [[NSOperationQueue alloc] init]; + _delegateQueue.maxConcurrentOperationCount = 1; + _delegateQueue.name = @"com.google.GTMSessionFetcher.NSURLSessionDelegateQueue"; + + _sessionCreationSemaphore = dispatch_semaphore_create(1); + + // Starting with the SDKs for OS X 10.11/iOS 9, the service has a default useragent. + // Apps can remove this and get the default system "CFNetwork" useragent by setting the + // fetcher service's userAgent property to nil. +#if (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_11) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_11) \ + || (TARGET_OS_IPHONE && defined(__IPHONE_9_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0) + _userAgent = GTMFetcherStandardUserAgentString(nil); +#endif + } + return self; +} + +- (void)dealloc { + [self detachAuthorizer]; + [_delegateDispatcher abandon]; +} + +#pragma mark Generate a new fetcher + +// Clients may override this method. Clients should not override any other library methods. +- (id)fetcherWithRequest:(NSURLRequest *)request + fetcherClass:(Class)fetcherClass { + GTMSessionFetcher *fetcher = [[fetcherClass alloc] initWithRequest:request + configuration:self.configuration]; + fetcher.callbackQueue = self.callbackQueue; + fetcher.sessionDelegateQueue = self.sessionDelegateQueue; + fetcher.challengeBlock = self.challengeBlock; + fetcher.credential = self.credential; + fetcher.proxyCredential = self.proxyCredential; + fetcher.authorizer = self.authorizer; + fetcher.cookieStorage = self.cookieStorage; + fetcher.allowedInsecureSchemes = self.allowedInsecureSchemes; + fetcher.allowLocalhostRequest = self.allowLocalhostRequest; + fetcher.allowInvalidServerCertificates = self.allowInvalidServerCertificates; + fetcher.configurationBlock = self.configurationBlock; + fetcher.retryEnabled = self.retryEnabled; + fetcher.retryBlock = self.retryBlock; + fetcher.maxRetryInterval = self.maxRetryInterval; + fetcher.minRetryInterval = self.minRetryInterval; + fetcher.properties = self.properties; + fetcher.service = self; + if (self.cookieStorageMethod >= 0) { + [fetcher setCookieStorageMethod:self.cookieStorageMethod]; + } + +#if GTM_BACKGROUND_TASK_FETCHING + fetcher.skipBackgroundTask = self.skipBackgroundTask; +#endif + + NSString *userAgent = self.userAgent; + if (userAgent.length > 0 + && [request valueForHTTPHeaderField:@"User-Agent"] == nil) { + [fetcher setRequestValue:userAgent + forHTTPHeaderField:@"User-Agent"]; + } + fetcher.testBlock = self.testBlock; + + return fetcher; +} + +- (GTMSessionFetcher *)fetcherWithRequest:(NSURLRequest *)request { + return [self fetcherWithRequest:request + fetcherClass:[GTMSessionFetcher class]]; +} + +- (GTMSessionFetcher *)fetcherWithURL:(NSURL *)requestURL { + return [self fetcherWithRequest:[NSURLRequest requestWithURL:requestURL]]; +} + +- (GTMSessionFetcher *)fetcherWithURLString:(NSString *)requestURLString { + NSURL *url = [NSURL URLWithString:requestURLString]; + return [self fetcherWithURL:url]; +} + +// Returns a session for the fetcher's host, or nil. +- (NSURLSession *)session { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + NSURLSession *session = _delegateDispatcher.session; + return session; + } +} + +// Returns a session for the fetcher's host, or nil. For shared sessions, this +// waits on a semaphore, blocking other fetchers while the caller creates the +// session if needed. +- (NSURLSession *)sessionForFetcherCreation { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + if (!_delegateDispatcher) { + // This fetcher is creating a non-shared session, so skip the semaphore usage. + return nil; + } + } + + // Wait if another fetcher is currently creating a session; avoid waiting + // inside the @synchronized block, as that can deadlock. + dispatch_semaphore_wait(_sessionCreationSemaphore, DISPATCH_TIME_FOREVER); + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + // Before getting the NSURLSession for task creation, it is + // important to invalidate and nil out the session discard timer; otherwise + // the session can be invalidated between when it is returned to the + // fetcher, and when the fetcher attempts to create its NSURLSessionTask. + [_delegateDispatcher startSessionUsage]; + + NSURLSession *session = _delegateDispatcher.session; + if (session) { + // The calling fetcher will receive a preexisting session, so + // we can allow other fetchers to create a session. + dispatch_semaphore_signal(_sessionCreationSemaphore); + } else { + // No existing session was obtained, so the calling fetcher will create the session; + // it *must* invoke fetcherDidCreateSession: to signal the dispatcher's semaphore after + // the session has been created (or fails to be created) to avoid a hang. + } + return session; + } +} + +- (id<NSURLSessionDelegate>)sessionDelegate { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _delegateDispatcher; + } +} + +#pragma mark Queue Management + +- (void)addRunningFetcher:(GTMSessionFetcher *)fetcher + forHost:(NSString *)host { + // Add to the array of running fetchers for this host, creating the array if needed. + NSMutableArray *runningForHost = [_runningFetchersByHost objectForKey:host]; + if (runningForHost == nil) { + runningForHost = [NSMutableArray arrayWithObject:fetcher]; + [_runningFetchersByHost setObject:runningForHost forKey:host]; + } else { + [runningForHost addObject:fetcher]; + } +} + +- (void)addDelayedFetcher:(GTMSessionFetcher *)fetcher + forHost:(NSString *)host { + // Add to the array of delayed fetchers for this host, creating the array if needed. + NSMutableArray *delayedForHost = [_delayedFetchersByHost objectForKey:host]; + if (delayedForHost == nil) { + delayedForHost = [NSMutableArray arrayWithObject:fetcher]; + [_delayedFetchersByHost setObject:delayedForHost forKey:host]; + } else { + [delayedForHost addObject:fetcher]; + } +} + +- (BOOL)isDelayingFetcher:(GTMSessionFetcher *)fetcher { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + NSString *host = fetcher.request.URL.host; + if (host == nil) { + return NO; + } + NSArray *delayedForHost = [_delayedFetchersByHost objectForKey:host]; + NSUInteger idx = [delayedForHost indexOfObjectIdenticalTo:fetcher]; + BOOL isDelayed = (delayedForHost != nil) && (idx != NSNotFound); + return isDelayed; + } +} + +- (BOOL)fetcherShouldBeginFetching:(GTMSessionFetcher *)fetcher { + // Entry point from the fetcher + NSURL *requestURL = fetcher.request.URL; + NSString *host = requestURL.host; + + // Addresses "file:///path" case where localhost is the implicit host. + if (host.length == 0 && [requestURL isFileURL]) { + host = @"localhost"; + } + + if (host.length == 0) { + // Data URIs legitimately have no host, reject other hostless URLs. + GTMSESSION_ASSERT_DEBUG([[requestURL scheme] isEqual:@"data"], @"%@ lacks host", fetcher); + return YES; + } + + BOOL shouldBeginResult; + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + NSMutableArray *runningForHost = [_runningFetchersByHost objectForKey:host]; + if (runningForHost != nil + && [runningForHost indexOfObjectIdenticalTo:fetcher] != NSNotFound) { + GTMSESSION_ASSERT_DEBUG(NO, @"%@ was already running", fetcher); + return YES; + } + + BOOL shouldRunNow = (fetcher.usingBackgroundSession + || _maxRunningFetchersPerHost == 0 + || _maxRunningFetchersPerHost > + [[self class] numberOfNonBackgroundSessionFetchers:runningForHost]); + if (shouldRunNow) { + [self addRunningFetcher:fetcher forHost:host]; + shouldBeginResult = YES; + } else { + [self addDelayedFetcher:fetcher forHost:host]; + shouldBeginResult = NO; + } + } // @synchronized(self) + + // We'll save the host that serves as the key for this fetcher's array + // to avoid any chance of the underlying request changing, stranding + // the fetcher in the wrong array + fetcher.serviceHost = host; + + return shouldBeginResult; +} + +- (void)startFetcher:(GTMSessionFetcher *)fetcher { + [fetcher beginFetchMayDelay:NO + mayAuthorize:YES]; +} + +// Internal utility. Returns a fetcher's delegate if it's a dispatcher, or nil if the fetcher +// is its own delegate and has no dispatcher. +- (GTMSessionFetcherSessionDelegateDispatcher *)delegateDispatcherForFetcher:(GTMSessionFetcher *)fetcher { + GTMSessionCheckNotSynchronized(self); + + NSURLSession *fetcherSession = fetcher.session; + if (fetcherSession) { + id<NSURLSessionDelegate> fetcherDelegate = fetcherSession.delegate; + BOOL hasDispatcher = (fetcherDelegate != nil && fetcherDelegate != fetcher); + if (hasDispatcher) { + GTMSESSION_ASSERT_DEBUG([fetcherDelegate isKindOfClass:[GTMSessionFetcherSessionDelegateDispatcher class]], + @"Fetcher delegate class: %@", [fetcherDelegate class]); + return (GTMSessionFetcherSessionDelegateDispatcher *)fetcherDelegate; + } + } + return nil; +} + +- (void)fetcherDidCreateSession:(GTMSessionFetcher *)fetcher { + if (fetcher.canShareSession) { + NSURLSession *fetcherSession = fetcher.session; + GTMSESSION_ASSERT_DEBUG(fetcherSession != nil, @"Fetcher missing its session: %@", fetcher); + + GTMSessionFetcherSessionDelegateDispatcher *delegateDispatcher = + [self delegateDispatcherForFetcher:fetcher]; + if (delegateDispatcher) { + GTMSESSION_ASSERT_DEBUG(delegateDispatcher.session == nil, + @"Fetcher made an extra session: %@", fetcher); + + // Save this fetcher's session. + delegateDispatcher.session = fetcherSession; + + // Allow other fetchers to request this session now. + dispatch_semaphore_signal(_sessionCreationSemaphore); + } + } +} + +- (void)fetcherDidBeginFetching:(GTMSessionFetcher *)fetcher { + // If this fetcher has a separate delegate with a shared session, then + // this fetcher should be added to the delegate's map of tasks to fetchers. + GTMSessionFetcherSessionDelegateDispatcher *delegateDispatcher = + [self delegateDispatcherForFetcher:fetcher]; + if (delegateDispatcher) { + GTMSESSION_ASSERT_DEBUG(fetcher.canShareSession, + @"Inappropriate shared session: %@", fetcher); + + // There should already be a session, from this or a previous fetcher. + // + // Sanity check that the fetcher's session is the delegate's shared session. + NSURLSession *sharedSession = delegateDispatcher.session; + NSURLSession *fetcherSession = fetcher.session; + GTMSESSION_ASSERT_DEBUG(sharedSession != nil, @"Missing delegate session: %@", fetcher); + GTMSESSION_ASSERT_DEBUG(fetcherSession == sharedSession, + @"Inconsistent session: %@ %@ (shared: %@)", + fetcher, fetcherSession, sharedSession); + + if (sharedSession != nil && fetcherSession == sharedSession) { + NSURLSessionTask *task = fetcher.sessionTask; + GTMSESSION_ASSERT_DEBUG(task != nil, @"Missing session task: %@", fetcher); + + if (task) { + [delegateDispatcher setFetcher:fetcher + forTask:task]; + } + } + } +} + +- (void)stopFetcher:(GTMSessionFetcher *)fetcher { + [fetcher stopFetching]; +} + +- (void)fetcherDidStop:(GTMSessionFetcher *)fetcher { + // Entry point from the fetcher + NSString *host = fetcher.serviceHost; + if (!host) { + // fetcher has been stopped previously + return; + } + + // This removeFetcher: invocation is a fallback; typically, fetchers are removed from the task + // map when the task completes. + GTMSessionFetcherSessionDelegateDispatcher *delegateDispatcher = + [self delegateDispatcherForFetcher:fetcher]; + [delegateDispatcher removeFetcher:fetcher]; + + NSMutableArray *fetchersToStart; + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + // If a test is waiting for all fetchers to stop, it needs to wait for this one + // to invoke its callbacks on the callback queue. + [_stoppedFetchersToWaitFor addObject:fetcher]; + + NSMutableArray *runningForHost = [_runningFetchersByHost objectForKey:host]; + [runningForHost removeObject:fetcher]; + + NSMutableArray *delayedForHost = [_delayedFetchersByHost objectForKey:host]; + [delayedForHost removeObject:fetcher]; + + while (delayedForHost.count > 0 + && [[self class] numberOfNonBackgroundSessionFetchers:runningForHost] + < _maxRunningFetchersPerHost) { + // Start another delayed fetcher running, scanning for the minimum + // priority value, defaulting to FIFO for equal priorities + GTMSessionFetcher *nextFetcher = nil; + for (GTMSessionFetcher *delayedFetcher in delayedForHost) { + if (nextFetcher == nil + || delayedFetcher.servicePriority < nextFetcher.servicePriority) { + nextFetcher = delayedFetcher; + } + } + + if (nextFetcher) { + [self addRunningFetcher:nextFetcher forHost:host]; + runningForHost = [_runningFetchersByHost objectForKey:host]; + + [delayedForHost removeObjectIdenticalTo:nextFetcher]; + + if (!fetchersToStart) { + fetchersToStart = [NSMutableArray array]; + } + [fetchersToStart addObject:nextFetcher]; + } + } + + if (runningForHost.count == 0) { + // None left; remove the empty array + [_runningFetchersByHost removeObjectForKey:host]; + } + + if (delayedForHost.count == 0) { + [_delayedFetchersByHost removeObjectForKey:host]; + } + } // @synchronized(self) + + // Start fetchers outside of the synchronized block to avoid a deadlock. + for (GTMSessionFetcher *nextFetcher in fetchersToStart) { + [self startFetcher:nextFetcher]; + } + + // The fetcher is no longer in the running or the delayed array, + // so remove its host and thread properties + fetcher.serviceHost = nil; +} + +- (NSUInteger)numberOfFetchers { + NSUInteger running = [self numberOfRunningFetchers]; + NSUInteger delayed = [self numberOfDelayedFetchers]; + return running + delayed; +} + +- (NSUInteger)numberOfRunningFetchers { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + NSUInteger sum = 0; + for (NSString *host in _runningFetchersByHost) { + NSArray *fetchers = [_runningFetchersByHost objectForKey:host]; + sum += fetchers.count; + } + return sum; + } +} + +- (NSUInteger)numberOfDelayedFetchers { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + NSUInteger sum = 0; + for (NSString *host in _delayedFetchersByHost) { + NSArray *fetchers = [_delayedFetchersByHost objectForKey:host]; + sum += fetchers.count; + } + return sum; + } +} + +- (NSArray *)issuedFetchers { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + NSMutableArray *allFetchers = [NSMutableArray array]; + void (^accumulateFetchers)(id, id, BOOL *) = ^(NSString *host, + NSArray *fetchersForHost, + BOOL *stop) { + [allFetchers addObjectsFromArray:fetchersForHost]; + }; + [_runningFetchersByHost enumerateKeysAndObjectsUsingBlock:accumulateFetchers]; + [_delayedFetchersByHost enumerateKeysAndObjectsUsingBlock:accumulateFetchers]; + + GTMSESSION_ASSERT_DEBUG(allFetchers.count == [NSSet setWithArray:allFetchers].count, + @"Fetcher appears multiple times\n running: %@\n delayed: %@", + _runningFetchersByHost, _delayedFetchersByHost); + + return allFetchers.count > 0 ? allFetchers : nil; + } +} + +- (NSArray *)issuedFetchersWithRequestURL:(NSURL *)requestURL { + NSString *host = requestURL.host; + if (host.length == 0) return nil; + + NSURL *targetURL = [requestURL absoluteURL]; + + NSArray *allFetchers = [self issuedFetchers]; + NSIndexSet *indexes = [allFetchers indexesOfObjectsPassingTest:^BOOL(GTMSessionFetcher *fetcher, + NSUInteger idx, + BOOL *stop) { + NSURL *fetcherURL = [fetcher.request.URL absoluteURL]; + return [fetcherURL isEqual:targetURL]; + }]; + + NSArray *result = nil; + if (indexes.count > 0) { + result = [allFetchers objectsAtIndexes:indexes]; + } + return result; +} + +- (void)stopAllFetchers { + NSArray *delayedFetchersByHost; + NSArray *runningFetchersByHost; + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + // Set the time barrier so fetchers know not to call back even if + // the stop calls below occur after the fetchers naturally + // stopped and so were removed from _runningFetchersByHost, + // but while the callbacks were already enqueued before stopAllFetchers + // was invoked. + _stoppedAllFetchersDate = [[NSDate alloc] init]; + + // Remove fetchers from the delayed list to avoid fetcherDidStop: from + // starting more fetchers running as a side effect of stopping one + delayedFetchersByHost = _delayedFetchersByHost.allValues; + [_delayedFetchersByHost removeAllObjects]; + + runningFetchersByHost = _runningFetchersByHost.allValues; + [_runningFetchersByHost removeAllObjects]; + } + + for (NSArray *delayedForHost in delayedFetchersByHost) { + for (GTMSessionFetcher *fetcher in delayedForHost) { + [self stopFetcher:fetcher]; + } + } + + for (NSArray *runningForHost in runningFetchersByHost) { + for (GTMSessionFetcher *fetcher in runningForHost) { + [self stopFetcher:fetcher]; + } + } +} + +- (NSDate *)stoppedAllFetchersDate { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _stoppedAllFetchersDate; + } +} + +#pragma mark Accessors + +- (BOOL)reuseSession { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _delegateDispatcher != nil; + } +} + +- (void)setReuseSession:(BOOL)shouldReuse { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + BOOL wasReusing = (_delegateDispatcher != nil); + if (shouldReuse != wasReusing) { + [self abandonDispatcher]; + if (shouldReuse) { + _delegateDispatcher = + [[GTMSessionFetcherSessionDelegateDispatcher alloc] initWithParentService:self + sessionDiscardInterval:_unusedSessionTimeout]; + } else { + _delegateDispatcher = nil; + } + } + } +} + +- (void)resetSession { + GTMSessionCheckNotSynchronized(self); + dispatch_semaphore_wait(_sessionCreationSemaphore, DISPATCH_TIME_FOREVER); + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + [self resetSessionInternal]; + } + + dispatch_semaphore_signal(_sessionCreationSemaphore); +} + +- (void)resetSessionInternal { + GTMSessionCheckSynchronized(self); + + // The old dispatchers may be retained as delegates of any ongoing sessions by those sessions. + if (_delegateDispatcher) { + [self abandonDispatcher]; + _delegateDispatcher = + [[GTMSessionFetcherSessionDelegateDispatcher alloc] initWithParentService:self + sessionDiscardInterval:_unusedSessionTimeout]; + } +} + +- (void)resetSessionForDispatcherDiscardTimer:(NSTimer *)timer { + GTMSessionCheckNotSynchronized(self); + + dispatch_semaphore_wait(_sessionCreationSemaphore, DISPATCH_TIME_FOREVER); + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (_delegateDispatcher.discardTimer == timer) { + // If the delegate dispatcher's current discardTimer is the same object as the timer + // that fired, no fetcher has recently attempted to start using the session by calling + // startSessionUsage, which invalidates and nils out the timer. + [self resetSessionInternal]; + } else { + // A fetcher has invalidated the timer between its triggering and now, potentially + // meaning a fetcher has requested access to the NSURLSession, and may be in the process + // of starting a new task. The dispatcher should not be abandoned, as this can lead + // to a race condition between calling -finishTasksAndInvalidate on the NSURLSession + // and the fetcher attempting to create a new task. + } + } + + dispatch_semaphore_signal(_sessionCreationSemaphore); +} + +- (NSTimeInterval)unusedSessionTimeout { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _unusedSessionTimeout; + } +} + +- (void)setUnusedSessionTimeout:(NSTimeInterval)timeout { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _unusedSessionTimeout = timeout; + _delegateDispatcher.discardInterval = timeout; + } +} + +// This method should be called inside of @synchronized(self) +- (void)abandonDispatcher { + GTMSessionCheckSynchronized(self); + [_delegateDispatcher abandon]; +} + +- (NSDictionary *)runningFetchersByHost { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return [_runningFetchersByHost copy]; + } +} + +- (void)setRunningFetchersByHost:(NSDictionary *)dict { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _runningFetchersByHost = [dict mutableCopy]; + } +} + +- (NSDictionary *)delayedFetchersByHost { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return [_delayedFetchersByHost copy]; + } +} + +- (void)setDelayedFetchersByHost:(NSDictionary *)dict { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _delayedFetchersByHost = [dict mutableCopy]; + } +} + +- (id<GTMFetcherAuthorizationProtocol>)authorizer { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _authorizer; + } +} + +- (void)setAuthorizer:(id<GTMFetcherAuthorizationProtocol>)obj { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (obj != _authorizer) { + [self detachAuthorizer]; + } + + _authorizer = obj; + } + + // Use the fetcher service for the authorization fetches if the auth + // object supports fetcher services + if ([obj respondsToSelector:@selector(setFetcherService:)]) { +#if GTM_USE_SESSION_FETCHER + [obj setFetcherService:self]; +#else + [obj setFetcherService:(id)self]; +#endif + } +} + +// This should be called inside a @synchronized(self) block except during dealloc. +- (void)detachAuthorizer { + // This method is called by the fetcher service's dealloc and setAuthorizer: + // methods; do not override. + // + // The fetcher service retains the authorizer, and the authorizer has a + // weak pointer to the fetcher service (a non-zeroing pointer for + // compatibility with iOS 4 and Mac OS X 10.5/10.6.) + // + // When this fetcher service no longer uses the authorizer, we want to remove + // the authorizer's dependence on the fetcher service. Authorizers can still + // function without a fetcher service. + if ([_authorizer respondsToSelector:@selector(fetcherService)]) { + id authFetcherService = [_authorizer fetcherService]; + if (authFetcherService == self) { + [_authorizer setFetcherService:nil]; + } + } +} + +- (dispatch_queue_t GTM_NONNULL_TYPE)callbackQueue { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _callbackQueue; + } // @synchronized(self) +} + +- (void)setCallbackQueue:(dispatch_queue_t GTM_NULLABLE_TYPE)queue { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _callbackQueue = queue ?: dispatch_get_main_queue(); + } // @synchronized(self) +} + +- (NSOperationQueue * GTM_NONNULL_TYPE)sessionDelegateQueue { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _delegateQueue; + } // @synchronized(self) +} + +- (void)setSessionDelegateQueue:(NSOperationQueue * GTM_NULLABLE_TYPE)queue { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _delegateQueue = queue ?: [NSOperationQueue mainQueue]; + } // @synchronized(self) +} + +- (NSOperationQueue *)delegateQueue { + // Provided for compatibility with the old fetcher service. The gtm-oauth2 code respects + // any custom delegate queue for calling the app. + return nil; +} + ++ (NSUInteger)numberOfNonBackgroundSessionFetchers:(NSArray *)fetchers { + NSUInteger sum = 0; + for (GTMSessionFetcher *fetcher in fetchers) { + if (!fetcher.usingBackgroundSession) { + ++sum; + } + } + return sum; +} + +@end + +@implementation GTMSessionFetcherService (TestingSupport) + ++ (instancetype)mockFetcherServiceWithFakedData:(NSData *)fakedDataOrNil + fakedError:(NSError *)fakedErrorOrNil { +#if !GTM_DISABLE_FETCHER_TEST_BLOCK + NSURL *url = [NSURL URLWithString:@"http://example.invalid"]; + NSHTTPURLResponse *fakedResponse = + [[NSHTTPURLResponse alloc] initWithURL:url + statusCode:(fakedErrorOrNil ? 500 : 200) + HTTPVersion:@"HTTP/1.1" + headerFields:nil]; + return [self mockFetcherServiceWithFakedData:fakedDataOrNil + fakedResponse:fakedResponse + fakedError:fakedErrorOrNil]; +#else + GTMSESSION_ASSERT_DEBUG(0, @"Test blocks disabled"); + return nil; +#endif // GTM_DISABLE_FETCHER_TEST_BLOCK +} + ++ (instancetype)mockFetcherServiceWithFakedData:(NSData *)fakedDataOrNil + fakedResponse:(NSHTTPURLResponse *)fakedResponse + fakedError:(NSError *)fakedErrorOrNil { +#if !GTM_DISABLE_FETCHER_TEST_BLOCK + GTMSessionFetcherService *service = [[self alloc] init]; + service.allowedInsecureSchemes = @[ @"http" ]; + service.testBlock = ^(GTMSessionFetcher *fetcherToTest, + GTMSessionFetcherTestResponse testResponse) { + testResponse(fakedResponse, fakedDataOrNil, fakedErrorOrNil); + }; + return service; +#else + GTMSESSION_ASSERT_DEBUG(0, @"Test blocks disabled"); + return nil; +#endif // GTM_DISABLE_FETCHER_TEST_BLOCK +} + +#pragma mark Synchronous Wait for Unit Testing + +- (BOOL)waitForCompletionOfAllFetchersWithTimeout:(NSTimeInterval)timeoutInSeconds { + NSDate *giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds]; + _stoppedFetchersToWaitFor = [NSMutableArray array]; + + BOOL shouldSpinRunLoop = [NSThread isMainThread]; + const NSTimeInterval kSpinInterval = 0.001; + BOOL didTimeOut = NO; + while (([self numberOfFetchers] > 0 || _stoppedFetchersToWaitFor.count > 0)) { + didTimeOut = [giveUpDate timeIntervalSinceNow] < 0; + if (didTimeOut) break; + + GTMSessionFetcher *stoppedFetcher = _stoppedFetchersToWaitFor.firstObject; + if (stoppedFetcher) { + [_stoppedFetchersToWaitFor removeObject:stoppedFetcher]; + [stoppedFetcher waitForCompletionWithTimeout:10.0 * kSpinInterval]; + } + + if (shouldSpinRunLoop) { + NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:kSpinInterval]; + [[NSRunLoop currentRunLoop] runUntilDate:stopDate]; + } else { + [NSThread sleepForTimeInterval:kSpinInterval]; + } + } + _stoppedFetchersToWaitFor = nil; + + return !didTimeOut; +} + +@end + +@implementation GTMSessionFetcherService (BackwardsCompatibilityOnly) + +- (NSInteger)cookieStorageMethod { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _cookieStorageMethod; + } +} + +- (void)setCookieStorageMethod:(NSInteger)cookieStorageMethod { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _cookieStorageMethod = cookieStorageMethod; + } +} + +@end + +@implementation GTMSessionFetcherSessionDelegateDispatcher { + __weak GTMSessionFetcherService *_parentService; + NSURLSession *_session; + + // The task map maps NSURLSessionTasks to GTMSessionFetchers + NSMutableDictionary *_taskToFetcherMap; + // The discard timer will invalidate sessions after the session's last task completes. + NSTimer *_discardTimer; + NSTimeInterval _discardInterval; +} + +@synthesize discardInterval = _discardInterval, + session = _session; + +- (instancetype)init { + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (instancetype)initWithParentService:(GTMSessionFetcherService *)parentService + sessionDiscardInterval:(NSTimeInterval)discardInterval { + self = [super init]; + if (self) { + _discardInterval = discardInterval; + _parentService = parentService; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ %p %@ %@", + [self class], self, + _session ?: @"<no session>", + _taskToFetcherMap.count > 0 ? _taskToFetcherMap : @"<no tasks>"]; +} + +- (NSTimer *)discardTimer { + GTMSessionCheckNotSynchronized(self); + @synchronized(self) { + return _discardTimer; + } +} + +// This method should be called inside of a @synchronized(self) block. +- (void)startDiscardTimer { + GTMSessionCheckSynchronized(self); + [_discardTimer invalidate]; + _discardTimer = nil; + if (_discardInterval > 0) { + _discardTimer = [NSTimer timerWithTimeInterval:_discardInterval + target:self + selector:@selector(discardTimerFired:) + userInfo:nil + repeats:NO]; + [_discardTimer setTolerance:(_discardInterval / 10)]; + [[NSRunLoop mainRunLoop] addTimer:_discardTimer forMode:NSRunLoopCommonModes]; + } +} + +// This method should be called inside of a @synchronized(self) block. +- (void)destroyDiscardTimer { + GTMSessionCheckSynchronized(self); + [_discardTimer invalidate]; + _discardTimer = nil; +} + +- (void)discardTimerFired:(NSTimer *)timer { + GTMSessionFetcherService *service; + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + NSUInteger numberOfTasks = _taskToFetcherMap.count; + if (numberOfTasks == 0) { + service = _parentService; + } + } + + // Inform the service that the discard timer has fired, and should check whether the + // service can abandon us. -resetSession cannot be called directly, as there is a + // race condition that must be guarded against with the NSURLSession being returned + // from sessionForFetcherCreation outside other locks. The service can take steps + // to prevent resetting the session if that has occurred. + // + // The service must be called from outside the @synchronized block. + [service resetSessionForDispatcherDiscardTimer:timer]; +} + +- (void)abandon { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + [self destroySessionAndTimer]; + } +} + +- (void)startSessionUsage { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + [self destroyDiscardTimer]; + } +} + +// This method should be called inside of a @synchronized(self) block. +- (void)destroySessionAndTimer { + GTMSessionCheckSynchronized(self); + [self destroyDiscardTimer]; + + // Break any retain cycle from the session holding the delegate. + [_session finishTasksAndInvalidate]; + + // Immediately clear the session so no new task may be issued with it. + // + // The _taskToFetcherMap needs to stay valid until the outstanding tasks finish. + _session = nil; +} + +- (void)setFetcher:(GTMSessionFetcher *)fetcher forTask:(NSURLSessionTask *)task { + GTMSESSION_ASSERT_DEBUG(fetcher != nil, @"missing fetcher"); + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (_taskToFetcherMap == nil) { + _taskToFetcherMap = [[NSMutableDictionary alloc] init]; + } + + if (fetcher) { + [_taskToFetcherMap setObject:fetcher forKey:task]; + [self destroyDiscardTimer]; + } + } +} + +- (void)removeFetcher:(GTMSessionFetcher *)fetcher { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + // Typically, a fetcher should be removed when its task invokes + // URLSession:task:didCompleteWithError:. + // + // When fetching with a testBlock, though, the task completed delegate + // method may not be invoked, requiring cleanup here. + NSArray *tasks = [_taskToFetcherMap allKeysForObject:fetcher]; + GTMSESSION_ASSERT_DEBUG(tasks.count <= 1, @"fetcher task not unmapped: %@", tasks); + [_taskToFetcherMap removeObjectsForKeys:tasks]; + + if (_taskToFetcherMap.count == 0) { + [self startDiscardTimer]; + } + } +} + +// This helper method provides synchronized access to the task map for the delegate +// methods below. +- (id)fetcherForTask:(NSURLSessionTask *)task { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return [_taskToFetcherMap objectForKey:task]; + } +} + +- (void)removeTaskFromMap:(NSURLSessionTask *)task { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + [_taskToFetcherMap removeObjectForKey:task]; + } +} + +- (void)setSession:(NSURLSession *)session { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _session = session; + } +} + +- (NSURLSession *)session { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _session; + } +} + +- (NSTimeInterval)discardInterval { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _discardInterval; + } +} + +- (void)setDiscardInterval:(NSTimeInterval)interval { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _discardInterval = interval; + } +} + +// NSURLSessionDelegate protocol methods. + +// - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session; +// +// TODO(seh): How do we route this to an appropriate fetcher? + + +- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error { + GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ didBecomeInvalidWithError:%@", + [self class], self, session, error); + NSDictionary *localTaskToFetcherMap; + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _session = nil; + + localTaskToFetcherMap = [_taskToFetcherMap copy]; + } + + // Any "suspended" tasks may not have received callbacks from NSURLSession when the session + // completes; we'll call them now. + [localTaskToFetcherMap enumerateKeysAndObjectsUsingBlock:^(NSURLSessionTask *task, + GTMSessionFetcher *fetcher, + BOOL *stop) { + if (fetcher.session == session) { + // Our delegate method URLSession:task:didCompleteWithError: will rely on + // _taskToFetcherMap so that should still contain this fetcher. + NSError *canceledError = [NSError errorWithDomain:NSURLErrorDomain + code:NSURLErrorCancelled + userInfo:nil]; + [self URLSession:session task:task didCompleteWithError:canceledError]; + } else { + GTMSESSION_ASSERT_DEBUG(0, @"Unexpected session in fetcher: %@ has %@ (expected %@)", + fetcher, fetcher.session, session); + } + }]; + + // Our tests rely on this notification to know the session discard timer fired. + NSDictionary *userInfo = @{ kGTMSessionFetcherServiceSessionKey : session }; + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc postNotificationName:kGTMSessionFetcherServiceSessionBecameInvalidNotification + object:_parentService + userInfo:userInfo]; +} + + +#pragma mark - NSURLSessionTaskDelegate + +// NSURLSessionTaskDelegate protocol methods. +// +// We won't test here if the fetcher responds to these since we only want this +// class to implement the same delegate methods the fetcher does (so NSURLSession's +// tests for respondsToSelector: will have the same result whether the session +// delegate is the fetcher or this dispatcher.) + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task +willPerformHTTPRedirection:(NSHTTPURLResponse *)response + newRequest:(NSURLRequest *)request + completionHandler:(void (^)(NSURLRequest *))completionHandler { + id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task]; + [fetcher URLSession:session + task:task +willPerformHTTPRedirection:response + newRequest:request + completionHandler:completionHandler]; +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task +didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge + completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))handler { + id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task]; + [fetcher URLSession:session + task:task + didReceiveChallenge:challenge + completionHandler:handler]; +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + needNewBodyStream:(void (^)(NSInputStream *bodyStream))handler { + id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task]; + [fetcher URLSession:session + task:task + needNewBodyStream:handler]; +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + didSendBodyData:(int64_t)bytesSent + totalBytesSent:(int64_t)totalBytesSent +totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend { + id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task]; + [fetcher URLSession:session + task:task + didSendBodyData:bytesSent + totalBytesSent:totalBytesSent +totalBytesExpectedToSend:totalBytesExpectedToSend]; +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task +didCompleteWithError:(NSError *)error { + id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task]; + + // This is the usual way tasks are removed from the task map. + [self removeTaskFromMap:task]; + + [fetcher URLSession:session + task:task + didCompleteWithError:error]; +} + +// NSURLSessionDataDelegate protocol methods. + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask +didReceiveResponse:(NSURLResponse *)response + completionHandler:(void (^)(NSURLSessionResponseDisposition))handler { + id<NSURLSessionDataDelegate> fetcher = [self fetcherForTask:dataTask]; + [fetcher URLSession:session + dataTask:dataTask + didReceiveResponse:response + completionHandler:handler]; +} + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask +didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask { + id<NSURLSessionDataDelegate> fetcher = [self fetcherForTask:dataTask]; + GTMSESSION_ASSERT_DEBUG(fetcher != nil, @"Missing fetcher for %@", dataTask); + [self removeTaskFromMap:dataTask]; + if (fetcher) { + GTMSESSION_ASSERT_DEBUG([fetcher isKindOfClass:[GTMSessionFetcher class]], + @"Expecting GTMSessionFetcher"); + [self setFetcher:(GTMSessionFetcher *)fetcher forTask:downloadTask]; + } + + [fetcher URLSession:session + dataTask:dataTask +didBecomeDownloadTask:downloadTask]; +} + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask + didReceiveData:(NSData *)data { + id<NSURLSessionDataDelegate> fetcher = [self fetcherForTask:dataTask]; + [fetcher URLSession:session + dataTask:dataTask + didReceiveData:data]; +} + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask + willCacheResponse:(NSCachedURLResponse *)proposedResponse + completionHandler:(void (^)(NSCachedURLResponse *))handler { + id<NSURLSessionDataDelegate> fetcher = [self fetcherForTask:dataTask]; + [fetcher URLSession:session + dataTask:dataTask + willCacheResponse:proposedResponse + completionHandler:handler]; +} + +// NSURLSessionDownloadDelegate protocol methods. + +- (void)URLSession:(NSURLSession *)session + downloadTask:(NSURLSessionDownloadTask *)downloadTask +didFinishDownloadingToURL:(NSURL *)location { + id<NSURLSessionDownloadDelegate> fetcher = [self fetcherForTask:downloadTask]; + [fetcher URLSession:session + downloadTask:downloadTask +didFinishDownloadingToURL:location]; +} + +- (void)URLSession:(NSURLSession *)session + downloadTask:(NSURLSessionDownloadTask *)downloadTask + didWriteData:(int64_t)bytesWritten + totalBytesWritten:(int64_t)totalWritten +totalBytesExpectedToWrite:(int64_t)totalExpected { + id<NSURLSessionDownloadDelegate> fetcher = [self fetcherForTask:downloadTask]; + [fetcher URLSession:session + downloadTask:downloadTask + didWriteData:bytesWritten + totalBytesWritten:totalWritten +totalBytesExpectedToWrite:totalExpected]; +} + +- (void)URLSession:(NSURLSession *)session + downloadTask:(NSURLSessionDownloadTask *)downloadTask + didResumeAtOffset:(int64_t)fileOffset +expectedTotalBytes:(int64_t)expectedTotalBytes { + id<NSURLSessionDownloadDelegate> fetcher = [self fetcherForTask:downloadTask]; + [fetcher URLSession:session + downloadTask:downloadTask + didResumeAtOffset:fileOffset + expectedTotalBytes:expectedTotalBytes]; +} + +@end diff --git a/Pods/GTMSessionFetcher/Source/GTMSessionUploadFetcher.h b/Pods/GTMSessionFetcher/Source/GTMSessionUploadFetcher.h @@ -0,0 +1,166 @@ +/* Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// GTMSessionUploadFetcher implements Google's resumable upload protocol. + +// +// This subclass of GTMSessionFetcher simulates the series of fetches +// needed for chunked upload as a single fetch operation. +// +// Protocol document: TBD +// +// To the client, the only fetcher that exists is this class; the subsidiary +// fetchers needed for uploading chunks are not visible (though the most recent +// chunk fetcher may be accessed via the -activeFetcher or -chunkFetcher methods, and +// -responseHeaders and -statusCode reflect results from the most recent chunk +// fetcher.) +// +// Chunk fetchers are discarded as soon as they have completed. +// +// The protocol also allows for a cancellation notification request to be sent to the +// server to allow discarding of the currently uploaded data and this will be sent +// automatically upon calling stopFetching if the upload has already started. +// +// Note: Unlike the fetcher superclass, the methods of GTMSessionUploadFetcher should +// only be used from the main thread until further work is done to make this subclass +// thread-safe. + +#import "GTMSessionFetcher.h" +#import "GTMSessionFetcherService.h" + +GTM_ASSUME_NONNULL_BEGIN + +// The value to use for file size parameters when the file size is not yet known. +extern int64_t const kGTMSessionUploadFetcherUnknownFileSize; + +// Unless an application knows it needs a smaller chunk size, it should use the standard +// chunk size, which sends the entire file as a single chunk to minimize upload overhead. +// Setting an explicit chunk size that comfortably fits in memory is advisable for large +// uploads. +extern int64_t const kGTMSessionUploadFetcherStandardChunkSize; + +// When uploading requires data buffer allocations (such as uploading from an NSData or +// an NSFileHandle) this is the maximum buffer size that will be created by the fetcher. +extern int64_t const kGTMSessionUploadFetcherMaximumDemandBufferSize; + +// Notification that the upload location URL was provided by the server. +extern NSString *const kGTMSessionFetcherUploadLocationObtainedNotification; + +// Block to provide data during uploads. +// +// Response data may be allocated with dataWithBytesNoCopy:length:freeWhenDone: for efficiency, +// and released after the response block returns. +// +// If the length of the file being uploaded is unknown or already set, send +// kGTMSessionUploadFetcherUnknownFileSize for |fullUploadLength|. Otherwise, set |fullUploadLength| +// to its proper value. +// +// Pass nil as the data (and optionally an NSError) for a failure. +typedef void (^GTMSessionUploadFetcherDataProviderResponse)(NSData * GTM_NULLABLE_TYPE data, + int64_t fullUploadLength, + NSError * GTM_NULLABLE_TYPE error); +// Do not call the response with an NSData object with less data than the requested length unless +// you are passing the fullUploadLength to the fetcher for the first time and it is the last chunk +// of data in the file being uploaded. +typedef void (^GTMSessionUploadFetcherDataProvider)(int64_t offset, int64_t length, + GTMSessionUploadFetcherDataProviderResponse response); + +// Block to be notified about the final status of the cancellation request started in stopFetching. +// +// |fetcher| will be the cancel request that was sent to the server, or nil if stopFetching is not +// going to send a cancel request. If |fetcher| is provided, the other parameters correspond to the +// completion handler of the cancellation request fetcher. +typedef void (^GTMSessionUploadFetcherCancellationHandler)( + GTMSessionFetcher * GTM_NULLABLE_TYPE fetcher, + NSData * GTM_NULLABLE_TYPE data, + NSError * GTM_NULLABLE_TYPE error); + +@interface GTMSessionUploadFetcher : GTMSessionFetcher + +// Create an upload fetcher specifying either the request or the resume location URL, +// then set an upload data source using one of these: +// +// setUploadFileURL: +// setUploadDataLength:provider: +// setUploadFileHandle: +// setUploadData: + ++ (instancetype)uploadFetcherWithRequest:(NSURLRequest *)request + uploadMIMEType:(NSString *)uploadMIMEType + chunkSize:(int64_t)chunkSize + fetcherService:(GTM_NULLABLE GTMSessionFetcherService *)fetcherServiceOrNil; + ++ (instancetype)uploadFetcherWithLocation:(NSURL * GTM_NULLABLE_TYPE)uploadLocationURL + uploadMIMEType:(NSString *)uploadMIMEType + chunkSize:(int64_t)chunkSize + fetcherService:(GTM_NULLABLE GTMSessionFetcherService *)fetcherServiceOrNil; + +// Allows dataProviders for files of unknown length. Pass kGTMSessionUploadFetcherUnknownFileSize as +// |fullLength| if the length is unknown. +- (void)setUploadDataLength:(int64_t)fullLength + provider:(GTM_NULLABLE GTMSessionUploadFetcherDataProvider)block; + ++ (NSArray *)uploadFetchersForBackgroundSessions; ++ (GTM_NULLABLE instancetype)uploadFetcherForSessionIdentifier:(NSString *)sessionIdentifier; + +- (void)pauseFetching; +- (void)resumeFetching; +- (BOOL)isPaused; + +@property(atomic, strong, GTM_NULLABLE) NSURL *uploadLocationURL; +@property(atomic, strong, GTM_NULLABLE) NSData *uploadData; +@property(atomic, strong, GTM_NULLABLE) NSURL *uploadFileURL; +@property(atomic, strong, GTM_NULLABLE) NSFileHandle *uploadFileHandle; +@property(atomic, copy, readonly, GTM_NULLABLE) GTMSessionUploadFetcherDataProvider uploadDataProvider; +@property(atomic, copy) NSString *uploadMIMEType; +@property(atomic, assign) int64_t chunkSize; +@property(atomic, readonly, assign) int64_t currentOffset; + +// The fetcher for the current data chunk, if any +@property(atomic, strong, GTM_NULLABLE) GTMSessionFetcher *chunkFetcher; + +// The active fetcher is the current chunk fetcher, or the upload fetcher itself +// if no chunk fetcher has yet been created. +@property(atomic, readonly) GTMSessionFetcher *activeFetcher; + +// The last request made by an active fetcher. Useful for testing. +@property(atomic, readonly, GTM_NULLABLE) NSURLRequest *lastChunkRequest; + +// The status code from the most recently-completed fetch. +@property(atomic, assign) NSInteger statusCode; + +// Invoked as part of the stop fetching process. Invoked immediately if there is no upload in +// progress, otherwise invoked with the results of the attempt to notify the server that the +// upload will not continue. +// +// Unlike other callbacks, since this is related specifically to the stopFetching flow it is not +// cleared by stopFetching. It will instead clear itself after it is invoked or if the completion +// has occured before stopFetching is called. +@property(atomic, copy, GTM_NULLABLE) GTMSessionUploadFetcherCancellationHandler + cancellationHandler; + +// Exposed for testing only. +@property(atomic, readonly, GTM_NULLABLE) dispatch_queue_t delegateCallbackQueue; +@property(atomic, readonly, GTM_NULLABLE) GTMSessionFetcherCompletionHandler delegateCompletionHandler; + +@end + +@interface GTMSessionFetcher (GTMSessionUploadFetcherMethods) + +@property(readonly, GTM_NULLABLE) GTMSessionUploadFetcher *parentUploadFetcher; + +@end + +GTM_ASSUME_NONNULL_END diff --git a/Pods/GTMSessionFetcher/Source/GTMSessionUploadFetcher.m b/Pods/GTMSessionFetcher/Source/GTMSessionUploadFetcher.m @@ -0,0 +1,1954 @@ +/* Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "GTMSessionUploadFetcher.h" + +static NSString *const kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey = @"_upChunk"; +static NSString *const kGTMSessionIdentifierUploadFileURLMetadataKey = @"_upFileURL"; +static NSString *const kGTMSessionIdentifierUploadFileLengthMetadataKey = @"_upFileLen"; +static NSString *const kGTMSessionIdentifierUploadLocationURLMetadataKey = @"_upLocURL"; +static NSString *const kGTMSessionIdentifierUploadMIMETypeMetadataKey = @"_uploadMIME"; +static NSString *const kGTMSessionIdentifierUploadChunkSizeMetadataKey = @"_upChSize"; +static NSString *const kGTMSessionIdentifierUploadCurrentOffsetMetadataKey = @"_upOffset"; + +static NSString *const kGTMSessionHeaderXGoogUploadChunkGranularity = @"X-Goog-Upload-Chunk-Granularity"; +static NSString *const kGTMSessionHeaderXGoogUploadCommand = @"X-Goog-Upload-Command"; +static NSString *const kGTMSessionHeaderXGoogUploadContentLength = @"X-Goog-Upload-Content-Length"; +static NSString *const kGTMSessionHeaderXGoogUploadContentType = @"X-Goog-Upload-Content-Type"; +static NSString *const kGTMSessionHeaderXGoogUploadOffset = @"X-Goog-Upload-Offset"; +static NSString *const kGTMSessionHeaderXGoogUploadProtocol = @"X-Goog-Upload-Protocol"; +static NSString *const kGTMSessionXGoogUploadProtocolResumable = @"resumable"; +static NSString *const kGTMSessionHeaderXGoogUploadSizeReceived = @"X-Goog-Upload-Size-Received"; +static NSString *const kGTMSessionHeaderXGoogUploadStatus = @"X-Goog-Upload-Status"; +static NSString *const kGTMSessionHeaderXGoogUploadURL = @"X-Goog-Upload-URL"; + +// Property of chunk fetchers identifying the parent upload fetcher. Non-retained NSValue. +static NSString *const kGTMSessionUploadFetcherChunkParentKey = @"_uploadFetcherChunkParent"; + +int64_t const kGTMSessionUploadFetcherUnknownFileSize = -1; + +int64_t const kGTMSessionUploadFetcherStandardChunkSize = (int64_t)LLONG_MAX; + +#if TARGET_OS_IPHONE +int64_t const kGTMSessionUploadFetcherMaximumDemandBufferSize = 10 * 1024 * 1024; // 10 MB for iOS, watchOS, tvOS +#else +int64_t const kGTMSessionUploadFetcherMaximumDemandBufferSize = 100 * 1024 * 1024; // 100 MB for macOS +#endif + +typedef NS_ENUM(NSUInteger, GTMSessionUploadFetcherStatus) { + kStatusUnknown, + kStatusActive, + kStatusFinal, + kStatusCancelled, +}; + +NSString *const kGTMSessionFetcherUploadLocationObtainedNotification = + @"kGTMSessionFetcherUploadLocationObtainedNotification"; + +#if !GTMSESSION_BUILD_COMBINED_SOURCES +@interface GTMSessionFetcher (ProtectedMethods) + +// Access to non-public method on the parent fetcher class. +- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks; +- (void)createSessionIdentifierWithMetadata:(NSDictionary *)metadata; +- (GTMSessionFetcherCompletionHandler)completionHandlerWithTarget:(id)target + didFinishSelector:(SEL)finishedSelector; +- (void)invokeOnCallbackQueue:(dispatch_queue_t)callbackQueue + afterUserStopped:(BOOL)afterStopped + block:(void (^)(void))block; +- (NSTimer *)retryTimer; +- (void)beginFetchForRetry; + +@property(readwrite, strong) NSData *downloadedData; +- (void)releaseCallbacks; + +- (NSInteger)statusCodeUnsynchronized; + +- (BOOL)userStoppedFetching; + +@end +#endif // !GTMSESSION_BUILD_COMBINED_SOURCES + +@interface GTMSessionUploadFetcher () + +// Changing readonly to readwrite. +@property(atomic, strong, readwrite) NSURLRequest *lastChunkRequest; +@property(atomic, readwrite, assign) int64_t currentOffset; + +// Internal properties. +@property(strong, atomic, GTM_NULLABLE) GTMSessionFetcher *fetcherInFlight; // Synchronized on self. + +@property(assign, atomic, getter=isSubdataGenerating) BOOL subdataGenerating; +@property(assign, atomic) BOOL shouldInitiateOffsetQuery; +@property(assign, atomic) int64_t uploadGranularity; + +@end + +@implementation GTMSessionUploadFetcher { + GTMSessionFetcher *_chunkFetcher; + + // We'll call through to the delegate's completion handler. + GTMSessionFetcherCompletionHandler _delegateCompletionHandler; + dispatch_queue_t _delegateCallbackQueue; + + // The initial fetch's body length and bytes actually sent are + // needed for calculating progress during subsequent chunk uploads + int64_t _initialBodyLength; + int64_t _initialBodySent; + + // The upload server address for the chunks of this upload session. + NSURL *_uploadLocationURL; + + // _uploadData, _uploadDataProvider, or _uploadFileHandle may be set, but only one. + NSData *_uploadData; + NSFileHandle *_uploadFileHandle; + GTMSessionUploadFetcherDataProvider _uploadDataProvider; + NSURL *_uploadFileURL; + int64_t _uploadFileLength; + NSString *_uploadMIMEType; + int64_t _chunkSize; + int64_t _uploadGranularity; + BOOL _isPaused; + BOOL _isRestartedUpload; + BOOL _shouldInitiateOffsetQuery; + + // Tied to useBackgroundSession property, since this property is applicable to chunk fetchers. + BOOL _useBackgroundSessionOnChunkFetchers; + + // We keep the latest offset into the upload data just for progress reporting. + int64_t _currentOffset; + + NSDictionary *_recentChunkReponseHeaders; + NSInteger _recentChunkStatusCode; + + // For waiting, we need to know the fetcher in flight, if any, and if subdata generation + // is in progress. + GTMSessionFetcher *_fetcherInFlight; + BOOL _isSubdataGenerating; + BOOL _isCancelInFlight; + + GTMSessionUploadFetcherCancellationHandler _cancellationHandler; +} + ++ (void)load { + [self uploadFetchersForBackgroundSessions]; +} + ++ (instancetype)uploadFetcherWithRequest:(NSURLRequest *)request + uploadMIMEType:(NSString *)uploadMIMEType + chunkSize:(int64_t)chunkSize + fetcherService:(GTMSessionFetcherService *)fetcherService { + GTMSessionUploadFetcher *fetcher = [self uploadFetcherWithRequest:request + fetcherService:fetcherService]; + [fetcher setLocationURL:nil + uploadMIMEType:uploadMIMEType + chunkSize:chunkSize]; + return fetcher; +} + ++ (instancetype)uploadFetcherWithLocation:(NSURL * GTM_NULLABLE_TYPE)uploadLocationURL + uploadMIMEType:(NSString *)uploadMIMEType + chunkSize:(int64_t)chunkSize + fetcherService:(GTMSessionFetcherService *)fetcherService { + GTMSessionUploadFetcher *fetcher = [self uploadFetcherWithRequest:nil + fetcherService:fetcherService]; + [fetcher setLocationURL:uploadLocationURL + uploadMIMEType:uploadMIMEType + chunkSize:chunkSize]; + return fetcher; +} + ++ (instancetype)uploadFetcherForSessionIdentifierMetadata:(NSDictionary *)metadata { + GTMSESSION_ASSERT_DEBUG( + [metadata[kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey] boolValue], + @"Session identifier metadata is not for an upload fetcher: %@", metadata); + + NSNumber *uploadFileLengthNum = metadata[kGTMSessionIdentifierUploadFileLengthMetadataKey]; + GTMSESSION_ASSERT_DEBUG(uploadFileLengthNum != nil, + @"Session metadata missing an UploadFileSize"); + if (uploadFileLengthNum == nil) return nil; + + int64_t uploadFileLength = [uploadFileLengthNum longLongValue]; + GTMSESSION_ASSERT_DEBUG(uploadFileLength >= 0, @"Session metadata UploadFileSize is unknown"); + + NSString *uploadFileURLString = metadata[kGTMSessionIdentifierUploadFileURLMetadataKey]; + GTMSESSION_ASSERT_DEBUG(uploadFileURLString, @"Session metadata missing an UploadFileURL"); + if (uploadFileURLString == nil) return nil; + + NSURL *uploadFileURL = [NSURL URLWithString:uploadFileURLString]; + // There used to be a call here to NSURL checkResourceIsReachableAndReturnError: to check for the + // existence of the file (also tried NSFileManager fileExistsAtPath:). We've determined + // empirically that the check can fail at startup even when the upload file does in fact exist. + // For now, we'll go ahead and restore the background upload fetcher. If the file doesn't exist, + // it will fail later. + + NSString *uploadLocationURLString = metadata[kGTMSessionIdentifierUploadLocationURLMetadataKey]; + NSURL *uploadLocationURL = + uploadLocationURLString ? [NSURL URLWithString:uploadLocationURLString] : nil; + + NSString *uploadMIMEType = + metadata[kGTMSessionIdentifierUploadMIMETypeMetadataKey]; + int64_t uploadChunkSize = + [metadata[kGTMSessionIdentifierUploadChunkSizeMetadataKey] longLongValue]; + if (uploadChunkSize <= 0) { + uploadChunkSize = kGTMSessionUploadFetcherStandardChunkSize; + } + int64_t currentOffset = + [metadata[kGTMSessionIdentifierUploadCurrentOffsetMetadataKey] longLongValue]; + GTMSESSION_ASSERT_DEBUG(currentOffset <= uploadFileLength, + @"CurrentOffset (%lld) exceeds UploadFileSize (%lld)", + currentOffset, uploadFileLength); + if (currentOffset > uploadFileLength) return nil; + + GTMSessionUploadFetcher *uploadFetcher = [self uploadFetcherWithLocation:uploadLocationURL + uploadMIMEType:uploadMIMEType + chunkSize:uploadChunkSize + fetcherService:nil]; + // Set the upload file length before setting the upload file URL tries to determine the length. + [uploadFetcher setUploadFileLength:uploadFileLength]; + + uploadFetcher.uploadFileURL = uploadFileURL; + uploadFetcher.sessionUserInfo = metadata; + uploadFetcher.useBackgroundSession = YES; + uploadFetcher.currentOffset = currentOffset; + uploadFetcher.delegateCallbackQueue = uploadFetcher.callbackQueue; + uploadFetcher.allowedInsecureSchemes = @[ @"http" ]; // Allowed on restored upload fetcher. + return uploadFetcher; +} + ++ (instancetype)uploadFetcherWithRequest:(NSURLRequest *)request + fetcherService:(GTMSessionFetcherService *)fetcherService { + // Internal utility method for instantiating fetchers + GTMSessionUploadFetcher *fetcher; + if ([fetcherService isKindOfClass:[GTMSessionFetcherService class]]) { + fetcher = [fetcherService fetcherWithRequest:request + fetcherClass:self]; + } else { + fetcher = [self fetcherWithRequest:request]; + } + fetcher.useBackgroundSession = YES; + return fetcher; +} + ++ (NSPointerArray *)uploadFetcherPointerArrayForBackgroundSessions { + static NSPointerArray *gUploadFetcherPointerArrayForBackgroundSessions = nil; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + gUploadFetcherPointerArrayForBackgroundSessions = [NSPointerArray weakObjectsPointerArray]; + }); + return gUploadFetcherPointerArrayForBackgroundSessions; +} + ++ (instancetype)uploadFetcherForSessionIdentifier:(NSString *)sessionIdentifier { + GTMSESSION_ASSERT_DEBUG(sessionIdentifier != nil, @"Invalid session identifier"); + NSArray *uploadFetchersForBackgroundSessions = [self uploadFetchersForBackgroundSessions]; + for (GTMSessionUploadFetcher *uploadFetcher in uploadFetchersForBackgroundSessions) { + if ([uploadFetcher.chunkFetcher.sessionIdentifier isEqual:sessionIdentifier]) { + return uploadFetcher; + } + } + return nil; +} + ++ (NSArray *)uploadFetchersForBackgroundSessions { + // Collect the background session upload fetchers that are still in memory. + NSPointerArray *uploadFetcherPointerArray = [self uploadFetcherPointerArrayForBackgroundSessions]; + [uploadFetcherPointerArray compact]; + NSMutableSet *restoredSessionIdentifiers = [[NSMutableSet alloc] init]; + NSMutableArray *uploadFetchers = [[NSMutableArray alloc] init]; + for (GTMSessionUploadFetcher *uploadFetcher in uploadFetcherPointerArray) { + NSString *sessionIdentifier = uploadFetcher.chunkFetcher.sessionIdentifier; + if (sessionIdentifier) { + [restoredSessionIdentifiers addObject:sessionIdentifier]; + [uploadFetchers addObject:uploadFetcher]; + } + } + + // The system may have other ongoing background upload sessions. Restore upload fetchers for those + // too. + NSArray *fetchers = [GTMSessionFetcher fetchersForBackgroundSessions]; + for (GTMSessionFetcher *fetcher in fetchers) { + NSString *sessionIdentifier = fetcher.sessionIdentifier; + if (!sessionIdentifier || [restoredSessionIdentifiers containsObject:sessionIdentifier]) { + continue; + } + NSDictionary *sessionIdentifierMetadata = [fetcher sessionIdentifierMetadata]; + if (sessionIdentifierMetadata == nil) { + continue; + } + if (![sessionIdentifierMetadata[kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey] boolValue]) { + continue; + } + GTMSessionUploadFetcher *uploadFetcher = + [self uploadFetcherForSessionIdentifierMetadata:sessionIdentifierMetadata]; + if (uploadFetcher == nil) { + // Something went wrong with this upload fetcher, so kill the restored chunk fetcher. + [fetcher stopFetching]; + continue; + } + [uploadFetchers addObject:uploadFetcher]; + uploadFetcher->_chunkFetcher = fetcher; + uploadFetcher->_fetcherInFlight = fetcher; + [uploadFetcher attachSendProgressBlockToChunkFetcher:fetcher]; + fetcher.completionHandler = + [fetcher completionHandlerWithTarget:uploadFetcher + didFinishSelector:@selector(chunkFetcher:finishedWithData:error:)]; + + GTMSESSION_LOG_DEBUG(@"%@ restoring upload fetcher %@ for chunk fetcher %@", + [self class], uploadFetcher, fetcher); + } + return uploadFetchers; +} + +- (void)setUploadData:(NSData *)data { + BOOL changed = NO; + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (_uploadData != data) { + _uploadData = data; + changed = YES; + } + } + if (changed) { + [self setupRequestHeaders]; + } +} + +- (NSData *)uploadData { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _uploadData; + } +} + +- (void)setUploadFileHandle:(NSFileHandle *)fh { + BOOL changed = NO; + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (_uploadFileHandle != fh) { + _uploadFileHandle = fh; + changed = YES; + } + } + if (changed) { + [self setupRequestHeaders]; + } +} + +- (NSFileHandle *)uploadFileHandle { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _uploadFileHandle; + } +} + +- (void)setUploadFileURL:(NSURL *)uploadURL { + BOOL changed = NO; + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (_uploadFileURL != uploadURL) { + _uploadFileURL = uploadURL; + changed = YES; + } + } + if (changed) { + [self setupRequestHeaders]; + } +} + +- (NSURL *)uploadFileURL { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _uploadFileURL; + } +} + +- (void)setUploadFileLength:(int64_t)fullLength { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (_uploadFileLength == kGTMSessionUploadFetcherUnknownFileSize && + fullLength != kGTMSessionUploadFetcherUnknownFileSize) { + _uploadFileLength = fullLength; + } + } +} + +- (void)setUploadDataLength:(int64_t)fullLength + provider:(GTMSessionUploadFetcherDataProvider)block { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _uploadDataProvider = [block copy]; + _uploadFileLength = fullLength; + } + [self setupRequestHeaders]; +} + +- (GTMSessionUploadFetcherDataProvider)uploadDataProvider { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _uploadDataProvider; + } +} + + +- (void)setUploadMIMEType:(NSString *)uploadMIMEType { + GTMSESSION_ASSERT_DEBUG(0, @"TODO: disallow setUploadMIMEType by making declaration readonly"); + // (and uploadMIMEType, chunksize, currentOffset) + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _uploadMIMEType = uploadMIMEType; + } +} + +- (NSString *)uploadMIMEType { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _uploadMIMEType; + } +} + +- (void)setChunkSize:(int64_t)chunkSize { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _chunkSize = chunkSize; + } +} + +- (int64_t)chunkSize { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _chunkSize; + } +} + +- (void)setupRequestHeaders { + GTMSessionCheckNotSynchronized(self); + +#if DEBUG + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + int hasData = (_uploadData != nil) ? 1 : 0; + int hasFileHandle = (_uploadFileHandle != nil) ? 1 : 0; + int hasFileURL = (_uploadFileURL != nil) ? 1 : 0; + int hasUploadDataProvider = (_uploadDataProvider != nil) ? 1 : 0; + int numberOfSources = hasData + hasFileHandle + hasFileURL + hasUploadDataProvider; + #pragma unused(numberOfSources) + GTMSESSION_ASSERT_DEBUG(numberOfSources == 1, + @"Need just one upload source (%d)", numberOfSources); + } // @synchronized(self) +#endif + + // Add our custom headers to the initial request indicating the data + // type and total size to be delivered later in the chunk requests. + NSMutableURLRequest *mutableRequest = [self.request mutableCopy]; + + GTMSESSION_ASSERT_DEBUG((mutableRequest == nil) != (_uploadLocationURL == nil), + @"Request and location are mutually exclusive"); + if (!mutableRequest) return; + + [mutableRequest setValue:kGTMSessionXGoogUploadProtocolResumable + forHTTPHeaderField:kGTMSessionHeaderXGoogUploadProtocol]; + [mutableRequest setValue:@"start" + forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand]; + [mutableRequest setValue:_uploadMIMEType + forHTTPHeaderField:kGTMSessionHeaderXGoogUploadContentType]; + [mutableRequest setValue:@([self fullUploadLength]).stringValue + forHTTPHeaderField:kGTMSessionHeaderXGoogUploadContentLength]; + + NSString *method = mutableRequest.HTTPMethod; + if (method == nil || [method caseInsensitiveCompare:@"GET"] == NSOrderedSame) { + [mutableRequest setHTTPMethod:@"POST"]; + } + + // Ensure the user agent header identifies this to the upload server as a + // GTMSessionUploadFetcher client. The /1 can be incremented in the unlikely circumstance + // we need to make a bug fix in the client that the server can recognize. + NSString *const kUserAgentStub = @"(GTMSUF/1)"; + NSString *userAgent = [mutableRequest valueForHTTPHeaderField:@"User-Agent"]; + if (userAgent == nil + || [userAgent rangeOfString:kUserAgentStub].location == NSNotFound) { + if (userAgent.length == 0) { + userAgent = GTMFetcherStandardUserAgentString(nil); + } + userAgent = [userAgent stringByAppendingFormat:@" %@", kUserAgentStub]; + [mutableRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"]; + } + [self setRequest:mutableRequest]; +} + +- (void)setLocationURL:(NSURL * GTM_NULLABLE_TYPE)location + uploadMIMEType:(NSString *)uploadMIMEType + chunkSize:(int64_t)chunkSize { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + GTMSESSION_ASSERT_DEBUG(chunkSize > 0, @"chunk size is zero"); + + // When resuming an upload, set the known upload target URL. + _uploadLocationURL = location; + + _uploadMIMEType = uploadMIMEType; + _chunkSize = chunkSize; + + // Indicate that we've not yet determined the file handle's length + _uploadFileLength = kGTMSessionUploadFetcherUnknownFileSize; + + // Indicate that we've not yet determined the upload fetcher status + _recentChunkStatusCode = -1; + + // If this is restarting an upload begun by another fetcher, + // the location is specified but the request is nil + _isRestartedUpload = (location != nil); + } // @synchronized(self) +} + +- (int64_t)fullUploadLength { + int64_t result; + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (_uploadData) { + result = (int64_t)_uploadData.length; + } else { + if (_uploadFileLength == kGTMSessionUploadFetcherUnknownFileSize) { + if (_uploadFileHandle) { + // First time through, seek to end to determine file length + _uploadFileLength = (int64_t)[_uploadFileHandle seekToEndOfFile]; + } else if (_uploadDataProvider) { + // _uploadFileLength is set when the _uploadDataProvider is set. + GTMSESSION_ASSERT_DEBUG(_uploadFileLength >= 0, @"No uploadDataProvider length set"); + } else { + NSNumber *filesizeNum; + NSError *valueError; + if ([_uploadFileURL getResourceValue:&filesizeNum + forKey:NSURLFileSizeKey + error:&valueError]) { + _uploadFileLength = filesizeNum.longLongValue; + } else { + GTMSESSION_ASSERT_DEBUG(NO, @"Cannot get file size: %@\n %@", + valueError, _uploadFileURL.path); + _uploadFileLength = 0; + } + } + } + result = _uploadFileLength; + } + } // @synchronized(self) + return result; +} + +// Make a subdata of the upload data. +- (void)generateChunkSubdataWithOffset:(int64_t)offset + length:(int64_t)length + response:(GTMSessionUploadFetcherDataProviderResponse)response { + GTMSessionUploadFetcherDataProvider uploadDataProvider = self.uploadDataProvider; + if (uploadDataProvider) { + uploadDataProvider(offset, length, response); + return; + } + + NSData *uploadData = self.uploadData; + if (uploadData) { + // NSData provided. + NSData *resultData; + if (offset == 0 && length == (int64_t)uploadData.length) { + resultData = uploadData; + } else { + int64_t dataLength = (int64_t)uploadData.length; + // Ensure our range is valid. b/18007814 + if (offset + length > dataLength) { + NSString *errorMessage = [NSString stringWithFormat: + @"Range invalid for upload data. offset: %lld\tlength: %lld\tdataLength: %lld", + offset, length, dataLength]; + GTMSESSION_ASSERT_DEBUG(NO, @"%@", errorMessage); + response(nil, + kGTMSessionUploadFetcherUnknownFileSize, + [self uploadChunkUnavailableErrorWithDescription:errorMessage]); + return; + } + NSRange range = NSMakeRange((NSUInteger)offset, (NSUInteger)length); + + @try { + resultData = [uploadData subdataWithRange:range]; + } + @catch (NSException *exception) { + NSString *errorMessage = exception.description; + GTMSESSION_ASSERT_DEBUG(NO, @"%@", errorMessage); + response(nil, + kGTMSessionUploadFetcherUnknownFileSize, + [self uploadChunkUnavailableErrorWithDescription:errorMessage]); + return; + } + } + response(resultData, kGTMSessionUploadFetcherUnknownFileSize, nil); + return; + } + NSURL *uploadFileURL = self.uploadFileURL; + if (uploadFileURL) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self generateChunkSubdataFromFileURL:uploadFileURL + offset:offset + length:length + response:response]; + }); + return; + } + GTMSESSION_ASSERT_DEBUG(_uploadFileHandle, @"Unexpectedly missing upload data package"); + NSFileHandle *uploadFileHandle = self.uploadFileHandle; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self generateChunkSubdataFromFileHandle:uploadFileHandle + offset:offset + length:length + response:response]; + }); +} + +- (void)generateChunkSubdataFromFileHandle:(NSFileHandle *)fileHandle + offset:(int64_t)offset + length:(int64_t)length + response:(GTMSessionUploadFetcherDataProviderResponse)response { + NSData *resultData; + NSError *error; + @try { + [fileHandle seekToFileOffset:(unsigned long long)offset]; + resultData = [fileHandle readDataOfLength:(NSUInteger)length]; + } + @catch (NSException *exception) { + GTMSESSION_ASSERT_DEBUG(NO, @"uploadFileHandle failed to read, %@", exception); + error = [self uploadChunkUnavailableErrorWithDescription:exception.description]; + } + // The response always re-dispatches to the main thread, so we skip doing that here. + response(resultData, kGTMSessionUploadFetcherUnknownFileSize, error); +} + +- (void)generateChunkSubdataFromFileURL:(NSURL *)fileURL + offset:(int64_t)offset + length:(int64_t)length + response:(GTMSessionUploadFetcherDataProviderResponse)response { + GTMSessionCheckNotSynchronized(self); + + NSData *resultData; + NSError *error; + int64_t fullUploadLength = [self fullUploadLength]; + NSData *mappedData = + [NSData dataWithContentsOfURL:fileURL + options:NSDataReadingMappedAlways + NSDataReadingUncached + error:&error]; + if (!mappedData) { + // We could not create an NSData by memory-mapping the file. +#if TARGET_IPHONE_SIMULATOR + // NSTemporaryDirectory() can differ in the simulator between app restarts, + // yet the contents for the new path remains unchanged, so try the latest temp path. + if ([error.domain isEqual:NSCocoaErrorDomain] && (error.code == NSFileReadNoSuchFileError)) { + NSString *filename = [fileURL lastPathComponent]; + NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:filename]; + NSURL *newFileURL = [NSURL fileURLWithPath:filePath]; + if (![newFileURL isEqual:fileURL]) { + [self generateChunkSubdataFromFileURL:newFileURL + offset:offset + length:length + response:response]; + return; + } + } +#endif + + // If the file is just too large to create an NSData for, or if for some other reason we can't + // map it, create an NSFileHandle instead to read a subset into an NSData. +#if DEBUG + NSNumber *fileSizeNum; + BOOL hasFileSize = [fileURL getResourceValue:&fileSizeNum forKey:NSURLFileSizeKey error:NULL]; + GTMSESSION_LOG_DEBUG(@"Note: uploadFileURL is falling back to creating upload chunks by reading" + @" an NSFileHandle since uploadFileURL failed to map the upload file," + @" file size %@, %@", + hasFileSize ? fileSizeNum : @"unknown", error); +#endif + + NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingFromURL:fileURL + error:&error]; + if (fileHandle != nil) { + [self generateChunkSubdataFromFileHandle:fileHandle + offset:offset + length:length + response:response]; + return; + } + GTMSESSION_ASSERT_DEBUG(NO, @"uploadFileURL failed to read, %@", error); + // Fall through with the error. + } else { + // Successfully created an NSData by memory-mapping the file. + if ((NSUInteger)(offset + length) > mappedData.length) { + NSString *errorMessage = [NSString stringWithFormat: + @"Range invalid for upload data. offset: %lld\tlength: %lld\tdataLength: %lld\texpected UploadLength: %lld", + offset, length, (long long)mappedData.length, fullUploadLength]; + GTMSESSION_ASSERT_DEBUG(NO, @"%@", errorMessage); + response(nil, + kGTMSessionUploadFetcherUnknownFileSize, + [self uploadChunkUnavailableErrorWithDescription:errorMessage]); + return; + } + if (offset > 0 || length < fullUploadLength) { + NSRange range = NSMakeRange((NSUInteger)offset, (NSUInteger)length); + resultData = [mappedData subdataWithRange:range]; + } else { + resultData = mappedData; + } + } + // The response always re-dispatches to the main thread, so we skip re-dispatching here. + response(resultData, kGTMSessionUploadFetcherUnknownFileSize, error); +} + +- (NSError *)uploadChunkUnavailableErrorWithDescription:(NSString *)description { + // The description in the userInfo is intended as a clue to programmers, not + // for client code to examine or rely on. + NSDictionary *userInfo = @{ @"description" : description }; + return [NSError errorWithDomain:kGTMSessionFetcherErrorDomain + code:GTMSessionFetcherErrorUploadChunkUnavailable + userInfo:userInfo]; +} + +- (NSError *)prematureFailureErrorWithUserInfo:(NSDictionary *)userInfo { + // An error for if we get an unexpected status from the upload server or + // otherwise cannot continue. This is an issue beyond the upload protocol; + // there's no way the client can do anything useful except give up. + NSError *error = [NSError errorWithDomain:kGTMSessionFetcherStatusDomain + code:501 // Not implemented + userInfo:userInfo]; + return error; +} + ++ (GTMSessionUploadFetcherStatus)uploadStatusFromResponseHeaders:(NSDictionary *)responseHeaders { + NSString *statusString = [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadStatus]; + if ([statusString isEqual:@"active"]) { + return kStatusActive; + } + if ([statusString isEqual:@"final"]) { + return kStatusFinal; + } + if ([statusString isEqual:@"cancelled"]) { + return kStatusCancelled; + } + return kStatusUnknown; +} + +#pragma mark Method overrides affecting the initial fetch only + +- (void)setCompletionHandler:(GTMSessionFetcherCompletionHandler)handler { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _delegateCompletionHandler = handler; + } +} + +- (void)setDelegateCallbackQueue:(dispatch_queue_t GTM_NULLABLE_TYPE)queue { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _delegateCallbackQueue = queue; + } +} + +- (dispatch_queue_t GTM_NULLABLE_TYPE)delegateCallbackQueue { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _delegateCallbackQueue; + } +} + +- (BOOL)isRestartedUpload { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _isRestartedUpload; + } +} + +- (GTMSessionFetcher * GTM_NULLABLE_TYPE)chunkFetcher { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _chunkFetcher; + } +} + +- (void)setChunkFetcher:(GTMSessionFetcher * GTM_NULLABLE_TYPE)fetcher { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _chunkFetcher = fetcher; + } +} + +- (void)setFetcherInFlight:(GTMSessionFetcher * GTM_NULLABLE_TYPE)fetcher { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _fetcherInFlight = fetcher; + } +} + +- (GTMSessionFetcher * GTM_NULLABLE_TYPE)fetcherInFlight { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _fetcherInFlight; + } +} + +- (void)setCancellationHandler:(GTMSessionUploadFetcherCancellationHandler GTM_NULLABLE_TYPE) + cancellationHandler { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _cancellationHandler = cancellationHandler; + } +} + +- (GTMSessionUploadFetcherCancellationHandler GTM_NULLABLE_TYPE)cancellationHandler { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _cancellationHandler; + } +} + +- (void)beginFetchForRetry { + GTMSessionCheckNotSynchronized(self); + + // Override the superclass to reset the initial body length and fetcher-in-flight, + // then call the superclass implementation. + [self setInitialBodyLength:[self bodyLength]]; + + GTMSESSION_ASSERT_DEBUG(self.fetcherInFlight == nil, @"unexpected fetcher in flight: %@", + self.fetcherInFlight); + self.fetcherInFlight = self; + [super beginFetchForRetry]; +} + +- (void)beginFetchWithCompletionHandler:(GTMSessionFetcherCompletionHandler)handler { + GTMSessionCheckNotSynchronized(self); + + [self setInitialBodyLength:[self bodyLength]]; + + // We'll hold onto the superclass's callback queue so we can invoke the handler + // even after the superclass has released the queue and its callback handler, as + // happens during auth failure. + [self setDelegateCallbackQueue:self.callbackQueue]; + self.completionHandler = handler; + + if ([self isRestartedUpload]) { + // When restarting an upload, we know the destination location for chunk fetches, + // but we need to query to find the initial offset. + if (![self isPaused]) { + [self sendQueryForUploadOffsetWithFetcherProperties:self.properties]; + } + return; + } + // We don't want to call into the client's completion block immediately + // after the finish of the initial connection (the delegate is called only + // when uploading finishes), so we substitute our own completion block to be + // called when the initial connection finishes + GTMSESSION_ASSERT_DEBUG(self.fetcherInFlight == nil, @"unexpected fetcher in flight: %@", + self.fetcherInFlight); + + self.fetcherInFlight = self; + [super beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { + self.fetcherInFlight = nil; + // callback + + BOOL hasTestBlock = (self.testBlock != nil); + if (![self isRestartedUpload] && !hasTestBlock) { + if (error == nil) { + [self beginChunkFetches]; + } else { + if ([self retryTimer] == nil) { + [self invokeFinalCallbackWithData:nil + error:error + shouldInvalidateLocation:YES]; + } + } + } else { + // If there was no initial request, then this fetch is resuming some + // other uploadFetcher's initial request, and the superclass's connection + // is never used, so at this point we call the user's actual completion + // block. + if (!hasTestBlock) { + [self invokeFinalCallbackWithData:data + error:error + shouldInvalidateLocation:YES]; + } else { + // There was a test block, so we won't do chunk fetches, but we simulate obtaining + // the data to be uploaded from the upload data provider block or the file handle, + // and then call back. + [self generateChunkSubdataWithOffset:0 + length:[self fullUploadLength] + response:^(NSData *generateData, int64_t fullUploadLength, NSError *generateError) { + [self invokeFinalCallbackWithData:data + error:error + shouldInvalidateLocation:YES]; + }]; + } + } + }]; +} + +- (void)beginChunkFetches { + GTMSessionCheckNotSynchronized(self); + +#if DEBUG + // The initial response of the resumable upload protocol should have an + // empty body + // + // This assert typically happens because the upload create/edit link URL was + // not supplied with the request, and the server is thus expecting a non- + // resumable request/response. + if (self.downloadedData.length > 0) { + NSData *downloadedData = self.downloadedData; + NSString *str = [[NSString alloc] initWithData:downloadedData + encoding:NSUTF8StringEncoding]; + #pragma unused(str) + GTMSESSION_ASSERT_DEBUG(NO, @"unexpected response data (uploading to the wrong URL?)\n%@", str); + } +#endif + + // We need to get the upload URL from the location header to continue. + NSDictionary *responseHeaders = [self responseHeaders]; + + [self retrieveUploadChunkGranularityFromResponseHeaders:responseHeaders]; + + GTMSessionUploadFetcherStatus uploadStatus = + [[self class] uploadStatusFromResponseHeaders:responseHeaders]; + GTMSESSION_ASSERT_DEBUG(uploadStatus != kStatusUnknown, + @"beginChunkFetches has unexpected upload status for headers %@", responseHeaders); + + BOOL isPrematureStop = (uploadStatus == kStatusFinal) || (uploadStatus == kStatusCancelled); + + NSString *uploadLocationURLStr = [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadURL]; + BOOL hasUploadLocation = (uploadLocationURLStr.length > 0); + + if (isPrematureStop || !hasUploadLocation) { + GTMSESSION_ASSERT_DEBUG(NO, @"Premature failure: upload-status:\"%@\" location:%@", + [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadStatus], uploadLocationURLStr); + // We cannot continue since we do not know the location to use + // as our upload destination. + NSDictionary *userInfo = nil; + NSData *downloadedData = self.downloadedData; + if (downloadedData.length > 0) { + userInfo = @{ kGTMSessionFetcherStatusDataKey : downloadedData }; + } + NSError *failureError = [self prematureFailureErrorWithUserInfo:userInfo]; + [self invokeFinalCallbackWithData:nil + error:failureError + shouldInvalidateLocation:YES]; + return; + } + + self.uploadLocationURL = [NSURL URLWithString:uploadLocationURLStr]; + + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc postNotificationName:kGTMSessionFetcherUploadLocationObtainedNotification + object:self]; + + // we've now sent all of the initial post body data, so we need to include + // its size in future progress indicator callbacks + [self setInitialBodySent:[self initialBodyLength]]; + + // just in case the user paused us during the initial fetch... + if (![self isPaused]) { + [self uploadNextChunkWithOffset:0]; + } +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + didSendBodyData:(int64_t)bytesSent + totalBytesSent:(int64_t)totalBytesSent + totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend { + // Overrides the superclass. + [self invokeDelegateWithDidSendBytes:bytesSent + totalBytesSent:totalBytesSent + totalBytesExpectedToSend:totalBytesExpectedToSend + [self fullUploadLength]]; +} + +- (BOOL)shouldReleaseCallbacksUponCompletion { + // Overrides the superclass. + + // We don't want the superclass to release the delegate and callback + // blocks once the initial fetch has finished + // + // This is invoked for only successful completion of the connection; + // an error always will invoke and release the callbacks + return NO; +} + +- (void)invokeFinalCallbackWithData:(NSData *)data + error:(NSError *)error + shouldInvalidateLocation:(BOOL)shouldInvalidateLocation { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (shouldInvalidateLocation) { + _uploadLocationURL = nil; + } + + dispatch_queue_t queue = _delegateCallbackQueue; + GTMSessionFetcherCompletionHandler handler = _delegateCompletionHandler; + if (queue && handler) { + [self invokeOnCallbackQueue:queue + afterUserStopped:NO + block:^{ + handler(data, error); + }]; + } + } // @synchronized(self) + + [self releaseUploadAndBaseCallbacks:!self.userStoppedFetching]; +} + +- (void)releaseUploadAndBaseCallbacks:(BOOL)shouldReleaseCancellation { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _delegateCallbackQueue = nil; + _delegateCompletionHandler = nil; + _uploadDataProvider = nil; + if (shouldReleaseCancellation) { + _cancellationHandler = nil; + } + } + + // Release the base class's callbacks, too, if needed. + [self releaseCallbacks]; +} + +- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks { + GTMSessionCheckNotSynchronized(self); + + // Clear _fetcherInFlight when stopped. Moved from stopFetching, since that's a public method, + // where this method does the work. Fixes issue clearing value when retryBlock included. + GTMSessionFetcher *fetcherInFlight = self.fetcherInFlight; + if (fetcherInFlight == self) { + self.fetcherInFlight = nil; + } + + [super stopFetchReleasingCallbacks:shouldReleaseCallbacks]; + + if (shouldReleaseCallbacks) { + [self releaseUploadAndBaseCallbacks:NO]; + } +} + +#pragma mark Chunk fetching methods + +- (void)uploadNextChunkWithOffset:(int64_t)offset { + // use the properties in each chunk fetcher + NSDictionary *props = [self properties]; + + [self uploadNextChunkWithOffset:offset + fetcherProperties:props]; +} + +- (void)sendQueryForUploadOffsetWithFetcherProperties:(NSDictionary *)props { + GTMSessionFetcher *queryFetcher = [self uploadFetcherWithProperties:props + isQueryFetch:YES]; + queryFetcher.bodyData = [NSData data]; + + NSString *originalComment = self.comment; + [queryFetcher setCommentWithFormat:@"%@ (query offset)", + originalComment ? originalComment : @"upload"]; + + [queryFetcher setRequestValue:@"query" forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand]; + + self.fetcherInFlight = queryFetcher; + [queryFetcher beginFetchWithDelegate:self + didFinishSelector:@selector(queryFetcher:finishedWithData:error:)]; +} + +- (void)queryFetcher:(GTMSessionFetcher *)queryFetcher + finishedWithData:(NSData *)data + error:(NSError *)error { + self.fetcherInFlight = nil; + + NSDictionary *responseHeaders = [queryFetcher responseHeaders]; + NSString *sizeReceivedHeader; + + GTMSessionUploadFetcherStatus uploadStatus = + [[self class] uploadStatusFromResponseHeaders:responseHeaders]; + GTMSESSION_ASSERT_DEBUG(uploadStatus != kStatusUnknown || error != nil, + @"query fetcher completion has unexpected upload status for headers %@", responseHeaders); + + if (error == nil) { + sizeReceivedHeader = [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadSizeReceived]; + + if (uploadStatus == kStatusCancelled || + (uploadStatus == kStatusActive && sizeReceivedHeader == nil)) { + NSDictionary *userInfo = nil; + if (data.length > 0) { + userInfo = @{ kGTMSessionFetcherStatusDataKey : data }; + } + error = [self prematureFailureErrorWithUserInfo:userInfo]; + } + } + + if (error == nil) { + int64_t offset = [sizeReceivedHeader longLongValue]; + int64_t fullUploadLength = [self fullUploadLength]; + if (uploadStatus == kStatusFinal || + (offset >= fullUploadLength && + fullUploadLength != kGTMSessionUploadFetcherUnknownFileSize)) { + // Handle we're done + [self chunkFetcher:queryFetcher finishedWithData:data error:nil]; + } else { + [self retrieveUploadChunkGranularityFromResponseHeaders:responseHeaders]; + [self uploadNextChunkWithOffset:offset]; + } + } else { + // Handle query error + [self chunkFetcher:queryFetcher finishedWithData:data error:error]; + } +} + +- (void)sendCancelUploadWithFetcherProperties:(NSDictionary *)props { + @synchronized(self) { + _isCancelInFlight = YES; + } + GTMSessionFetcher *cancelFetcher = [self uploadFetcherWithProperties:props + isQueryFetch:YES]; + cancelFetcher.bodyData = [NSData data]; + + NSString *originalComment = self.comment; + [cancelFetcher setCommentWithFormat:@"%@ (cancel)", + originalComment ? originalComment : @"upload"]; + + [cancelFetcher setRequestValue:@"cancel" forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand]; + + self.fetcherInFlight = cancelFetcher; + [cancelFetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { + self.fetcherInFlight = nil; + if (![self triggerCancellationHandlerForFetch:cancelFetcher data:data error:error]) { + if (error) { + GTMSESSION_LOG_DEBUG(@"cancelFetcher %@", error); + } + } + @synchronized(self) { + self->_isCancelInFlight = NO; + } + }]; +} + +- (void)uploadNextChunkWithOffset:(int64_t)offset + fetcherProperties:(NSDictionary *)props { + GTMSessionCheckNotSynchronized(self); + + // Example chunk headers: + // X-Goog-Upload-Command: upload, finalize + // X-Goog-Upload-Offset: 0 + // Content-Length: 2000000 + // Content-Type: image/jpeg + // + // {bytes 0-1999999} + + // The chunk upload URL requires no authentication header. + GTMSessionFetcher *chunkFetcher = [self uploadFetcherWithProperties:props + isQueryFetch:NO]; + [self attachSendProgressBlockToChunkFetcher:chunkFetcher]; + int64_t chunkSize = [self updateChunkFetcher:chunkFetcher + forChunkAtOffset:offset]; + BOOL isUploadingFileURL = (self.uploadFileURL != nil); + int64_t fullUploadLength = [self fullUploadLength]; + + // The chunk size may have changed, so determine again if we're uploading the full file. + BOOL isUploadingFullFile = (offset == 0 && + fullUploadLength != kGTMSessionUploadFetcherUnknownFileSize && + chunkSize >= fullUploadLength); + if (isUploadingFullFile && isUploadingFileURL) { + // The data is the full upload file URL. + chunkFetcher.bodyFileURL = self.uploadFileURL; + [self beginChunkFetcher:chunkFetcher + offset:offset]; + } else { + // Make an NSData for the subset for this upload chunk. + self.subdataGenerating = YES; + [self generateChunkSubdataWithOffset:offset + length:chunkSize + response:^(NSData *chunkData, int64_t uploadFileLength, NSError *chunkError) { + // The subdata methods may leave us on a background thread. + dispatch_async(dispatch_get_main_queue(), ^{ + self.subdataGenerating = NO; + + // dont allow the updating of fileLength for uploads not using a data provider as they + // should know the file length before the upload starts. + if (self->_uploadDataProvider != nil && uploadFileLength > 0) { + [self setUploadFileLength:uploadFileLength]; + // Update the command and content-length headers if this is the last chunk to be sent. + if (offset + chunkSize >= uploadFileLength) { + int64_t updatedChunkSize = [self updateChunkFetcher:chunkFetcher + forChunkAtOffset:offset]; + if (updatedChunkSize == 0) { + // Calling beginChunkFetcher early when there is no more data to send allows us to + // properly handle nil chunkData below without having to account for the case where + // we are just finalizing the file. + chunkFetcher.bodyData = [[NSData alloc] init]; + [self beginChunkFetcher:chunkFetcher + offset:offset]; + return; + } + } + } + + if (chunkData == nil) { + NSError *responseError = chunkError; + if (!responseError) { + responseError = [self uploadChunkUnavailableErrorWithDescription:@"chunkData is nil"]; + } + [self invokeFinalCallbackWithData:nil + error:responseError + shouldInvalidateLocation:YES]; + return; + } + + BOOL didWriteFile = NO; + if (isUploadingFileURL) { + // Make a temporary file with the data subset. + NSString *tempName = + [NSString stringWithFormat:@"GTMUpload_temp_%@", [[NSUUID UUID] UUIDString]]; + NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:tempName]; + NSError *writeError; + didWriteFile = [chunkData writeToFile:tempPath + options:NSDataWritingAtomic + error:&writeError]; + if (didWriteFile) { + chunkFetcher.bodyFileURL = [NSURL fileURLWithPath:tempPath]; + } else { + GTMSESSION_LOG_DEBUG(@"writeToFile failed: %@\n%@", writeError, tempPath); + } + } + if (!didWriteFile) { + chunkFetcher.bodyData = [chunkData copy]; + } + [self beginChunkFetcher:chunkFetcher + offset:offset]; + }); + }]; + } +} + +- (void)beginChunkFetcher:(GTMSessionFetcher *)chunkFetcher + offset:(int64_t)offset { + + // Track the current offset for progress reporting + self.currentOffset = offset; + + // Hang on to the fetcher in case we need to cancel it. We set these before beginning the + // chunk fetch so the observers notified of chunk fetches can inspect the upload fetcher to + // match to the chunk. + self.chunkFetcher = chunkFetcher; + self.fetcherInFlight = chunkFetcher; + + // Update the last chunk request, including any request headers. + self.lastChunkRequest = chunkFetcher.request; + + [chunkFetcher beginFetchWithDelegate:self + didFinishSelector:@selector(chunkFetcher:finishedWithData:error:)]; +} + +- (void)attachSendProgressBlockToChunkFetcher:(GTMSessionFetcher *)chunkFetcher { + chunkFetcher.sendProgressBlock = ^(int64_t bytesSent, int64_t totalBytesSent, + int64_t totalBytesExpectedToSend) { + // The total bytes expected include the initial body and the full chunked + // data, independent of how big this fetcher's chunk is. + int64_t initialBodySent = [self bodyLength]; // TODO(grobbins) use [self initialBodySent] + int64_t totalSent = initialBodySent + self.currentOffset + totalBytesSent; + int64_t totalExpected = initialBodySent + [self fullUploadLength]; + + [self invokeDelegateWithDidSendBytes:bytesSent + totalBytesSent:totalSent + totalBytesExpectedToSend:totalExpected]; + }; +} + +- (NSDictionary *)uploadSessionIdentifierMetadata { + NSMutableDictionary *metadata = [NSMutableDictionary dictionary]; + metadata[kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey] = @YES; + GTMSESSION_ASSERT_DEBUG(self.uploadFileURL, + @"Invalid upload fetcher to create session identifier for metadata"); + metadata[kGTMSessionIdentifierUploadFileURLMetadataKey] = [self.uploadFileURL absoluteString]; + metadata[kGTMSessionIdentifierUploadFileLengthMetadataKey] = @([self fullUploadLength]); + + if (self.uploadLocationURL) { + metadata[kGTMSessionIdentifierUploadLocationURLMetadataKey] = + [self.uploadLocationURL absoluteString]; + } + if (self.uploadMIMEType) { + metadata[kGTMSessionIdentifierUploadMIMETypeMetadataKey] = self.uploadMIMEType; + } + metadata[kGTMSessionIdentifierUploadChunkSizeMetadataKey] = @(self.chunkSize); + metadata[kGTMSessionIdentifierUploadCurrentOffsetMetadataKey] = @(self.currentOffset); + return metadata; +} + +- (GTMSessionFetcher *)uploadFetcherWithProperties:(NSDictionary *)properties + isQueryFetch:(BOOL)isQueryFetch { + GTMSessionCheckNotSynchronized(self); + + // Common code to make a request for a query command or for a chunk upload. + NSURL *uploadLocationURL = self.uploadLocationURL; + NSMutableURLRequest *chunkRequest = [NSMutableURLRequest requestWithURL:uploadLocationURL]; + [chunkRequest setHTTPMethod:@"PUT"]; + + // copy the user-agent from the original connection + // n.b. that self.request is nil for upload fetchers created with an existing upload location + // URL. + NSURLRequest *origRequest = self.request; + NSString *userAgent = [origRequest valueForHTTPHeaderField:@"User-Agent"]; + if (userAgent.length > 0) { + [chunkRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"]; + } + + [chunkRequest setValue:kGTMSessionXGoogUploadProtocolResumable + forHTTPHeaderField:kGTMSessionHeaderXGoogUploadProtocol]; + + // To avoid timeouts when debugging, copy the timeout of the initial fetcher. + NSTimeInterval origTimeout = [origRequest timeoutInterval]; + [chunkRequest setTimeoutInterval:origTimeout]; + + // + // Make a new chunk fetcher. + // + GTMSessionFetcher *chunkFetcher = [GTMSessionFetcher fetcherWithRequest:chunkRequest]; + chunkFetcher.callbackQueue = self.callbackQueue; + chunkFetcher.sessionUserInfo = self.sessionUserInfo; + chunkFetcher.configurationBlock = self.configurationBlock; + chunkFetcher.allowedInsecureSchemes = self.allowedInsecureSchemes; + chunkFetcher.allowLocalhostRequest = self.allowLocalhostRequest; + chunkFetcher.allowInvalidServerCertificates = self.allowInvalidServerCertificates; + chunkFetcher.useUploadTask = !isQueryFetch; + + if (self.uploadFileURL && !isQueryFetch && self.useBackgroundSession) { + [chunkFetcher createSessionIdentifierWithMetadata:[self uploadSessionIdentifierMetadata]]; + } + + // Give the chunk fetcher the same properties as the previous chunk fetcher + chunkFetcher.properties = [properties mutableCopy]; + [chunkFetcher setProperty:[NSValue valueWithNonretainedObject:self] + forKey:kGTMSessionUploadFetcherChunkParentKey]; + + // copy other fetcher settings to the new fetcher + chunkFetcher.retryEnabled = self.retryEnabled; + chunkFetcher.maxRetryInterval = self.maxRetryInterval; + + if ([self isRetryEnabled]) { + // We interpose our own retry method both so we can change the request to ask the server to + // tell us where to resume the chunk. + chunkFetcher.retryBlock = ^(BOOL suggestedWillRetry, NSError *chunkError, + GTMSessionFetcherRetryResponse response) { + void (^finish)(BOOL) = ^(BOOL shouldRetry){ + // We'll retry by sending an offset query. + if (shouldRetry) { + self.shouldInitiateOffsetQuery = !isQueryFetch; + + // We don't know what our actual offset is anymore, but the server will tell us. + self.currentOffset = 0; + } + // We don't actually want to retry this specific fetcher. + response(NO); + }; + + GTMSessionFetcherRetryBlock retryBlock = self.retryBlock; + if (retryBlock) { + // Ask the client, then call the finish block above. + retryBlock(suggestedWillRetry, chunkError, finish); + } else { + finish(suggestedWillRetry); + } + }; + } + + return chunkFetcher; +} + +- (void)chunkFetcher:(GTMSessionFetcher *)chunkFetcher + finishedWithData:(NSData *)data + error:(NSError *)error { + BOOL hasDestroyedOldChunkFetcher = NO; + self.fetcherInFlight = nil; + + NSDictionary *responseHeaders = [chunkFetcher responseHeaders]; + GTMSessionUploadFetcherStatus uploadStatus = + [[self class] uploadStatusFromResponseHeaders:responseHeaders]; + GTMSESSION_ASSERT_DEBUG(uploadStatus != kStatusUnknown + || error != nil + || self.wasCreatedFromBackgroundSession, + @"chunk fetcher completion has kStatusUnknown upload status for headers %@ fetcher %@", + responseHeaders, self); + BOOL isUploadStatusStopped = (uploadStatus == kStatusFinal || uploadStatus == kStatusCancelled); + + // Check if the fetcher was actually querying. If it failed, do not retry, + // as it would enter an infinite retry loop. + NSString *uploadCommand = + chunkFetcher.request.allHTTPHeaderFields[kGTMSessionHeaderXGoogUploadCommand]; + BOOL isQueryFetch = [uploadCommand isEqual:@"query"]; + + // TODO + // Maybe here we can check to see if the request had x goog content length set. (the file length one). + int64_t previousContentLength = + [[chunkFetcher.request valueForHTTPHeaderField:@"Content-Length"] longLongValue]; + // The Content-Length header may not be present if the chunk fetcher was recreated from + // a background session. + BOOL hasKnownChunkSize = (previousContentLength > 0); + BOOL needsQuery = (!hasKnownChunkSize && !isUploadStatusStopped); + + if (error || (needsQuery && !isQueryFetch)) { + NSInteger status = error.code; + + // Status 4xx indicates a bad offset in the Google upload protocol. However, do not retry status + // 404 per spec, nor if the upload size appears to have been zero (since the server will just + // keep asking us to retry.) + if (self.shouldInitiateOffsetQuery || + (needsQuery && !isQueryFetch) || + ([error.domain isEqual:kGTMSessionFetcherStatusDomain] && + status >= 400 && status <= 499 && + status != 404 && + uploadStatus == kStatusActive && + previousContentLength > 0)) { + self.shouldInitiateOffsetQuery = NO; + [self destroyChunkFetcher]; + hasDestroyedOldChunkFetcher = YES; + [self sendQueryForUploadOffsetWithFetcherProperties:chunkFetcher.properties]; + } else { + // Some unexpected status has occurred; handle it as we would a regular + // object fetcher failure. + [self invokeFinalCallbackWithData:data + error:error + shouldInvalidateLocation:NO]; + } + } else { + // The chunk has uploaded successfully. + int64_t newOffset = self.currentOffset + previousContentLength; +#if DEBUG + // Verify that if we think all of the uploading data has been sent, the server responded with + // the "final" upload status. + BOOL hasUploadAllData = (newOffset == [self fullUploadLength]); + BOOL isFinalStatus = (uploadStatus == kStatusFinal); + #pragma unused(hasUploadAllData,isFinalStatus) + GTMSESSION_ASSERT_DEBUG(hasUploadAllData == isFinalStatus || !hasKnownChunkSize, + @"uploadStatus:%@ newOffset:%lld (%lld + %lld) fullUploadLength:%lld" + @" chunkFetcher:%@ requestHeaders:%@ responseHeaders:%@", + [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadStatus], + newOffset, self.currentOffset, previousContentLength, + [self fullUploadLength], + chunkFetcher, chunkFetcher.request.allHTTPHeaderFields, + responseHeaders); +#endif + if (isUploadStatusStopped || (_currentOffset > _uploadFileLength && _uploadFileLength > 0)) { + // This was the last chunk. + if (error == nil && uploadStatus == kStatusCancelled) { + // Report cancelled status as an error. + NSDictionary *userInfo = nil; + if (data.length > 0) { + userInfo = @{ kGTMSessionFetcherStatusDataKey : data }; + } + data = nil; + error = [self prematureFailureErrorWithUserInfo:userInfo]; + } else { + // The upload is in final status. + // + // Take the chunk fetcher's data as the superclass data. + self.downloadedData = data; + self.statusCode = chunkFetcher.statusCode; + } + + // we're done + [self invokeFinalCallbackWithData:data + error:error + shouldInvalidateLocation:YES]; + } else { + // Start the next chunk. + self.currentOffset = newOffset; + + // We want to destroy this chunk fetcher before creating the next one, but + // we want to pass on its properties + NSDictionary *props = [chunkFetcher properties]; + + // We no longer need to be able to cancel this chunkFetcher. Destroy it + // before we create a new chunk fetcher. + [self destroyChunkFetcher]; + hasDestroyedOldChunkFetcher = YES; + + [self uploadNextChunkWithOffset:newOffset + fetcherProperties:props]; + } + } + if (!hasDestroyedOldChunkFetcher) { + [self destroyChunkFetcher]; + } +} + +- (void)destroyChunkFetcher { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (_fetcherInFlight == _chunkFetcher) { + _fetcherInFlight = nil; + } + + [_chunkFetcher stopFetching]; + + NSURL *chunkFileURL = _chunkFetcher.bodyFileURL; + BOOL wasTemporaryUploadFile = ![chunkFileURL isEqual:_uploadFileURL]; + if (wasTemporaryUploadFile) { + NSError *error; + [[NSFileManager defaultManager] removeItemAtURL:chunkFileURL + error:&error]; + if (error) { + GTMSESSION_LOG_DEBUG(@"removingItemAtURL failed: %@\n%@", error, chunkFileURL); + } + } + + _recentChunkReponseHeaders = _chunkFetcher.responseHeaders; + + // To avoid retain cycles, remove all properties except the parent identifier. + _chunkFetcher.properties = + @{ kGTMSessionUploadFetcherChunkParentKey : [NSValue valueWithNonretainedObject:self] }; + + _chunkFetcher.retryBlock = nil; + _chunkFetcher.sendProgressBlock = nil; + _chunkFetcher = nil; + } // @synchronized(self) +} + +// This method calculates the proper values to pass to the client's send progress block. +// +// The actual total bytes sent include the initial body sent, plus the +// offset into the batched data prior to the current chunk fetcher + +- (void)invokeDelegateWithDidSendBytes:(int64_t)bytesSent + totalBytesSent:(int64_t)totalBytesSent + totalBytesExpectedToSend:(int64_t)totalBytesExpected { + GTMSessionCheckNotSynchronized(self); + + // Ensure the chunk fetcher survives the callback in case the user pauses the upload process. + __block GTMSessionFetcher *holdFetcher = self.chunkFetcher; + + [self invokeOnCallbackQueue:self.delegateCallbackQueue + afterUserStopped:NO + block:^{ + GTMSessionFetcherSendProgressBlock sendProgressBlock = self.sendProgressBlock; + if (sendProgressBlock) { + sendProgressBlock(bytesSent, totalBytesSent, totalBytesExpected); + } + holdFetcher = nil; + }]; +} + +- (void)retrieveUploadChunkGranularityFromResponseHeaders:(NSDictionary *)responseHeaders { + GTMSessionCheckNotSynchronized(self); + + // Standard granularity for Google uploads is 256K. + NSString *chunkGranularityHeader = + [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadChunkGranularity]; + self.uploadGranularity = chunkGranularityHeader.longLongValue; +} + +#pragma mark - + +- (BOOL)isPaused { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _isPaused; + } // @synchronized(self) +} + +- (void)pauseFetching { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _isPaused = YES; + } // @synchronized(self) + + // Pausing just means stopping the current chunk from uploading; + // when we resume, we will send a query request to the server to + // figure out what bytes to resume sending. + // + // We won't try to cancel the initial data upload, but rather will check + // for being paused in beginChunkFetches. + [self destroyChunkFetcher]; +} + +- (void)resumeFetching { + BOOL wasPaused; + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + wasPaused = _isPaused; + _isPaused = NO; + } // @synchronized(self) + + if (wasPaused) { + [self sendQueryForUploadOffsetWithFetcherProperties:self.properties]; + } +} + +- (void)stopFetching { + // Overrides the superclass + [self destroyChunkFetcher]; + + // If we think the server is waiting for more data, then tell it there won't be more. + if (self.uploadLocationURL) { + [self sendCancelUploadWithFetcherProperties:[self properties]]; + self.uploadLocationURL = nil; + } else { + [self invokeOnCallbackQueue:self.callbackQueue + afterUserStopped:YES + block:^{ + // Repeated calls to stopFetching may cause this path to be reached despite having sent a real + // cancel request, check here to ensure that the cancellation handler invocation which fires + // will definitely be for the real request sent previously. + @synchronized(self) { + if (self->_isCancelInFlight) { + return; + } + } + [self triggerCancellationHandlerForFetch:nil data:nil error:nil]; + }]; + } + + [super stopFetching]; +} + +// Fires the cancellation handler, returning whether there was a handler to be fired. +- (BOOL)triggerCancellationHandlerForFetch:(GTMSessionFetcher *)fetcher + data:(NSData *)data + error:(NSError *)error { + GTMSessionUploadFetcherCancellationHandler handler = self.cancellationHandler; + if (handler) { + handler(fetcher, data, error); + self.cancellationHandler = nil; + return YES; + } + return NO; +} + +#pragma mark - + +- (int64_t)updateChunkFetcher:(GTMSessionFetcher *)chunkFetcher + forChunkAtOffset:(int64_t)offset { + BOOL isUploadingFileURL = (self.uploadFileURL != nil); + + // Upload another chunk, meeting server-required granularity. + int64_t chunkSize = self.chunkSize; + + int64_t fullUploadLength = [self fullUploadLength]; + BOOL isFileLengthKnown = fullUploadLength >= 0; + + BOOL isUploadingFullFile = (offset == 0 && isFileLengthKnown && chunkSize >= fullUploadLength); + if (!isUploadingFileURL || !isUploadingFullFile) { + // We're not uploading the entire file and given the file URL. Since we'll be + // allocating a subdata block for a chunk, we need to bound it to something that + // won't blow the process's memory. + if (chunkSize > kGTMSessionUploadFetcherMaximumDemandBufferSize) { + chunkSize = kGTMSessionUploadFetcherMaximumDemandBufferSize; + } + } + + int64_t granularity = self.uploadGranularity; + if (granularity > 0) { + if (chunkSize < granularity) { + chunkSize = granularity; + } else { + chunkSize = chunkSize - (chunkSize % granularity); + } + } + + GTMSESSION_ASSERT_DEBUG(offset < fullUploadLength || fullUploadLength == 0, + @"offset %lld exceeds data length %lld", offset, fullUploadLength); + + if (granularity > 0) { + offset = offset - (offset % granularity); + } + + // If the chunk size is bigger than the remaining data, or else + // it's close enough in size to the remaining data that we'd rather + // avoid having a whole extra http fetch for the leftover bit, then make + // this chunk size exactly match the remaining data size + NSString *command; + int64_t thisChunkSize = chunkSize; + + BOOL isChunkTooBig = (thisChunkSize >= (fullUploadLength - offset)); + BOOL isChunkAlmostBigEnough = (fullUploadLength - offset - 2500 < thisChunkSize); + BOOL isFinalChunk = (isChunkTooBig || isChunkAlmostBigEnough) && isFileLengthKnown; + if (isFinalChunk) { + thisChunkSize = fullUploadLength - offset; + if (thisChunkSize > 0) { + command = @"upload, finalize"; + } else { + command = @"finalize"; + } + } else { + command = @"upload"; + } + NSString *lengthStr = @(thisChunkSize).stringValue; + NSString *offsetStr = @(offset).stringValue; + + [chunkFetcher setRequestValue:command forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand]; + [chunkFetcher setRequestValue:lengthStr forHTTPHeaderField:@"Content-Length"]; + [chunkFetcher setRequestValue:offsetStr forHTTPHeaderField:kGTMSessionHeaderXGoogUploadOffset]; + if (_uploadFileLength != kGTMSessionUploadFetcherUnknownFileSize) { + [chunkFetcher setRequestValue:@([self fullUploadLength]).stringValue + forHTTPHeaderField:kGTMSessionHeaderXGoogUploadContentLength]; + } + + // Append the range of bytes in this chunk to the fetcher comment. + NSString *baseComment = self.comment; + [chunkFetcher setCommentWithFormat:@"%@ (%lld-%lld)", + baseComment ? baseComment : @"upload", offset, MAX(0, offset + thisChunkSize - 1)]; + + return thisChunkSize; +} + +// Public properties. +@synthesize currentOffset = _currentOffset, + delegateCompletionHandler = _delegateCompletionHandler, + chunkFetcher = _chunkFetcher, + lastChunkRequest = _lastChunkRequest, + subdataGenerating = _subdataGenerating, + shouldInitiateOffsetQuery = _shouldInitiateOffsetQuery, + uploadGranularity = _uploadGranularity; + +// Internal properties. +@dynamic fetcherInFlight; +@dynamic activeFetcher; +@dynamic statusCode; +@dynamic delegateCallbackQueue; + ++ (void)removePointer:(void *)pointer fromPointerArray:(NSPointerArray *)pointerArray { + for (NSUInteger index = 0, count = pointerArray.count; index < count; ++index) { + void *pointerAtIndex = [pointerArray pointerAtIndex:index]; + if (pointerAtIndex == pointer) { + [pointerArray removePointerAtIndex:index]; + return; + } + } +} + +- (BOOL)useBackgroundSession { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _useBackgroundSessionOnChunkFetchers; + } // @synchronized(self +} + +- (void)setUseBackgroundSession:(BOOL)useBackgroundSession { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (_useBackgroundSessionOnChunkFetchers != useBackgroundSession) { + _useBackgroundSessionOnChunkFetchers = useBackgroundSession; + NSPointerArray *uploadFetcherPointerArrayForBackgroundSessions = + [[self class] uploadFetcherPointerArrayForBackgroundSessions]; + if (_useBackgroundSessionOnChunkFetchers) { + [uploadFetcherPointerArrayForBackgroundSessions addPointer:(__bridge void *)self]; + } else { + [[self class] removePointer:(__bridge void *)self + fromPointerArray:uploadFetcherPointerArrayForBackgroundSessions]; + } + } + } // @synchronized(self +} + +- (BOOL)canFetchWithBackgroundSession { + // The initial upload fetcher is always a foreground session; the + // useBackgroundSession property will apply only to chunk fetchers, + // not to queries. + return NO; +} + +- (NSDictionary *)responseHeaders { + GTMSessionCheckNotSynchronized(self); + // Overrides the superclass + + // If asked for the fetcher's response, use the most recent chunk fetcher's response, + // since the original request's response lacks useful information like the actual + // Content-Type. + NSDictionary *dict = self.chunkFetcher.responseHeaders; + if (dict) { + return dict; + } + + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + if (_recentChunkReponseHeaders) { + return _recentChunkReponseHeaders; + } + } // @synchronized(self + + // No chunk fetcher yet completed, so return whatever we have from the initial fetch. + return [super responseHeaders]; +} + +- (NSInteger)statusCodeUnsynchronized { + GTMSessionCheckSynchronized(self); + + if (_recentChunkStatusCode != -1) { + // Overrides the superclass to indicate status appropriate to the initial + // or latest chunk fetch + return _recentChunkStatusCode; + } else { + return [super statusCodeUnsynchronized]; + } +} + + +- (void)setStatusCode:(NSInteger)val { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _recentChunkStatusCode = val; + } +} + +- (int64_t)initialBodyLength { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _initialBodyLength; + } +} + +- (void)setInitialBodyLength:(int64_t)length { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _initialBodyLength = length; + } +} + +- (int64_t)initialBodySent { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _initialBodySent; + } +} + +- (void)setInitialBodySent:(int64_t)length { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _initialBodySent = length; + } +} + +- (NSURL *)uploadLocationURL { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + return _uploadLocationURL; + } +} + +- (void)setUploadLocationURL:(NSURL *)locationURL { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + + _uploadLocationURL = locationURL; + } +} + +- (GTMSessionFetcher *)activeFetcher { + GTMSessionFetcher *result = self.fetcherInFlight; + if (result) return result; + + return self; +} + +- (BOOL)isFetching { + // If there is an active chunk fetcher, then the upload fetcher is considered + // to still be fetching. + if (self.fetcherInFlight != nil) return YES; + + return [super isFetching]; +} + +- (BOOL)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds { + NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds]; + + while (self.fetcherInFlight || self.subdataGenerating) { + if ([timeoutDate timeIntervalSinceNow] < 0) return NO; + + if (self.subdataGenerating) { + // Allow time for subdata generation. + NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:0.001]; + [[NSRunLoop currentRunLoop] runUntilDate:stopDate]; + } else { + // Wait for any chunk or query fetchers that still have pending callbacks or + // notifications. + BOOL timedOut; + + if (self.fetcherInFlight == self) { + timedOut = ![super waitForCompletionWithTimeout:timeoutInSeconds]; + } else { + timedOut = ![self.fetcherInFlight waitForCompletionWithTimeout:timeoutInSeconds]; + } + if (timedOut) return NO; + } + } + return YES; +} + +@end + +@implementation GTMSessionFetcher (GTMSessionUploadFetcherMethods) + +- (GTMSessionUploadFetcher *)parentUploadFetcher { + NSValue *property = [self propertyForKey:kGTMSessionUploadFetcherChunkParentKey]; + if (!property) return nil; + + GTMSessionUploadFetcher *uploadFetcher = property.nonretainedObjectValue; + + GTMSESSION_ASSERT_DEBUG([uploadFetcher isKindOfClass:[GTMSessionUploadFetcher class]], + @"Unexpected parent upload fetcher class: %@", [uploadFetcher class]); + return uploadFetcher; +} + +@end diff --git a/Pods/GoogleAPIClientForREST/LICENSE b/Pods/GoogleAPIClientForREST/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Pods/GoogleAPIClientForREST/README.md b/Pods/GoogleAPIClientForREST/README.md @@ -0,0 +1,48 @@ +# Google APIs Client Library for Objective-C for REST # + +**Project site** <https://github.com/google/google-api-objectivec-client-for-rest><br> +**Discussion group** <http://groups.google.com/group/google-api-objectivec-client> + +[![Build Status](https://travis-ci.org/google/google-api-objectivec-client-for-rest.svg?branch=master)](https://travis-ci.org/google/google-api-objectivec-client-for-rest) + +Written by Google, this library is a flexible and efficient Objective-C +framework for accessing JSON APIs. + +This is the recommended library for accessing JSON-based Google APIs for iOS and +Mac OS X applications. The library is compatible with applications built for +iOS 7 and later, and Mac OS X 10.9 and later. + +**To get started** with Google APIs and the Objective-C client library, Read the +[wiki](https://github.com/google/google-api-objectivec-client-for-rest/wiki). +See +[BuildingTheLibrary](https://github.com/google/google-api-objectivec-client-for-rest/wiki/BuildingTheLibrary) +for how to add the library to a Mac or iPhone application project, it covers +directly adding sources or using CocoaPods. Study the +[example applications](https://github.com/google/google-api-objectivec-client-for-rest/tree/master/Examples). + +Generated interfaces for Google APIs are in the +[GeneratedServices folder](https://github.com/google/google-api-objectivec-client-for-rest/tree/master/Source/GeneratedServices). + +In addition to the pre generated classes included with the library, you can +generate your own source for other services that have a +[discovery document](https://developers.google.com/discovery/v1/reference/apis#resource-representations) +by using the +[ServiceGenerator](https://github.com/google/google-api-objectivec-client-for-rest/wiki/ServiceGenerator). + +**If you have a problem** or want a new feature to be included in the library, +please join the +[discussion group](http://groups.google.com/group/google-api-objectivec-client). +Be sure to include +[http logs](https://github.com/google/google-api-objectivec-client-for-rest/wiki#logging-http-server-traffic) +for requests and responses when posting questions. Bugs may also be submitted +on the [issues list](https://github.com/google/google-api-objectivec-client-for-rest/issues). + +**Externally-included projects**: The library includes code from the separate +projects [GTM Session Fetcher](https://github.com/google/gtm-session-fetcher), +[GTMAppAuth](https://github.com/google/GTMAppAuth). + +**Google Data APIs**: The much older library for XML-based APIs is +[still available](https://github.com/google/gdata-objectivec-client). + +Other useful classes for Mac and iOS developers are available in the +[Google Toolbox for Mac](https://github.com/google/google-toolbox-for-mac). diff --git a/Pods/GoogleAPIClientForREST/Source/GTLRDefines.h b/Pods/GoogleAPIClientForREST/Source/GTLRDefines.h @@ -0,0 +1,109 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// GTLRDefines.h +// + +// Ensure Apple's conditionals we depend on are defined. +#import <TargetConditionals.h> +#import <AvailabilityMacros.h> + +// These can be redefined via a prefix if you are prefixing symbols to prefix +// the names used in strings. Something like: +// #define _HELPER(x) "MyPrefix" #x +// #define GTLR_CLASSNAME_STR(x) @_HELPER(x) +// #define GTLR_CLASSNAME_CSTR(x) _HELPER(x) +#ifndef GTLR_CLASSNAME_STR + #define _GTLR_CLASSNAME_HELPER(x) #x + #define GTLR_CLASSNAME_STR(x) @_GTLR_CLASSNAME_HELPER(x) + #define GTLR_CLASSNAME_CSTR(x) _GTLR_CLASSNAME_HELPER(x) +#endif + +// Provide a common definition for externing constants/functions +#if defined(__cplusplus) + #define GTLR_EXTERN extern "C" +#else + #define GTLR_EXTERN extern +#endif + +// +// GTLR_ASSERT defaults to bridging to NSAssert. This macro exists just in case +// it needs to be remapped. +// GTLR_DEBUG_ASSERT is similar, but compiles in only for debug builds +// + +#ifndef GTLR_ASSERT + // NSCAssert to avoid capturing self if used in a block. + #define GTLR_ASSERT(condition, ...) NSCAssert(condition, __VA_ARGS__) +#endif // GTLR_ASSERT + +#ifndef GTLR_DEBUG_ASSERT + #if DEBUG && !defined(NS_BLOCK_ASSERTIONS) + #define GTLR_DEBUG_ASSERT(condition, ...) GTLR_ASSERT(condition, __VA_ARGS__) + #elif DEBUG + // In DEBUG builds with assertions blocked, log to avoid unused variable warnings. + #define GTLR_DEBUG_ASSERT(condition, ...) if (!(condition)) { NSLog(__VA_ARGS__); } + #else + #define GTLR_DEBUG_ASSERT(condition, ...) do { } while (0) + #endif +#endif + +#ifndef GTLR_DEBUG_LOG + #if DEBUG + #define GTLR_DEBUG_LOG(...) NSLog(__VA_ARGS__) + #else + #define GTLR_DEBUG_LOG(...) do { } while (0) + #endif +#endif + +#ifndef GTLR_DEBUG_ASSERT_CURRENT_QUEUE + #define GTLR_ASSERT_CURRENT_QUEUE_DEBUG(targetQueue) \ + GTLR_DEBUG_ASSERT(0 == strcmp(GTLR_QUEUE_NAME(targetQueue), \ + GTLR_QUEUE_NAME(DISPATCH_CURRENT_QUEUE_LABEL)), \ + @"Current queue is %s (expected %s)", \ + GTLR_QUEUE_NAME(DISPATCH_CURRENT_QUEUE_LABEL), \ + GTLR_QUEUE_NAME(targetQueue)) + + #define GTLR_QUEUE_NAME(queue) \ + (strlen(dispatch_queue_get_label(queue)) > 0 ? dispatch_queue_get_label(queue) : "unnamed") +#endif // GTLR_ASSERT_CURRENT_QUEUE_DEBUG + +// Sanity check the min versions. + +#if (defined(TARGET_OS_TV) && TARGET_OS_TV) || (defined(TARGET_OS_WATCH) && TARGET_OS_WATCH) + // No min checks for these two. +#elif TARGET_OS_IPHONE + #if !defined(__IPHONE_9_0) || (__IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_9_0) + #error "This project expects to be compiled with the iOS 9.0 SDK (or later)." + #endif + #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0 + #error "The minimum supported iOS version is 7.0." + #endif +#elif TARGET_OS_MAC + #if !defined(MAC_OS_X_VERSION_10_10) || (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_10) + #error "This project expects to be compiled with the OS X 10.10 SDK (or later)." + #endif + #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_9 + #error "The minimum supported OS X version is 10.9." + #endif +#else + #error "Unknown target platform." +#endif + +// Version marker used to validate the generated sources against the library +// version. The will be changed any time the library makes a change that means +// past sources need to be regenerated. +#define GTLR_RUNTIME_VERSION 3000 diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRBatchQuery.h b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRBatchQuery.h @@ -0,0 +1,85 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Batch query documentation: +// https://github.com/google/google-api-objectivec-client-for-rest/wiki#batch-operations + +#import "GTLRQuery.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface GTLRBatchQuery : NSObject <GTLRQueryProtocol> + +/** + * Queries included in this batch. Each query should have a unique @c requestID. + */ +@property(atomic, copy, nullable) NSArray<GTLRQuery *> *queries; + +/** + * Flag indicating if query execution should skip authorization. Defaults to NO. + */ +@property(atomic, assign) BOOL shouldSkipAuthorization; + +/** + * Any additional HTTP headers for this batch. + * + * These headers override the same keys from the service object's + * @c additionalHTTPHeaders. + */ +@property(atomic, copy, nullable) NSDictionary<NSString *, NSString *> *additionalHTTPHeaders; + +/** + * Any additional URL query parameters to add to the batch query. + * + * These query parameters override the same keys from the service object's + * @c additionalURLQueryParameters + */ +@property(atomic, copy, nullable) NSDictionary<NSString *, NSString *> *additionalURLQueryParameters; + +/** + * The batch request multipart boundary, once determined. + */ +@property(atomic, copy, nullable) NSString *boundary; + +/** + * The brief string to identify this query in @c GTMSessionFetcher http logs. + * + * The default logging name for batch requests includes the API method names. + */ +@property(atomic, copy, nullable) NSString *loggingName; + +/** + * Constructor for a batch query, for use with @c addQuery: + */ ++ (instancetype)batchQuery; + +/** + * Constructor for a batch query, from an array of @c GTLRQuery objects. + */ ++ (instancetype)batchQueryWithQueries:(NSArray<GTLRQuery *> *)array; + +/** + * Add a single @c GTLRQuery to the batch. + */ +- (void)addQuery:(GTLRQuery *)query; + +/** + * Search the batch for a query with the specified ID. + */ +- (nullable GTLRQuery *)queryForRequestID:(NSString *)requestID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRBatchQuery.m b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRBatchQuery.m @@ -0,0 +1,179 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !__has_feature(objc_arc) +#error "This file needs to be compiled with ARC enabled." +#endif + +#import "GTLRBatchQuery.h" + +#import "GTLRService.h" + +#if DEBUG +static void DebugAssertValidBatchQueryItem(GTLRQuery *query) { + GTLR_DEBUG_ASSERT([query isKindOfClass:[GTLRQuery class]], + @"unexpected query class: %@", [query class]); + GTLR_DEBUG_ASSERT(query.uploadParameters == nil, + @"batch may not contain upload: %@", query); + GTLR_DEBUG_ASSERT(!query.hasExecutionParameters, + @"queries added to a batch may not contain executionParameters: %@", query); + GTLR_DEBUG_ASSERT(!query.queryInvalid, + @"batch may not contain query already executed: %@", query); +} +#else +static void DebugAssertValidBatchQueryItem(GTLRQuery *query) { } +#endif + +@implementation GTLRBatchQuery { + NSMutableArray<GTLRQuery *> *_queries; + NSMutableDictionary *_requestIDMap; + GTLRServiceExecutionParameters *_executionParameters; +} + +@synthesize shouldSkipAuthorization = _shouldSkipAuthorization, + additionalHTTPHeaders = _additionalHTTPHeaders, + additionalURLQueryParameters = _additionalURLQueryParameters, + boundary = _boundary, + loggingName = _loggingName; + ++ (instancetype)batchQuery { + GTLRBatchQuery *obj = [[self alloc] init]; + return obj; +} + ++ (instancetype)batchQueryWithQueries:(NSArray<GTLRQuery *> *)queries { + GTLRBatchQuery *obj = [self batchQuery]; + obj.queries = queries; + +#if DEBUG + for (GTLRQuery *query in queries) { + DebugAssertValidBatchQueryItem(query); + } +#endif + return obj; +} + +- (id)copyWithZone:(NSZone *)zone { + // Deep copy the list of queries + GTLRBatchQuery *newBatch = [[[self class] allocWithZone:zone] init]; + if (_queries) { + newBatch.queries = [[NSArray alloc] initWithArray:_queries + copyItems:YES]; + } + + // Using the executionParameters ivar avoids creating the object. + newBatch.executionParameters = _executionParameters; + + // Copied in the same order as synthesized above. + newBatch.shouldSkipAuthorization = _shouldSkipAuthorization; + newBatch.additionalHTTPHeaders = _additionalHTTPHeaders; + newBatch.additionalURLQueryParameters = _additionalURLQueryParameters; + newBatch.boundary = _boundary; + newBatch.loggingName = _loggingName; + + // No need to copy _requestIDMap as it's created on demand. + return newBatch; +} + +- (NSString *)description { + NSArray *queries = self.queries; + NSArray *loggingNames = [queries valueForKey:@"loggingName"]; + NSMutableSet *dedupedNames = [NSMutableSet setWithArray:loggingNames]; // de-dupe + [dedupedNames removeObject:[NSNull null]]; // In case any didn't have a loggingName. + NSString *namesStr = [[dedupedNames allObjects] componentsJoinedByString:@","]; + + return [NSString stringWithFormat:@"%@ %p (queries:%tu - %@)", + [self class], self, queries.count, namesStr]; +} + +#pragma mark - + +- (BOOL)isBatchQuery { + return YES; +} + +- (GTLRUploadParameters *)uploadParameters { + // File upload is not supported for batches + return nil; +} + +- (void)invalidateQuery { + NSArray *queries = self.queries; + [queries makeObjectsPerformSelector:@selector(invalidateQuery)]; + + _executionParameters = nil; +} + +- (GTLRQuery *)queryForRequestID:(NSString *)requestID { + GTLRQuery *result = [_requestIDMap objectForKey:requestID]; + if (result) return result; + + // We've not before tried to look up a query, or the map is stale + _requestIDMap = [[NSMutableDictionary alloc] init]; + + for (GTLRQuery *query in _queries) { + [_requestIDMap setObject:query forKey:query.requestID]; + } + + result = [_requestIDMap objectForKey:requestID]; + return result; +} + +#pragma mark - + +- (void)setQueries:(NSArray<GTLRQuery *> *)array { +#if DEBUG + for (GTLRQuery *query in array) { + DebugAssertValidBatchQueryItem(query); + } +#endif + + _queries = [array mutableCopy]; +} + +- (NSArray<GTLRQuery *> *)queries { + return _queries; +} + +- (void)addQuery:(GTLRQuery *)query { + DebugAssertValidBatchQueryItem(query); + + if (_queries == nil) { + _queries = [[NSMutableArray alloc] init]; + } + + [_queries addObject:query]; +} + +- (GTLRServiceExecutionParameters *)executionParameters { + @synchronized(self) { + if (!_executionParameters) { + _executionParameters = [[GTLRServiceExecutionParameters alloc] init]; + } + } + return _executionParameters; +} + +- (void)setExecutionParameters:(GTLRServiceExecutionParameters *)executionParameters { + @synchronized(self) { + _executionParameters = executionParameters; + } +} + +- (BOOL)hasExecutionParameters { + return _executionParameters.hasParameters; +} + +@end diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRBatchResult.h b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRBatchResult.h @@ -0,0 +1,78 @@ +/* Copyright (c) 2011 Google Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#import "GTLRObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@class GTLRErrorObject; + +/** + * A batch result includes a dictionary of successes, a dictionary of failures, and a dictionary of + * HTTP response headers. + * + * Dictionary keys are request ID strings; dictionary values are @c GTLRObject for + * successes, @c GTLRErrorObject for failures, @c NSDictionary for responseHeaders. + * + * For successes with no returned object (such as from delete operations), + * the object for the dictionary entry is @c NSNull. + * + * The original query for each result is available from the service ticket, as shown in + * the code snippet here. + * + * When the queries in the batch are unrelated, adding a @c completionBlock to each of + * the queries may be a simpler way to handle the batch results. + * + * @code + * NSDictionary *successes = batchResults.successes; + * for (NSString *requestID in successes) { + * GTLRObject *obj = successes[requestID]; + * GTLRQuery *query = [ticket queryForRequestID:requestID]; + * NSLog(@"Query %@ returned object %@", query, obj); + * } + * + * NSDictionary *failures = batchResults.failures; + * for (NSString *requestID in failures) { + * GTLRErrorObject *errorObj = failures[requestID]; + * GTLRQuery *query = [ticket queryForRequestID:requestID]; + * NSLog(@"Query %@ failed with error %@", query, errorObj); + * } + * @endcode + */ +@interface GTLRBatchResult : GTLRObject + +/** + * Object results of successful queries in the batch, keyed by request ID. + * + * Queries which do not return an object when successful have a @c NSNull value. + */ +@property(atomic, strong, nullable) NSDictionary<NSString *, __kindof GTLRObject *> *successes; + +/** + * Object results of unsuccessful queries in the batch, keyed by request ID. + */ +@property(atomic, strong, nullable) NSDictionary<NSString *, GTLRErrorObject *> *failures; + +/** + * Any HTTP response headers that were returned for a query request. Headers are optional therefore + * not all queries will have them. Query request with response headers are stored in a + * dictionary and keyed by request ID. + */ +@property(atomic, strong, nullable) + NSDictionary<NSString *, NSDictionary *> *responseHeaders; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRBatchResult.m b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRBatchResult.m @@ -0,0 +1,168 @@ +/* Copyright (c) 2011 Google Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#if !__has_feature(objc_arc) +#error "This file needs to be compiled with ARC enabled." +#endif + +#import "GTLRBatchResult.h" + +#import "GTLRErrorObject.h" +#import "GTLRUtilities.h" + +static NSString *const kGTLRBatchResultSuccessesKeys = @"successesKeys"; +static NSString *const kGTLRBatchResultSuccessKeyPrefix = @"Success-"; +static NSString *const kGTLRBatchResultFailuresKeys = @"failuresKeys"; +static NSString *const kGTLRBatchResultFailurKeyPrefix = @"Failure-"; +static NSString *const kGTLRBatchResultResponseHeaders = @"responseHeaders"; + +@implementation GTLRBatchResult + +@synthesize successes = _successes, + failures = _failures, + responseHeaders = _responseHeaders; + +// Since this class doesn't use the json property, provide the basic NSObject +// methods needed to ensure proper behaviors. + +- (id)copyWithZone:(NSZone *)zone { + GTLRBatchResult* newObject = [super copyWithZone:zone]; + newObject.successes = [self.successes copyWithZone:zone]; + newObject.failures = [self.failures copyWithZone:zone]; + newObject.responseHeaders = [self.responseHeaders copyWithZone:zone]; + return newObject; +} + +- (NSUInteger)hash { + NSUInteger result = [super hash]; + result += result * 13 + [self.successes hash]; + result += result * 13 + [self.failures hash]; + result += result * 13 + [self.responseHeaders hash]; + return result; +} + +- (BOOL)isEqual:(id)object { + if (self == object) return YES; + + if (![super isEqual:object]) { + return NO; + } + + if (![object isKindOfClass:[GTLRBatchResult class]]) { + return NO; + } + + GTLRBatchResult *other = (GTLRBatchResult *)object; + if (!GTLR_AreEqualOrBothNil(self.successes, other.successes)) { + return NO; + } + if (!GTLR_AreEqualOrBothNil(self.failures, other.failures)) { + return NO; + } + return GTLR_AreEqualOrBothNil(self.responseHeaders, other.responseHeaders); +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ %p (successes:%tu failures:%tu responseHeaders:%tu)", + [self class], self, + self.successes.count, self.failures.count, self.responseHeaders.count]; +} + +// This class is a subclass of GTLRObject, which declares NSSecureCoding +// conformance. Since this class does't really use the json property, provide +// a custom implementation to maintain the contract. +// +// For success/failures, one could do: +// [encoder encodeObject:self.successes forKey:kGTLRBatchResultSuccesses]; +// [encoder encodeObject:self.failures forKey:kGTLRBatchResultFailuresKeys]; +// and then use -decodeObjectOfClasses:forKey:, but nothing actually checks the +// structure of the dictionary, so instead the dicts are blown out to provide +// better validation by the encoder/decoder. + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (instancetype)initWithCoder:(NSCoder *)decoder { + self = [super initWithCoder:decoder]; + if (self) { + NSArray<NSString *> *keys = + [decoder decodeObjectOfClass:[NSArray class] + forKey:kGTLRBatchResultSuccessesKeys]; + if (keys.count) { + NSMutableDictionary *dict = + [NSMutableDictionary dictionaryWithCapacity:keys.count]; + for (NSString *key in keys) { + NSString *storageKey = + [kGTLRBatchResultSuccessKeyPrefix stringByAppendingString:key]; + GTLRObject *obj = [decoder decodeObjectOfClass:[GTLRObject class] + forKey:storageKey]; + if (obj) { + [dict setObject:obj forKey:key]; + } + } + self.successes = dict; + } + + keys = [decoder decodeObjectOfClass:[NSArray class] + forKey:kGTLRBatchResultFailuresKeys]; + if (keys.count) { + NSMutableDictionary *dict = + [NSMutableDictionary dictionaryWithCapacity:keys.count]; + for (NSString *key in keys) { + NSString *storageKey = + [kGTLRBatchResultFailurKeyPrefix stringByAppendingString:key]; + GTLRObject *obj = [decoder decodeObjectOfClass:[GTLRObject class] + forKey:storageKey]; + if (obj) { + [dict setObject:obj forKey:key]; + } + } + self.failures = dict; + } + + self.responseHeaders = + [decoder decodeObjectOfClass:[NSDictionary class] + forKey:kGTLRBatchResultResponseHeaders]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)encoder { + [super encodeWithCoder:encoder]; + [encoder encodeObject:self.successes.allKeys + forKey:kGTLRBatchResultSuccessesKeys]; + [self.successes enumerateKeysAndObjectsUsingBlock:^(NSString *key, + GTLRObject * obj, + BOOL * stop) { + NSString *storageKey = + [kGTLRBatchResultSuccessKeyPrefix stringByAppendingString:key]; + [encoder encodeObject:obj forKey:storageKey]; + }]; + + [encoder encodeObject:self.failures.allKeys forKey:kGTLRBatchResultFailuresKeys]; + [self.failures enumerateKeysAndObjectsUsingBlock:^(NSString *key, + GTLRObject * obj, + BOOL * stop) { + NSString *storageKey = + [kGTLRBatchResultFailurKeyPrefix stringByAppendingString:key]; + [encoder encodeObject:obj forKey:storageKey]; + }]; + + [encoder encodeObject:self.responseHeaders + forKey:kGTLRBatchResultResponseHeaders]; +} + +@end diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRDateTime.h b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRDateTime.h @@ -0,0 +1,115 @@ +/* Copyright (c) 2011 Google Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#import <Foundation/Foundation.h> +#import "GTLRDefines.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * An immutable class representing a date and optionally a time in UTC. + */ +@interface GTLRDateTime : NSObject <NSCopying> + +/** + * Constructor from a string representation. + */ ++ (nullable instancetype)dateTimeWithRFC3339String:(nullable NSString *)str; + +/** + * Constructor from a date and time representation. + */ ++ (instancetype)dateTimeWithDate:(NSDate *)date; + +/** + * Constructor from a date and time representation, along with an offset + * minutes value used when creating a RFC3339 string representation. + * + * The date value is independent of time zone; the offset affects how the + * date will be rendered as a string. + * + * The offsetMinutes may be initialized from a NSTimeZone as + * (timeZone.secondsFromGMT / 60) + */ ++ (instancetype)dateTimeWithDate:(NSDate *)date + offsetMinutes:(NSInteger)offsetMinutes; + +/** + * Constructor from a date for an all-day event. + * + * Use this constructor to create a @c GTLRDateTime that is "date only". + * + * @note @c hasTime will be set to NO. + */ ++ (instancetype)dateTimeForAllDayWithDate:(NSDate *)date; + +/** + * Constructor from date components. + */ ++ (instancetype)dateTimeWithDateComponents:(NSDateComponents *)date; + +/** + * The represented date and time. + * + * If @c hasTime is NO, the time is set to noon GMT so the date is valid for all time zones. + */ +@property(nonatomic, readonly) NSDate *date; + +/** + * The date and time as a RFC3339 string representation. + */ +@property(nonatomic, readonly) NSString *RFC3339String; + +/** + * The date and time as a RFC3339 string representation. + * + * This returns the same string as @c RFC3339String. + */ +@property(nonatomic, readonly) NSString *stringValue; + +/** + * The represented date and time as date components. + */ +@property(nonatomic, readonly, copy) NSDateComponents *dateComponents; + +/** + * The fraction of seconds represented, 0-999. + */ +@property(nonatomic, readonly) NSInteger milliseconds; + +/** + * The time offset displayed in the string representation, if any. + * + * If the offset is not nil, the date and time will be rendered as a string + * for the time zone indicated by the offset. + * + * An app may create a NSTimeZone for this with + * [NSTimeZone timeZoneForSecondsFromGMT:(offsetMinutes.integerValue * 60)] + */ +@property(nonatomic, readonly, nullable) NSNumber *offsetMinutes; + +/** + * Flag indicating if the object represents date only, or date with time. + */ +@property(nonatomic, readonly) BOOL hasTime; + +/** + * The calendar used by this class, Gregorian and UTC. + */ ++ (NSCalendar *)calendar; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRDateTime.m b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRDateTime.m @@ -0,0 +1,373 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !__has_feature(objc_arc) +#error "This file needs to be compiled with ARC enabled." +#endif + +#import "GTLRDateTime.h" + +static NSUInteger const kGTLRDateComponentBits = (NSCalendarUnitYear | NSCalendarUnitMonth + | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute + | NSCalendarUnitSecond); + +@interface GTLRDateTime () + +- (void)setFromDate:(NSDate *)date; +- (void)setFromRFC3339String:(NSString *)str; + +@property(nonatomic, copy, readwrite) NSDateComponents *dateComponents; +@property(nonatomic, assign, readwrite) NSInteger milliseconds; +@property(nonatomic, strong, readwrite, nullable) NSNumber *offsetMinutes; + +@property(nonatomic, assign, readwrite) BOOL hasTime; + +@end + + +@implementation GTLRDateTime { + NSDate *_cachedDate; + NSString *_cachedRFC3339String; +} + +// A note about _milliseconds: +// RFC 3339 has support for fractions of a second. NSDateComponents is all +// NSInteger based, so it can't handle a fraction of a second. NSDate is +// built on NSTimeInterval so it has sub-millisecond precision. GTLR takes +// the compromise of supporting the RFC's optional fractional second support +// by maintaining a number of milliseconds past what fits in the +// NSDateComponents. The parsing and string conversions will include +// 3 decimal digits (hence milliseconds). When going to a string, the decimal +// digits are only included if the milliseconds are non zero. + +@dynamic date; +@dynamic RFC3339String; +@dynamic stringValue; +@dynamic hasTime; + +@synthesize dateComponents = _dateComponents, + milliseconds = _milliseconds, + offsetMinutes = _offsetMinutes; + ++ (instancetype)dateTimeWithRFC3339String:(NSString *)str { + if (str == nil) return nil; + + GTLRDateTime *result = [[self alloc] init]; + [result setFromRFC3339String:str]; + return result; +} + ++ (instancetype)dateTimeWithDate:(NSDate *)date { + if (date == nil) return nil; + + GTLRDateTime *result = [[self alloc] init]; + [result setFromDate:date]; + return result; +} + ++ (instancetype)dateTimeWithDate:(NSDate *)date + offsetMinutes:(NSInteger)offsetMinutes { + GTLRDateTime *result = [self dateTimeWithDate:date]; + result.offsetMinutes = @(offsetMinutes); + return result; +} + ++ (instancetype)dateTimeForAllDayWithDate:(NSDate *)date { + if (date == nil) return nil; + + GTLRDateTime *result = [[self alloc] init]; + [result setFromDate:date]; + result.hasTime = NO; + return result; +} + ++ (instancetype)dateTimeWithDateComponents:(NSDateComponents *)components { + NSCalendar *cal = components.calendar ?: [self calendar]; + NSDate *date = [cal dateFromComponents:components]; + + return [self dateTimeWithDate:date]; +} + +- (id)copyWithZone:(NSZone *)zone { + // Object is immutable + return self; +} + +- (BOOL)isEqual:(GTLRDateTime *)other { + if (self == other) return YES; + if (![other isKindOfClass:[GTLRDateTime class]]) return NO; + + BOOL areDateComponentsEqual = [self.dateComponents isEqual:other.dateComponents]; + if (!areDateComponentsEqual) return NO; + + NSNumber *offsetMinutes = self.offsetMinutes; + NSNumber *otherOffsetMinutes = other.offsetMinutes; + if ((offsetMinutes == nil) != (otherOffsetMinutes == nil) + || (offsetMinutes.integerValue != otherOffsetMinutes.integerValue)) return NO; + + return (self.milliseconds == other.milliseconds); +} + +- (NSUInteger)hash { + return [[self date] hash]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ %p: {%@}", + [self class], self, self.RFC3339String]; +} + +- (NSDate *)date { + @synchronized(self) { + if (_cachedDate) return _cachedDate; + } + + NSDateComponents *dateComponents = self.dateComponents; + NSTimeInterval extraMillisecondsAsSeconds = 0.0; + NSCalendar *cal = [[self class] calendar]; + + if (!self.hasTime) { + // We're not keeping track of a time, but NSDate always is based on + // an absolute time. We want to avoid returning an NSDate where the + // calendar date appears different from what was used to create our + // date-time object. + // + // We'll make a copy of the date components, setting the time on our + // copy to noon GMT, since that ensures the date renders correctly for + // any time zone. + NSDateComponents *noonDateComponents = [dateComponents copy]; + [noonDateComponents setHour:12]; + [noonDateComponents setMinute:0]; + [noonDateComponents setSecond:0]; + dateComponents = noonDateComponents; + } else { + // Add in the fractional seconds that don't fit into NSDateComponents. + extraMillisecondsAsSeconds = ((NSTimeInterval)self.milliseconds) / 1000.0; + } + + NSDate *date = [cal dateFromComponents:dateComponents]; + + // Add in any milliseconds that didn't fit into the dateComponents. + if (extraMillisecondsAsSeconds > 0.0) { + date = [date dateByAddingTimeInterval:extraMillisecondsAsSeconds]; + } + + @synchronized(self) { + _cachedDate = date; + } + return date; +} + +- (NSString *)stringValue { + return self.RFC3339String; +} + +- (NSString *)RFC3339String { + @synchronized(self) { + if (_cachedRFC3339String) return _cachedRFC3339String; + } + + NSDateComponents *dateComponents = self.dateComponents; + + NSString *timeString = @""; // timeString like "T15:10:46-08:00" + + if (self.hasTime) { + NSString *fractionalSecondsString = @""; + if (self.milliseconds > 0.0) { + fractionalSecondsString = [NSString stringWithFormat:@".%03ld", (long)self.milliseconds]; + } + + // If the dateTime was created from a string with a time offset, render that back in + // and adjust the time. + NSString *offsetStr = @"Z"; + NSNumber *offsetMinutes = self.offsetMinutes; + if (offsetMinutes != nil) { + BOOL isNegative = NO; + NSInteger offsetVal = offsetMinutes.integerValue; + if (offsetVal < 0) { + isNegative = YES; + offsetVal = -offsetVal; + } + NSInteger mins = offsetVal % 60; + NSInteger hours = (offsetVal - mins) / 60; + offsetStr = [NSString stringWithFormat:@"%c%02ld:%02ld", + isNegative ? '-' : '+', (long)hours, (long)mins]; + + // Adjust date components back to account for the offset. + // + // This is the inverse of the adjustment done in setFromRFC3339String:. + if (offsetVal != 0) { + NSDate *adjustedDate = + [self.date dateByAddingTimeInterval:(offsetMinutes.integerValue * 60)]; + NSCalendar *calendar = [[self class] calendar]; + dateComponents = [calendar components:kGTLRDateComponentBits + fromDate:adjustedDate]; + } + } + + timeString = [NSString stringWithFormat:@"T%02ld:%02ld:%02ld%@%@", + (long)dateComponents.hour, (long)dateComponents.minute, + (long)dateComponents.second, fractionalSecondsString, + offsetStr]; + } + + // full dateString like "2006-11-17T15:10:46-08:00" + NSString *dateString = [NSString stringWithFormat:@"%04ld-%02ld-%02ld%@", + (long)dateComponents.year, (long)dateComponents.month, + (long)dateComponents.day, timeString]; + + @synchronized(self) { + _cachedRFC3339String = dateString; + } + return dateString; +} + +- (void)setFromDate:(NSDate *)date { + NSCalendar *cal = [[self class] calendar]; + + NSDateComponents *components = [cal components:kGTLRDateComponentBits + fromDate:date]; + self.dateComponents = components; + + // Extract the fractional seconds. + NSTimeInterval asTimeInterval = [date timeIntervalSince1970]; + NSTimeInterval worker = asTimeInterval - trunc(asTimeInterval); + self.milliseconds = (NSInteger)round(worker * 1000.0); +} + +- (void)setFromRFC3339String:(NSString *)str { + static NSCharacterSet *gDashSet; + static NSCharacterSet *gTSet; + static NSCharacterSet *gColonSet; + static NSCharacterSet *gPlusMinusZSet; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + gDashSet = [NSCharacterSet characterSetWithCharactersInString:@"-"]; + gTSet = [NSCharacterSet characterSetWithCharactersInString:@"Tt "]; + gColonSet = [NSCharacterSet characterSetWithCharactersInString:@":"]; + gPlusMinusZSet = [NSCharacterSet characterSetWithCharactersInString:@"+-zZ"]; + }); + + NSInteger year = NSDateComponentUndefined; + NSInteger month = NSDateComponentUndefined; + NSInteger day = NSDateComponentUndefined; + NSInteger hour = NSDateComponentUndefined; + NSInteger minute = NSDateComponentUndefined; + NSInteger sec = NSDateComponentUndefined; + NSInteger milliseconds = 0; + double secDouble = -1.0; + NSString* sign = nil; + NSInteger offsetHour = 0; + NSInteger offsetMinute = 0; + + if (str.length > 0) { + NSScanner* scanner = [NSScanner scannerWithString:str]; + // There should be no whitespace, so no skip characters. + [scanner setCharactersToBeSkipped:nil]; + + // for example, scan 2006-11-17T15:10:46-08:00 + // or 2006-11-17T15:10:46Z + if (// yyyy-mm-dd + [scanner scanInteger:&year] && + [scanner scanCharactersFromSet:gDashSet intoString:NULL] && + [scanner scanInteger:&month] && + [scanner scanCharactersFromSet:gDashSet intoString:NULL] && + [scanner scanInteger:&day] && + // Thh:mm:ss + [scanner scanCharactersFromSet:gTSet intoString:NULL] && + [scanner scanInteger:&hour] && + [scanner scanCharactersFromSet:gColonSet intoString:NULL] && + [scanner scanInteger:&minute] && + [scanner scanCharactersFromSet:gColonSet intoString:NULL] && + [scanner scanDouble:&secDouble]) { + + // At this point we got secDouble, pull it apart. + sec = (NSInteger)secDouble; + double worker = secDouble - ((double)sec); + milliseconds = (NSInteger)round(worker * 1000.0); + + // Finish parsing, now the offset info. + if (// Z or +hh:mm + [scanner scanCharactersFromSet:gPlusMinusZSet intoString:&sign] && + [scanner scanInteger:&offsetHour] && + [scanner scanCharactersFromSet:gColonSet intoString:NULL] && + [scanner scanInteger:&offsetMinute]) { + } + } + } + + NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; + [dateComponents setYear:year]; + [dateComponents setMonth:month]; + [dateComponents setDay:day]; + [dateComponents setHour:hour]; + [dateComponents setMinute:minute]; + [dateComponents setSecond:sec]; + + BOOL isMinusOffset = [sign isEqual:@"-"]; + if (isMinusOffset || [sign isEqual:@"+"]) { + NSInteger totalOffsetMinutes = ((offsetHour * 60) + offsetMinute) * (isMinusOffset ? -1 : 1); + self.offsetMinutes = @(totalOffsetMinutes); + + // Minus offset means Universal time is that many hours and minutes ahead. + // + // This is the inverse of the adjustment done above in RFC3339String. + NSTimeInterval deltaOffsetSeconds = -totalOffsetMinutes * 60; + NSCalendar *calendar = [[self class] calendar]; + NSDate *scannedDate = [calendar dateFromComponents:dateComponents]; + NSDate *offsetDate = [scannedDate dateByAddingTimeInterval:deltaOffsetSeconds]; + + dateComponents = [calendar components:kGTLRDateComponentBits + fromDate:offsetDate]; + } + + self.dateComponents = dateComponents; + self.milliseconds = milliseconds; +} + +- (BOOL)hasTime { + NSDateComponents *dateComponents = self.dateComponents; + + BOOL hasTime = ([dateComponents hour] != NSDateComponentUndefined + && [dateComponents minute] != NSDateComponentUndefined); + + return hasTime; +} + +- (void)setHasTime:(BOOL)shouldHaveTime { + // We'll set time values to zero or kUndefinedDateComponent as appropriate. + BOOL hadTime = self.hasTime; + + if (shouldHaveTime && !hadTime) { + [_dateComponents setHour:0]; + [_dateComponents setMinute:0]; + [_dateComponents setSecond:0]; + _milliseconds = 0; + } else if (hadTime && !shouldHaveTime) { + [_dateComponents setHour:NSDateComponentUndefined]; + [_dateComponents setMinute:NSDateComponentUndefined]; + [_dateComponents setSecond:NSDateComponentUndefined]; + _milliseconds = 0; + } +} + ++ (NSCalendar *)calendar { + NSCalendar *cal = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; + cal.timeZone = (NSTimeZone * _Nonnull)[NSTimeZone timeZoneWithName:@"Universal"]; + return cal; +} + +@end diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRDuration.h b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRDuration.h @@ -0,0 +1,83 @@ +/* Copyright (c) 2016 Google Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#import <Foundation/Foundation.h> +#import "GTLRDefines.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * An immutable class representing a string data type 'google-duration'. + * It is based off the protocol buffers definition: + * https://github.com/google/protobuf/blob/master/src/google/protobuf/duration.proto + */ +@interface GTLRDuration : NSObject <NSCopying> + +/** + * Signed seconds of the span of time. Must be from -315,576,000,000 + * to +315,576,000,000 inclusive. + **/ +@property(nonatomic, readonly) int64_t seconds; + +/** + * Signed fractions of a second at nanosecond resolution of the span + * of time. Durations less than one second are represented with a 0 + * `seconds` field and a positive or negative `nanos` field. For durations + * of one second or more, a non-zero value for the `nanos` field must be + * of the same sign as the `seconds` field. Must be from -999,999,999 + * to +999,999,999 inclusive. + **/ +@property(nonatomic, readonly) int32_t nanos; + +/** + * This duration expressed as a NSTimeInterval. + * + * @note: Not all second/nanos combinations can be represented in a + * NSTimeInterval, so this could be a lossy transform. + **/ +@property(nonatomic, readonly) NSTimeInterval timeInterval; + +/** + * Returns the string form used to send this data type in a JSON payload. + */ +@property(nonatomic, readonly) NSString *jsonString; + +/** + * Constructor for a new duration with the given seconds and nanoseconds. + * + * Will fail if seconds/nanos differ in sign or if nanos is more than one + * second. + **/ ++ (nullable instancetype)durationWithSeconds:(int64_t)seconds + nanos:(int32_t)nanos; + +/** + * Constructor for a new duration from the given string form. + * + * Will return nil if jsonString is invalid. + **/ ++ (nullable instancetype)durationWithJSONString:(nullable NSString *)jsonString; + +/** + * Constructor for a new duration from the NSTimeInterval. + * + * @note NSTimeInterval doesn't always express things as exactly as one might + * expect, so coverting from to integer seconds & nanos can reveal this. + **/ ++ (instancetype)durationWithTimeInterval:(NSTimeInterval)timeInterval; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRDuration.m b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRDuration.m @@ -0,0 +1,222 @@ +/* Copyright (c) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !__has_feature(objc_arc) +#error "This file needs to be compiled with ARC enabled." +#endif + +#import "GTLRDuration.h" + +static const int32_t kNanosPerMillisecond = 1000000; +static const int32_t kNanosPerMicrosecond = 1000; +static const int32_t kNanosPerSecond = 1000000000; + +static int32_t IntPow10(int x) { + int32_t result = 1; + for (int i = 0; i < x; ++i) { + result *= 10; + } + return result; +} + +@implementation GTLRDuration + +@dynamic timeInterval; + +@synthesize seconds = _seconds, + nanos = _nanos, + jsonString = _jsonString; + ++ (instancetype)durationWithSeconds:(int64_t)seconds nanos:(int32_t)nanos { + if (seconds < 0) { + if (nanos > 0) { + // secs was -, nanos was + + return nil; + } + } else if (seconds > 0) { + if (nanos < 0) { + // secs was +, nanos was - + return nil; + } + } + if ((nanos <= -kNanosPerSecond) || (nanos >= kNanosPerSecond)) { + // more than a seconds worth + return nil; + } + return [[self alloc] initWithSeconds:seconds nanos:nanos jsonString:NULL]; +} + ++ (instancetype)durationWithJSONString:(NSString *)jsonString { + // It has to end in "s", so it needs >1 character. + if (jsonString.length <= 1) { + return nil; + } + + static NSCharacterSet *gNumberSet; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + gNumberSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789"]; + }); + + NSScanner* scanner = [NSScanner scannerWithString:jsonString]; + // There should be no whitespace, so no skip characters. + [scanner setCharactersToBeSkipped:nil]; + + // Can start with a '-'. + BOOL isNeg = [scanner scanString:@"-" intoString:NULL]; + + int64_t seconds; + if (![scanner scanLongLong:&seconds]) { + return nil; + } + + // Since the sign was manually scanned, seconds should be positive + // (i.e. no "--#" in the put). + if (seconds < 0) { + return nil; + } + + // See if it has a ".[nanos]". Spec seems to say it is required, but play + // it safe and make it optional. + int32_t nanos = 0; + if ([scanner scanString:@"." intoString:NULL]) { + NSString *nanosStr; + if (![scanner scanCharactersFromSet:gNumberSet intoString:&nanosStr]) { + return nil; + } + // Ensure not too many digits (also ensure it is within range). + if (nanosStr.length > 9) { + return nil; + } + // Can use NSString's intValue since the character set was controlled. + nanos = [nanosStr intValue]; + // Scale based on length. + nanos *= IntPow10(9 - (int)nanosStr.length); + } + + // And must have the final 's'. + if (![scanner scanString:@"s" intoString:NULL]) { + return nil; + } + + // Better be the end... + if (![scanner isAtEnd]) { + return nil; + } + + if (isNeg) { + seconds = -seconds; + nanos = -nanos; + } + + // Pass on the json string so it will be reflected back out as it came in + // (incase it had a different number of digits, etc). + return [[self alloc] initWithSeconds:seconds + nanos:nanos + jsonString:jsonString]; +} + ++ (instancetype)durationWithTimeInterval:(NSTimeInterval)timeInterval { + NSTimeInterval seconds; + NSTimeInterval nanos = modf(timeInterval, &seconds); + nanos *= (NSTimeInterval)kNanosPerSecond; + + return [[self alloc] initWithSeconds:(int64_t)seconds + nanos:(int32_t)nanos + jsonString:NULL]; +} + +- (instancetype)init { + return [self initWithSeconds:0 nanos:0 jsonString:NULL]; +} + +- (instancetype)initWithSeconds:(int64_t)seconds + nanos:(int32_t)nanos + jsonString:(NSString *)jsonString { + self = [super init]; + if (self) { + // Sanity asserts, the class methods should make sure this doesn't happen. + GTLR_DEBUG_ASSERT((((seconds <= 0) && (nanos <= 0)) || + ((seconds >= 0) && (nanos >= 0))), + @"Seconds and nanos must have the same sign (%lld & %d)", + seconds, nanos); + GTLR_DEBUG_ASSERT(((nanos < kNanosPerSecond) && + (nanos > -kNanosPerSecond)), + @"Nanos is a second or more (%d)", nanos); + + _seconds = seconds; + _nanos = nanos; + + if (jsonString.length) { + _jsonString = [jsonString copy]; + } else { + // Based off the JSON serialization code in protocol buffers + // ( https://github.com/google/protobuf/ ). + NSString *sign = @""; + if ((seconds < 0) || (nanos < 0)) { + sign = @"-"; + seconds = -seconds; + nanos = -nanos; + } + int nanoDigts; + int32_t nanoDivider; + if (nanos % kNanosPerMillisecond == 0) { + nanoDigts = 3; + nanoDivider = kNanosPerMillisecond; + } else if (nanos % kNanosPerMicrosecond == 0) { + nanoDigts = 6; + nanoDivider = kNanosPerMicrosecond; + } else { + nanoDigts = 9; + nanoDivider = 1; + } + _jsonString = [NSString stringWithFormat:@"%@%lld.%0*ds", + sign, seconds, nanoDigts, (nanos / nanoDivider)]; + } + } + return self; +} + +- (NSTimeInterval)timeInterval { + NSTimeInterval result = self.seconds; + result += (NSTimeInterval)self.nanos / (NSTimeInterval)kNanosPerSecond; + return result; +} + +- (id)copyWithZone:(NSZone *)zone { + // Object is immutable + return self; +} + +- (BOOL)isEqual:(GTLRDuration *)other { + if (self == other) return YES; + if (![other isKindOfClass:[GTLRDuration class]]) return NO; + + BOOL result = ((self.seconds == other.seconds) && + (self.nanos == other.nanos)); + return result; +} + +- (NSUInteger)hash { + NSUInteger result = (NSUInteger)((self.seconds * 13) + self.nanos); + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ %p: {%@}", + [self class], self, self.jsonString]; +} + +@end diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRErrorObject.h b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRErrorObject.h @@ -0,0 +1,116 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GTLRObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@class GTLRErrorObjectErrorItem; +@class GTLRErrorObjectDetail; + +/** + * This class wraps JSON responses (both V1 and V2 of Google JSON errors) and NSErrors. + * + * A GTLRErrorObject can be created using +objectWithJSON: or +objectWithFoundationError: + */ +@interface GTLRErrorObject : GTLRObject + +/** + * Convenience method for creating an error object from an NSError. + * + * @param error The @c NSError to be encapsulated by the @c GTLRErrorObject + * + * @return A @c GTLRErrorObject wrapping the NSError. + */ ++ (instancetype)objectWithFoundationError:(NSError *)error; + +/** + * Convenience utility for extracting the GTLRErrorObject that was used to create an NSError. + * + * @param foundationError The NSError that may have been obtained from a GTLRErrorObject. + * + * @return The GTLRErrorObject, nil if the error was not originally from a GTLRErrorObject. + */ ++ (nullable GTLRErrorObject *)underlyingObjectForError:(NSError *)foundationError; + +// +// V1 & V2 properties. +// + +/** + * The numeric error code. + */ +@property(nonatomic, strong, nullable) NSNumber *code; + +/** + * An error message string, typically provided by the API server. This is not localized, + * and its reliability depends on the API server. + */ +@property(nonatomic, strong, nullable) NSString *message; + +// +// V1 properties. +// + +/** + * Underlying errors that occurred on the server. + */ +@property(nonatomic, strong, nullable) NSArray<GTLRErrorObjectErrorItem *> *errors; + +// +// V2 properties +// + +/** + * A status error string, defined by the API server, such as "NOT_FOUND". + */ +@property(nonatomic, strong, nullable) NSString *status; + +/** + * Additional diagnostic error details provided by the API server. + */ +@property(nonatomic, strong, nullable) NSArray<GTLRErrorObjectDetail *> *details; + +/** + * An NSError, either underlying the error object or manufactured from the error object's + * properties. + */ +@property(nonatomic, readonly) NSError *foundationError; + +@end + +/** + * Class representing the items of the "errors" array inside the Google V1 error JSON. + * + * Client applications should not rely on the property values of these items. + */ +@interface GTLRErrorObjectErrorItem : GTLRObject +@property(nonatomic, strong, nullable) NSString *domain; +@property(nonatomic, strong, nullable) NSString *reason; +@property(nonatomic, strong, nullable) NSString *message; +@property(nonatomic, strong, nullable) NSString *location; +@end + +/** + * Class representing the items of the "details" array inside the Google V2 error JSON. + * + * Client applications should not rely on the property values of these items. + */ +@interface GTLRErrorObjectDetail : GTLRObject +@property(nonatomic, strong, nullable) NSString *type; +@property(nonatomic, strong, nullable) NSString *detail; +@end + +NS_ASSUME_NONNULL_END diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRErrorObject.m b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRErrorObject.m @@ -0,0 +1,140 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !__has_feature(objc_arc) +#error "This file needs to be compiled with ARC enabled." +#endif + +#import "GTLRErrorObject.h" + +#import "GTLRUtilities.h" +#import "GTLRService.h" + +static NSString *const kGTLRErrorObjectFoundationErrorKey = @"foundationError"; + +@implementation GTLRErrorObject { + NSError *_originalFoundationError; +} + +// V1 & V2 properties. +@dynamic code; +@dynamic message; + +// V1 properties. +@dynamic errors; + +// V2 properties. +@dynamic status; +@dynamic details; + +// Implemented below. +@dynamic foundationError; + ++ (instancetype)objectWithFoundationError:(NSError *)error { + GTLRErrorObject *object = [self object]; + object->_originalFoundationError = error; + object.code = @(error.code); + object.message = error.localizedDescription; + return object; +} + ++ (NSDictionary *)arrayPropertyToClassMap { + return @{ + @"errors" : [GTLRErrorObjectErrorItem class], + @"details" : [GTLRErrorObjectDetail class] + }; +} + +- (NSError *)foundationError { + // If there was an original foundation error, copy its userInfo as the basis for ours. + NSMutableDictionary *userInfo = + [NSMutableDictionary dictionaryWithDictionary:_originalFoundationError.userInfo]; + + // This structured GTLRErrorObject will be available in the error's userInfo + // dictionary. + userInfo[kGTLRStructuredErrorKey] = self; + + NSError *error; + if (_originalFoundationError) { + error = [NSError errorWithDomain:_originalFoundationError.domain + code:_originalFoundationError.code + userInfo:userInfo]; + } else { + NSString *reasonStr = self.message; + if (reasonStr) { + userInfo[NSLocalizedDescriptionKey] = reasonStr; + } + + error = [NSError errorWithDomain:kGTLRErrorObjectDomain + code:self.code.integerValue + userInfo:userInfo]; + } + return error; +} + ++ (GTLRErrorObject *)underlyingObjectForError:(NSError *)foundationError { + NSDictionary *userInfo = [foundationError userInfo]; + GTLRErrorObject *errorObj = [userInfo objectForKey:kGTLRStructuredErrorKey]; + return errorObj; +} + +- (BOOL)isEqual:(id)object { + // Include the underlying foundation error in equality checks. + if (self == object) return YES; + if (![super isEqual:object]) return NO; + if (![object isKindOfClass:[GTLRErrorObject class]]) return NO; + GTLRErrorObject *other = (GTLRErrorObject *)object; + return GTLR_AreEqualOrBothNil(_originalFoundationError, + other->_originalFoundationError); +} + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (instancetype)initWithCoder:(NSCoder *)decoder { + self = [super initWithCoder:decoder]; + if (self) { + _originalFoundationError = + [decoder decodeObjectOfClass:[NSError class] + forKey:kGTLRErrorObjectFoundationErrorKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)encoder { + [super encodeWithCoder:encoder]; + [encoder encodeObject:_originalFoundationError + forKey:kGTLRErrorObjectFoundationErrorKey]; +} + +@end + +@implementation GTLRErrorObjectErrorItem +@dynamic domain; +@dynamic reason; +@dynamic message; +@dynamic location; +@end + +@implementation GTLRErrorObjectDetail +@dynamic type; +@dynamic detail; + ++ (NSDictionary *)propertyToJSONKeyMap { + return @{ @"type" : @"@type" }; +} + +@end diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRObject.h b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRObject.h @@ -0,0 +1,317 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// GTLRObject documentation: +// https://github.com/google/google-api-objectivec-client-for-rest/wiki#objects-and-queries + +#import <Foundation/Foundation.h> + +#import "GTLRDefines.h" +#import "GTLRDateTime.h" +#import "GTLRDuration.h" + +NS_ASSUME_NONNULL_BEGIN + +@class GTLRObject; + +/** + * Protocol that can be implemented to provide custom logic for what class + * should be created out of the given JSON. + */ +@protocol GTLRObjectClassResolver <NSObject> +- (Class)classForJSON:(NSDictionary *)json + defaultClass:(Class)defaultClass; +@end + +/** + * Standard GTLRObjectClassResolver used by the core library. + */ +@interface GTLRObjectClassResolver : NSObject<GTLRObjectClassResolver> + +/** + * Returns a resolver that will look up the 'kind' properties to find classes + * based on the JSON. + * + * The generated service classes provide a +kindStringToClassMap method for any + * mappings that were found from discovery when generating the service. + */ ++ (instancetype)resolverWithKindMap:(NSDictionary<NSString *, Class> *)kindStringToClassMap; + +/** + * Returns a resolver that will look up the 'kind' properties to find classes + * based on the JSON and then applies mapping of surrogate classes to swap out + * specific classes. + * + * Surrogates are subclasses to be instantiated instead of standard classes + * when creating objects from the JSON. For example, this code will, for one query's + * execution, swap a service's default resolver for one that will then use + * MyCalendarEventSubclass instead of GTLRCalendarEvent and + * MyCalendarReminderSubclass instead of GTLRCalendarReminder. + * + * @code + * NSDictionary *surrogates = @{ + * [GTLRCalendarEvent class] : [MyCalendarEventSubclass class] + * [GTLRCalendarReminder class] : [MyCalendarReminderSubclass class], + * }; + * NSDictionary *serviceKindMap = [[calendarService class] kindStringToClassMap]; + * GTLRObjectClassResolver *updatedResolver = + * [GTLRObjectClassResolver resolverWithKindMap:serviceKindMap + * surrogates:surrogates]; + * query.executionParameters.objectClassResolver = updatedResolver; + * @endcode + * + * @note To install surrogates for all queries executed by the service, use + * the service's @c -setSurrogates method. + */ ++ (instancetype)resolverWithKindMap:(NSDictionary<NSString *, Class> *)kindStringToClassMap + surrogates:(NSDictionary<Class, Class> *)surrogates; + +@end + +/** + * @c GTLRObject serves as the common superclass for classes wrapping JSON, errors, and other data + * passed in server requests and responses. + * + * @note This class is @em not safe for simultaneous use from multiple threads. Applications should + * serialize or protect access to a @c GTLRObject instance as they would for any standard + * Cocoa mutable container. + */ +@interface GTLRObject : NSObject <NSCopying, NSSecureCoding> + +/** + * The JSON underlying the property values for this object. + * + * The JSON should be accessed or set using the generated properties of a + * class derived from GTLRObject or with the methods @c setJSONValue:forKey: + * and @c JSONValueForKey: + * + * @note: Applications should use @c additionalPropertyForKey: when accessing + * API object properties that do not have generated @c \@property accessors. + */ +@property(nonatomic, strong, nullable) NSMutableDictionary *JSON; + +/** + * A dictionary retained by the object for the convenience of the client application. + * + * A client application may use this to retain any dictionary. + * + * The values of the user properties dictionary will not be sent to the server during + * query execution, and will not be copied by NSCopying or encoded by NSSecureCoding. + */ +@property(nonatomic, strong) NSDictionary *userProperties; + +///////////////////////////////////////////////////////////////////////////////////////////// +// +// Public methods +// +// These methods are intended for users of the library +// +///////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Constructor for an empty object. + */ ++ (instancetype)object; + +/** + * Constructor for an object including JSON. + */ ++ (instancetype)objectWithJSON:(nullable NSDictionary *)dict; + +/** + * Constructor for an object including JSON and providing a resolver to help + * select the correct classes for sub objects within the json. + * + * The generated services provide a default resolver (-objectClassResolver) + * that covers the kinds for that service. They also expose the kind mappings + * via the +kindStringToClassMap method. + */ ++ (instancetype)objectWithJSON:(nullable NSDictionary *)dict + objectClassResolver:(id<GTLRObjectClassResolver>)objectClassResolver; + +/** + * The JSON for the object, or an empty string if there is no JSON or if the JSON + * dictionary cannot be represented as JSON. + */ +- (NSString *)JSONString; + +/** + * Generic access for setting entries in the JSON dictionary. This creates the JSON dictionary + * if necessary. + * + * @note: Applications should use @c setAdditionalProperty:forKey: when setting + * API object properties that do not have generated @c \@property accessors. + */ +- (void)setJSONValue:(nullable id)obj forKey:(nonnull NSString *)key; + +/** + * Generic access to the JSON dictionary. + * + * @note: Applications should use @c additionalPropertyForKey: when accessing + * API object properties that do not have generated @c \@property accessors. + */ +- (nullable id)JSONValueForKey:(NSString *)key; + +/** + * The list of keys in this object's JSON that are not listed as properties on the object. + */ +- (nullable NSArray<NSString *> *)additionalJSONKeys; + +/** + * Setter for any key in the JSON that is not listed as a @c \@property in the class declaration. + */ +- (void)setAdditionalProperty:(id)obj forName:(NSString *)name; + +/** + * Accessor for any key in the JSON that is not listed as a @c \@property in the class + * declaration. + */ +- (nullable id)additionalPropertyForName:(NSString *)name; + +/** + * A dictionary of all keys in the JSON that is not listed as a @c \@property in the class + * declaration. + */ +- (NSDictionary<NSString *, id> *)additionalProperties; + +/** + * A string for a partial query describing the fields present. + * + * @note Only the first element of any array is examined. + * + * @see https://developers.google.com/google-apps/tasks/performance?csw=1#partial + * + * @return A @c fields string describing the fields present in the object. + */ +- (NSString *)fieldsDescription; + +/** + * An object containing only the changes needed to do a partial update (patch), + * where the patch would be to change an object from the original to the receiver, + * such as + * @c GTLRSomeObject *patchObject = [newVersion patchObjectFromOriginal:oldVersion]; + * + * @note This method returns nil if there are no changes between the original and the receiver. + * + * @see https://developers.google.com/google-apps/tasks/performance?csw=1#patch + * + * @param original The original object from which to create the patch object. + * + * @return The object used for the patch body. + */ +- (nullable id)patchObjectFromOriginal:(GTLRObject *)original; + +/** + * A null value to set object properties for patch queries that delete fields. + * + * Do not use this except when setting an object property for a patch query. + * + * @return The null value object. + */ ++ (id)nullValue; + +#pragma mark Internal + +/////////////////////////////////////////////////////////////////////////////// +// +// Protected methods +// +// These methods are intended for subclasses of GTLRObject +// + +// Creation of objects from a JSON dictionary. The class created depends on +// the content of the JSON, not the class messaged. ++ (nullable GTLRObject *)objectForJSON:(NSMutableDictionary *)json + defaultClass:(nullable Class)defaultClass + objectClassResolver:(id<GTLRObjectClassResolver>)objectClassResolver; + +// Property-to-key mapping (for JSON keys which are not used as method names) ++ (nullable NSDictionary<NSString *, NSString *> *)propertyToJSONKeyMap; + +// property-to-Class mapping for array properties (to say what is in the array) ++ (nullable NSDictionary<NSString *, Class> *)arrayPropertyToClassMap; + +// The default class for additional JSON keys ++ (nullable Class)classForAdditionalProperties; + +// Indicates if a "kind" property on this class can be used for the class +// registry or if it appears to be non standard. ++ (BOOL)isKindValidForClassRegistry; + +@end + +/** + * Collection results have a property containing an array of @c GTLRObject + * + * This provides support for @c NSFastEnumeration and for indexed subscripting to + * access the objects in the array. + */ +@interface GTLRCollectionObject : GTLRObject<NSFastEnumeration> + +/** + * The property name that holds the collection. + * + * @return The key for the property holding the array of @c GTLRObject items. + */ ++ (NSString *)collectionItemsKey; + +// objectAtIndexedSubscript: will throw if the index is out of bounds (like +// NSArray does). +- (nullable id)objectAtIndexedSubscript:(NSUInteger)idx; + +@end + +/** + * A GTLRDataObject holds media data and the MIME type of the data returned by a media + * download query. + * + * The JSON for the object may be nil. + */ +@interface GTLRDataObject : GTLRObject + +/** + * The downloaded media data. + */ +@property(atomic, strong) NSData *data; + +/** + * The MIME type of the downloaded media data. + */ +@property(atomic, copy) NSString *contentType; + +@end + +/** + * Base class used when a service method directly returns an array instead + * of a JSON object. This exists for the methods not up to spec. + */ +@interface GTLRResultArray : GTLRCollectionObject + +/** + * This method should only be called by subclasses. + */ +- (nullable NSArray *)itemsWithItemClass:(Class)itemClass; +@end + +/** + * Helper to call the resolver and find the class to use for the given JSON. + * Intended for internal library use only. + */ +Class GTLRObjectResolveClass( + id<GTLRObjectClassResolver> objectClassResolver, + NSDictionary *json, + Class defaultClass); + +NS_ASSUME_NONNULL_END diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRObject.m b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRObject.m @@ -0,0 +1,760 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !__has_feature(objc_arc) +#error "This file needs to be compiled with ARC enabled." +#endif + +#include <objc/runtime.h> + +#import "GTLRObject.h" +#import "GTLRRuntimeCommon.h" +#import "GTLRUtilities.h" + +static NSString *const kUserDataPropertyKey = @"_userData"; + +static NSString *const kGTLRObjectJSONCoderKey = @"json"; + +static NSMutableDictionary *DeepMutableCopyOfJSONDictionary(NSDictionary *initialJSON); + +@interface GTLRObject () <GTLRRuntimeCommon> + +@property(nonatomic, strong) id<GTLRObjectClassResolver>objectClassResolver; + +@end + +@implementation GTLRObject { + // Any complex object hung off this object goes into the cache so the + // next fetch will get the same object back instead of having to recreate + // it. + NSMutableDictionary *_childCache; +} + +@synthesize JSON = _json, + objectClassResolver = _objectClassResolver, + userProperties = _userProperties; + ++ (instancetype)object { + return [[self alloc] init]; +} + ++ (instancetype)objectWithJSON:(NSDictionary *)dict { + GTLRObject *obj = [self object]; + obj->_json = DeepMutableCopyOfJSONDictionary(dict); + return obj; +} + ++ (instancetype)objectWithJSON:(nullable NSDictionary *)dict + objectClassResolver:(id<GTLRObjectClassResolver>)objectClassResolver { + GTLRObject *obj = [self objectWithJSON:dict]; + obj->_objectClassResolver = objectClassResolver; + return obj; +} + ++ (NSDictionary<NSString *, NSString *> *)propertyToJSONKeyMap { + return nil; +} + ++ (NSDictionary<NSString *, Class> *)arrayPropertyToClassMap { + return nil; +} + ++ (Class)classForAdditionalProperties { + return Nil; +} + ++ (BOOL)isKindValidForClassRegistry { + return YES; +} + +- (BOOL)isEqual:(GTLRObject *)other { + if (self == other) return YES; + if (other == nil) return NO; + + // The objects should be the same class, or one should be a subclass of the + // other's class + if (![other isKindOfClass:[self class]] + && ![self isKindOfClass:[other class]]) return NO; + + // What we're not comparing here: + // properties + return GTLR_AreEqualOrBothNil(_json, [other JSON]); +} + +// By definition, for two objects to potentially be considered equal, +// they must have the same hash value. The hash is mostly ignored, +// but removeObjectsInArray: in Leopard does seem to check the hash, +// and NSObject's default hash method just returns the instance pointer. +// We'll define hash here for all of our GTLRObjects. +- (NSUInteger)hash { + return (NSUInteger) (__bridge void *) [GTLRObject class]; +} + +- (id)copyWithZone:(NSZone *)zone { + GTLRObject *newObject = [[[self class] allocWithZone:zone] init]; + newObject.JSON = DeepMutableCopyOfJSONDictionary(self.JSON); + newObject.objectClassResolver = self.objectClassResolver; + + // What we're not copying: + // userProperties + return newObject; +} + +- (NSString *)descriptionWithLocale:(id)locale { + return self.description; +} + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (instancetype)initWithCoder:(NSCoder *)decoder { + self = [super init]; + if (self) { + _json = [decoder decodeObjectOfClass:[NSMutableDictionary class] + forKey:kGTLRObjectJSONCoderKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)encoder { + [encoder encodeObject:_json forKey:kGTLRObjectJSONCoderKey]; +} + +#pragma mark JSON values + +- (void)setJSONValue:(id)obj forKey:(NSString *)key { + NSMutableDictionary *dict = self.JSON; + if (dict == nil && obj != nil) { + dict = [NSMutableDictionary dictionaryWithCapacity:1]; + self.JSON = dict; + } + [dict setValue:obj forKey:key]; +} + +- (id)JSONValueForKey:(NSString *)key { + id obj = [self.JSON objectForKey:key]; + return obj; +} + +- (NSString *)JSONString { + NSError *error; + NSDictionary *json = self.JSON; + if (json) { + NSData *data = [NSJSONSerialization dataWithJSONObject:json + options:NSJSONWritingPrettyPrinted + error:&error]; + GTLR_DEBUG_ASSERT(data != nil, @"JSONString generate failed: %@\n JSON: %@", error, json); + if (data) { + NSString *jsonStr = [[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding]; + if (jsonStr) return jsonStr; + } + } + return @""; +} + +- (NSArray<NSString *> *)additionalJSONKeys { + NSArray *knownKeys = [[self class] allKnownKeys]; + NSMutableArray *result; + NSArray *allKeys = _json.allKeys; + if (allKeys) { + result = [NSMutableArray arrayWithArray:allKeys]; + [result removeObjectsInArray:knownKeys]; + // Return nil instead of an empty array. + if (result.count == 0) { + result = nil; + } + } + return result; +} + +#pragma mark Partial - Fields + +- (NSString *)fieldsDescription { + NSString *str = [GTLRObject fieldsDescriptionForJSON:self.JSON]; + return str; +} + ++ (NSString *)fieldsDescriptionForJSON:(NSDictionary *)targetJSON { + // Internal routine: recursively generate a string field description + // by joining elements + NSArray *array = [self fieldsElementsForJSON:targetJSON]; + NSString *str = [array componentsJoinedByString:@","]; + return str; +} + ++ (NSArray *)fieldsElementsForJSON:(NSDictionary *)targetJSON { + // Internal routine: recursively generate an array of field description + // element strings + NSMutableArray *resultFields = [NSMutableArray array]; + + // Sorting the dictionary keys gives us deterministic results when iterating + NSArray *sortedKeys = [targetJSON.allKeys sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; + for (NSString *key in sortedKeys) { + // We'll build a comma-separated list of fields + id value = [targetJSON objectForKey:key]; + if ([value isKindOfClass:[NSString class]] + || [value isKindOfClass:[NSNumber class]]) { + // Basic type (string, number), so the key is what we want + [resultFields addObject:key]; + } else if ([value isKindOfClass:[NSDictionary class]]) { + // Object (dictionary): "parent/child1,parent/child2,parent/child3" + NSArray *subElements = [self fieldsElementsForJSON:value]; + for (NSString *subElem in subElements) { + NSString *prepended = [NSString stringWithFormat:@"%@/%@", + key, subElem]; + [resultFields addObject:prepended]; + } + } else if ([value isKindOfClass:[NSArray class]]) { + // Array; we'll generate from the first array entry: + // "parent(child1,child2,child3)" + // + // Open question: should this instead create the union of elements for + // all items in the array, rather than just get fields from the first + // array object? + if (((NSArray *)value).count > 0) { + id firstObj = [value objectAtIndex:0]; + if ([firstObj isKindOfClass:[NSDictionary class]]) { + // An array of objects + NSString *contentsStr = [self fieldsDescriptionForJSON:firstObj]; + NSString *encapsulated = [NSString stringWithFormat:@"%@(%@)", + key, contentsStr]; + [resultFields addObject:encapsulated]; + } else { + // An array of some basic type, or of arrays + [resultFields addObject:key]; + } + } + } else { + GTLR_ASSERT(0, @"GTLRObject unknown field element for %@ (%@)", + key, NSStringFromClass([value class])); + } + } + return resultFields; +} + +#pragma mark Partial - Patch + +- (id)patchObjectFromOriginal:(GTLRObject *)original { + GTLRObject *resultObj; + NSMutableDictionary *resultJSON = [GTLRObject patchDictionaryForJSON:self.JSON + fromOriginalJSON:original.JSON]; + if (resultJSON.count > 0) { + // Avoid an extra copy by assigning the JSON directly rather than using +objectWithJSON: + resultObj = [[self class] object]; + resultObj.JSON = resultJSON; + } else { + // Client apps should not attempt to patch with an object containing + // empty JSON + resultObj = nil; + } + return resultObj; +} + ++ (NSMutableDictionary *)patchDictionaryForJSON:(NSDictionary *)newJSON + fromOriginalJSON:(NSDictionary *)originalJSON { + // Internal recursive routine to create an object suitable for + // our patch semantics + NSMutableDictionary *resultJSON = [NSMutableDictionary dictionary]; + + // Iterate through keys present in the old object + NSArray *originalKeys = originalJSON.allKeys; + for (NSString *key in originalKeys) { + id originalValue = [originalJSON objectForKey:key]; + id newValue = [newJSON valueForKey:key]; + if (newValue == nil) { + // There is no new value for this key, so set the value to NSNull + [resultJSON setValue:[NSNull null] forKey:key]; + } else if (!GTLR_AreEqualOrBothNil(originalValue, newValue)) { + // The values for this key differ + if ([originalValue isKindOfClass:[NSDictionary class]] + && [newValue isKindOfClass:[NSDictionary class]]) { + // Both are objects; recurse + NSMutableDictionary *subDict = [self patchDictionaryForJSON:newValue + fromOriginalJSON:originalValue]; + [resultJSON setValue:subDict forKey:key]; + } else { + // They are non-object values; the new replaces the old. Per the + // documentation for patch, this replaces entire arrays. + [resultJSON setValue:newValue forKey:key]; + } + } else { + // The values are the same; omit this key-value pair + } + } + + // Iterate through keys present only in the new object, and add them to the + // result + NSMutableArray *newKeys = [NSMutableArray arrayWithArray:newJSON.allKeys]; + [newKeys removeObjectsInArray:originalKeys]; + + for (NSString *key in newKeys) { + id value = [newJSON objectForKey:key]; + [resultJSON setValue:value forKey:key]; + } + return resultJSON; +} + ++ (id)nullValue { + return [NSNull null]; +} + +#pragma mark Additional Properties + +- (id)additionalPropertyForName:(NSString *)name { + // Return the cached object, if any, before creating one. + id result = [self cacheChildForKey:name]; + if (result != nil) { + return result; + } + + Class defaultClass = [[self class] classForAdditionalProperties]; + id jsonObj = [self JSONValueForKey:name]; + BOOL shouldCache = NO; + if (jsonObj != nil) { + id<GTLRObjectClassResolver>objectClassResolver = self.objectClassResolver; + result = [GTLRRuntimeCommon objectFromJSON:jsonObj + defaultClass:defaultClass + objectClassResolver:objectClassResolver + isCacheable:&shouldCache]; + } + + [self setCacheChild:(shouldCache ? result : nil) + forKey:name]; + return result; +} + +- (void)setAdditionalProperty:(id)obj forName:(NSString *)name { + BOOL shouldCache = NO; + Class defaultClass = [[self class] classForAdditionalProperties]; + id json = [GTLRRuntimeCommon jsonFromAPIObject:obj + expectedClass:defaultClass + isCacheable:&shouldCache]; + [self setJSONValue:json forKey:name]; + [self setCacheChild:(shouldCache ? obj : nil) + forKey:name]; +} + +- (NSDictionary<NSString *, id> *)additionalProperties { + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + + NSArray *propertyNames = [self additionalJSONKeys]; + for (NSString *name in propertyNames) { + id obj = [self additionalPropertyForName:name]; + [result setObject:obj forKey:name]; + } + + return result; +} + +#pragma mark Child Cache methods + +// There is no property for _childCache as there shouldn't be KVC/KVO +// support for it, it's an implementation detail. + +- (void)setCacheChild:(id)obj forKey:(NSString *)key { + if (_childCache == nil && obj != nil) { + _childCache = [[NSMutableDictionary alloc] initWithObjectsAndKeys: + obj, key, nil]; + } else { + [_childCache setValue:obj forKey:key]; + } +} + +- (id)cacheChildForKey:(NSString *)key { + id obj = [_childCache objectForKey:key]; + return obj; +} + +#pragma mark Support methods + ++ (NSMutableArray *)allDeclaredProperties { + NSMutableArray *array = [NSMutableArray array]; + + // walk from this class up the hierarchy to GTLRObject + Class topClass = class_getSuperclass([GTLRObject class]); + for (Class currClass = self; + currClass != topClass; + currClass = class_getSuperclass(currClass)) { + // step through this class's properties, and add the property names to the + // array + objc_property_t *properties = class_copyPropertyList(currClass, NULL); + if (properties) { + for (objc_property_t *prop = properties; + *prop != NULL; + ++prop) { + const char *propName = property_getName(*prop); + // We only want dynamic properties; their attributes contain ",D". + const char *attr = property_getAttributes(*prop); + const char *dynamicMarker = strstr(attr, ",D"); + if (dynamicMarker && + (dynamicMarker[2] == 0 || dynamicMarker[2] == ',' )) { + [array addObject:(id _Nonnull)@(propName)]; + } + } + free(properties); + } + } + return array; +} + ++ (NSArray *)allKnownKeys { + NSArray *allProps = [self allDeclaredProperties]; + NSMutableArray *knownKeys = [NSMutableArray arrayWithArray:allProps]; + + NSDictionary *propMap = [GTLRObject propertyToJSONKeyMapForClass:[self class]]; + + NSUInteger idx = 0; + for (NSString *propName in allProps) { + NSString *jsonKey = [propMap objectForKey:propName]; + if (jsonKey) { + [knownKeys replaceObjectAtIndex:idx + withObject:jsonKey]; + } + ++idx; + } + return knownKeys; +} + +- (NSString *)description { + NSString *jsonDesc = [self JSONDescription]; + + NSString *str = [NSString stringWithFormat:@"%@ %p: %@", + [self class], self, jsonDesc]; + return str; +} + +// Internal utility for creating an appropriate description summary for the object's JSON. +- (NSString *)JSONDescription { + // Find the list of declared and otherwise known JSON keys for this class. + NSArray *knownKeys = [[self class] allKnownKeys]; + + NSMutableString *descStr = [NSMutableString stringWithString:@"{"]; + + NSString *spacer = @""; + for (NSString *key in [[_json allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]) { + NSString *value = nil; + // show question mark for JSON keys not supported by a declared property: + // foo?:"Hi mom." + NSString *qmark = [knownKeys containsObject:key] ? @"" : @"?"; + + // determine property value to dislay + id rawValue = [_json valueForKey:key]; + if ([rawValue isKindOfClass:[NSDictionary class]]) { + // for dictionaries, show the list of keys: + // {key1,key2,key3} + NSArray *subKeys = [((NSDictionary *)rawValue).allKeys sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; + NSString *subkeyList = [subKeys componentsJoinedByString:@","]; + value = [NSString stringWithFormat:@"{%@}", subkeyList]; + } else if ([rawValue isKindOfClass:[NSArray class]]) { + // for arrays, show the number of items in the array: + // [3] + value = [NSString stringWithFormat:@"[%tu]", ((NSArray *)rawValue).count]; + } else if ([rawValue isKindOfClass:[NSString class]]) { + // for strings, show the string in quotes: + // "Hi mom." + value = [NSString stringWithFormat:@"\"%@\"", rawValue]; + } else { + // for numbers, show just the number + value = [rawValue description]; + } + [descStr appendFormat:@"%@%@%@:%@", spacer, key, qmark, value]; + spacer = @" "; + } + [descStr appendString:@"}"]; + return descStr; +} + +#pragma mark Object Instantiation + ++ (GTLRObject *)objectForJSON:(NSMutableDictionary *)json + defaultClass:(Class)defaultClass + objectClassResolver:(id<GTLRObjectClassResolver>)objectClassResolver { + if (((id)json == [NSNull null]) || json.count == 0) { + if (json != nil && defaultClass != Nil) { + // The JSON included an empty dictionary, just create the object. + Class classToCreate = + GTLRObjectResolveClass(objectClassResolver, + [NSDictionary dictionary], + defaultClass); + return [classToCreate object]; + } + // No actual result, such as the response from a delete. + return nil; + } + + if (defaultClass == Nil) { + defaultClass = self; + } + + Class classToCreate = + GTLRObjectResolveClass(objectClassResolver, json, defaultClass); + + // now instantiate the GTLRObject + GTLRObject *parsedObject = [classToCreate object]; + parsedObject.objectClassResolver = objectClassResolver; + parsedObject.JSON = json; + return parsedObject; +} + +#pragma mark Runtime Utilities + +static NSMutableDictionary *gJSONKeyMapCache = nil; +static NSMutableDictionary *gArrayPropertyToClassMapCache = nil; + ++ (void)initialize { + // Note that initialize is guaranteed by the runtime to be called in a + // thread-safe manner + if (gJSONKeyMapCache == nil) { + gJSONKeyMapCache = [[NSMutableDictionary alloc] init]; + } + if (gArrayPropertyToClassMapCache == nil) { + gArrayPropertyToClassMapCache = [[NSMutableDictionary alloc] init]; + } +} + ++ (NSDictionary *)propertyToJSONKeyMapForClass:(Class<GTLRRuntimeCommon>)aClass { + NSDictionary *resultMap = + [GTLRRuntimeCommon mergedClassDictionaryForSelector:@selector(propertyToJSONKeyMap) + startClass:aClass + ancestorClass:[GTLRObject class] + cache:gJSONKeyMapCache]; + return resultMap; +} + ++ (NSDictionary *)arrayPropertyToClassMapForClass:(Class<GTLRRuntimeCommon>)aClass { + NSDictionary *resultMap = + [GTLRRuntimeCommon mergedClassDictionaryForSelector:@selector(arrayPropertyToClassMap) + startClass:aClass + ancestorClass:[GTLRObject class] + cache:gArrayPropertyToClassMapCache]; + return resultMap; +} + +#pragma mark Runtime Support + ++ (Class<GTLRRuntimeCommon>)ancestorClass { + return [GTLRObject class]; +} + ++ (BOOL)resolveInstanceMethod:(SEL)sel { + BOOL resolved = [GTLRRuntimeCommon resolveInstanceMethod:sel onClass:self]; + if (resolved) + return YES; + + return [super resolveInstanceMethod:sel]; +} + +@end + +@implementation GTLRCollectionObject + ++ (NSString *)collectionItemsKey { + // GTLRCollectionObject fast enumeration, indexed access, and automatic pagination + // (when shouldFetchNextPages is enabled) applies to the object array property "items". + // The array property's key may be different if subclasses override this method. + return @"items"; +} + +- (id)objectAtIndexedSubscript:(NSUInteger)idx { + NSString *key = [[self class] collectionItemsKey]; + NSArray *items = [self valueForKey:key]; + if (items == nil) { + [NSException raise:NSRangeException + format:@"index %tu beyond bounds (%@ property \"%@\" is nil)", + idx, [self class], key]; + } + id result = [items objectAtIndexedSubscript:idx]; + return result; +} + +// NSFastEnumeration protocol +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state + objects:(__unsafe_unretained id _Nonnull *)stackbuf + count:(NSUInteger)len { + NSString *key = [[self class] collectionItemsKey]; + NSArray *items = [self valueForKey:key]; + NSUInteger result = [items countByEnumeratingWithState:state + objects:stackbuf + count:len]; + return result; +} + +@end + +@implementation GTLRDataObject + +@synthesize data = _data, + contentType = _contentType; + +- (NSString *)description { + NSString *jsonDesc = @""; + if (self.JSON.count > 0) { + jsonDesc = [self JSONDescription]; + } + return [NSString stringWithFormat:@"%@ %p: %tu bytes, contentType:%@ %@", + [self class], self, self.data.length, self.contentType, jsonDesc]; +} + +- (id)copyWithZone:(NSZone *)zone { + GTLRDataObject *newObj = [super copyWithZone:zone]; + newObj.data = [self.data copy]; + newObj.contentType = self.contentType; + return newObj; +} + +@end + +@implementation GTLRResultArray + +- (NSArray *)itemsWithItemClass:(Class)itemClass { + // Return the cached array before creating on demand. + NSString *cacheKey = @"result_array_items"; + NSMutableArray *cachedArray = [self cacheChildForKey:cacheKey]; + if (cachedArray != nil) { + return cachedArray; + } + NSArray *result = nil; + NSArray *array = (NSArray *)self.JSON; + if (array != nil) { + if ([array isKindOfClass:[NSArray class]]) { + id<GTLRObjectClassResolver>objectClassResolver = self.objectClassResolver; + result = [GTLRRuntimeCommon objectFromJSON:array + defaultClass:itemClass + objectClassResolver:objectClassResolver + isCacheable:NULL]; + } else { +#if DEBUG + if (![array isKindOfClass:[NSNull class]]) { + GTLR_DEBUG_LOG(@"GTLRObject: unexpected JSON: %@ should be an array, actually is a %@:\n%@", + NSStringFromClass([self class]), + NSStringFromClass([array class]), + array); + } +#endif + result = array; + } + } + + [self setCacheChild:result forKey:cacheKey]; + return result; +} + +- (NSString *)JSONDescription { + // Just like GTLRObject's handing of arrays, just return the count. + return [NSString stringWithFormat:@"[%tu]", self.JSON.count]; +} + +@end + +Class GTLRObjectResolveClass( + id<GTLRObjectClassResolver>objectClassResolver, + NSDictionary *json, + Class defaultClass) { + Class result = [objectClassResolver classForJSON:json + defaultClass:defaultClass]; + if (result == Nil) { + result = defaultClass; + } + return result; +} + +@implementation GTLRObjectClassResolver { + NSDictionary<NSString *, Class> *_kindToClassMap; + NSDictionary<Class, Class> *_surrogates; +} + ++ (instancetype)resolverWithKindMap:(NSDictionary<NSString *, Class> *)kindStringToClassMap { + GTLRObjectClassResolver *result = [[self alloc] initWithKindMap:kindStringToClassMap + surrogates:nil]; + return result; +} + ++ (instancetype)resolverWithKindMap:(NSDictionary<NSString *, Class> *)kindStringToClassMap + surrogates:(NSDictionary<Class, Class> *)surrogates { + GTLRObjectClassResolver *result = [[self alloc] initWithKindMap:kindStringToClassMap + surrogates:surrogates]; + return result; +} + +- (instancetype)initWithKindMap:(NSDictionary<NSString *, Class> *)kindStringToClassMap + surrogates:(NSDictionary<Class, Class> *)surrogates { + self = [super init]; + if (self) { + _kindToClassMap = [kindStringToClassMap copy]; + _surrogates = [surrogates copy]; + } + return self; +} + +- (Class)classForJSON:(NSDictionary *)json + defaultClass:(Class)defaultClass { + Class result = defaultClass; + + // Apply kind map. + BOOL shouldUseKind = (result == Nil) || [result isKindValidForClassRegistry]; + if (shouldUseKind && [json isKindOfClass:[NSDictionary class]]) { + NSString *kind = [json valueForKey:@"kind"]; + if ([kind isKindOfClass:[NSString class]] && kind.length > 0) { + Class dynamicClass = [_kindToClassMap objectForKey:kind]; + if (dynamicClass) { + result = dynamicClass; + } + } + } + + // Apply surrogate map. + Class surrogate = [_surrogates objectForKey:result]; + if (surrogate) { + result = surrogate; + } + + return result; +} + +@end + +static NSMutableDictionary *DeepMutableCopyOfJSONDictionary(NSDictionary *initialJSON) { + if (!initialJSON) return nil; + + NSMutableDictionary *result; + CFPropertyListRef ref = CFPropertyListCreateDeepCopy(kCFAllocatorDefault, + (__bridge CFPropertyListRef)(initialJSON), + kCFPropertyListMutableContainers); + if (ref) { + result = CFBridgingRelease(ref); + } else { + // Failed to copy, probably due to a non-plist type such as NSNull. + // + // As a fallback, round-trip through NSJSONSerialization. + NSError *serializationError; + NSData *data = [NSJSONSerialization dataWithJSONObject:initialJSON + options:0 + error:&serializationError]; + if (!data) { + GTLR_DEBUG_ASSERT(0, @"Copy failed due to serialization: %@\nJSON: %@", + serializationError, initialJSON); + } else { + result = [NSJSONSerialization JSONObjectWithData:data + options:NSJSONReadingMutableContainers + error:&serializationError]; + GTLR_DEBUG_ASSERT(result != nil, @"Copy failed due to deserialization: %@\nJSON: %@", + serializationError, + [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); + } + } + return result; +} diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRQuery.h b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRQuery.h @@ -0,0 +1,253 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Query documentation: +// https://github.com/google/google-api-objectivec-client-for-rest/wiki#query-operations + +#import "GTLRObject.h" +#import "GTLRUploadParameters.h" + +NS_ASSUME_NONNULL_BEGIN + +@class GTLRServiceTicket; +@class GTLRServiceExecutionParameters; +@class GTLRQuery; + +/** + * This protocol is just to support passing of either a batch or a single query + * to a GTLRService instance. The library does not expect or support client app + * implementations of this protocol. + */ +@protocol GTLRQueryProtocol <NSObject, NSCopying> + +/** + * Service ticket values may be set in the execution parameters for an individual query + * prior to executing the query. + */ +@property(atomic, strong, null_resettable) GTLRServiceExecutionParameters *executionParameters; + +- (BOOL)isBatchQuery; +- (BOOL)hasExecutionParameters; +- (BOOL)shouldSkipAuthorization; +- (void)invalidateQuery; +- (nullable NSDictionary<NSString *, NSString *> *)additionalHTTPHeaders; +- (nullable NSDictionary<NSString *, NSString *> *)additionalURLQueryParameters; +- (nullable NSString *)loggingName; +- (nullable GTLRUploadParameters *)uploadParameters; + +@end + +@protocol GTLRQueryCollectionProtocol +@optional +@property(nonatomic, strong) NSString *pageToken; +@end + +/** + * A block called when a query completes executing. + * + * Errors passed to the completionBlock will have an "underlying" GTLRErrorObject + * when the server returned an error for this specific query: + * + * GTLRErrorObject *errorObj = [GTLRErrorObject underlyingObjectForError:callbackError]; + * if (errorObj) { + * // The server returned this error for this specific query. + * } else { + * // The query execution fetch failed. + * } + * + * @param callbackTicket The ticket that tracked query execution. + * @param object The result of query execution. This will be derived from + * GTLRObject. + * @param callbackError If non-nil, the query execution failed. + */ +typedef void (^GTLRQueryCompletionBlock)(GTLRServiceTicket *callbackTicket, + id _Nullable object, + NSError * _Nullable callbackError); + +/** + * Class for a single query. + */ +@interface GTLRQuery : NSObject <GTLRQueryProtocol, NSCopying> + +/** + * The object to be uploaded with the query. The JSON of this object becomes + * the body for PUT and POST requests. + */ +@property(atomic, strong, nullable) GTLRObject *bodyObject; + +/** + * Each query must have a request ID string. The client app may replace the + * default assigned request ID with a custom string, provided that if + * used in a batch query, all request IDs in the batch must be unique. + */ +@property(atomic, copy) NSString *requestID; + +/** + * For queries which support file upload, the MIME type and file URL + * or data must be provided. + */ +@property(atomic, copy, nullable) GTLRUploadParameters *uploadParameters; + +/** + * Any additional URL query parameters for this query. + * + * These query parameters override the same keys from the service object's + * additionalURLQueryParameters + */ +@property(atomic, copy, nullable) NSDictionary<NSString *, NSString *> *additionalURLQueryParameters; + +/** + * Any additional HTTP headers for this query. + * + * These headers override the same keys from the service object's additionalHTTPHeaders + */ +@property(atomic, copy, nullable) NSDictionary<NSString *, NSString *> *additionalHTTPHeaders; + +/** + * If set, when the query is executed, an @c "alt" query parameter is added + * with this value and the raw result of the query is returned in a + * GTLRDataObject. This is useful when the server documents result datatypes + * other than JSON ("csv", for example). + */ +@property(atomic, copy) NSString *downloadAsDataObjectType; + +/** + * If set, and the query also has a non-empty @c downloadAsDataObjectType, the + * URL to download from will be modified to include "download/". This extra path + * component avoids the need for a server redirect to the download URL. + */ +@property(atomic, assign) BOOL useMediaDownloadService; + +/** + * Clients may set this to YES to disallow authorization. Defaults to NO. + */ +@property(atomic, assign) BOOL shouldSkipAuthorization; + +/** + * An optional callback block to be called immediately before the executeQuery: completion handler. + * + * The completionBlock property is particularly useful for queries executed in a batch. + */ +@property(atomic, copy, nullable) GTLRQueryCompletionBlock completionBlock; + +/** + * The brief string to identify this query in GTMSessionFetcher http logs. + * + * A default logging name is set by the code generator, but may be overridden by the client app. + */ +@property(atomic, copy, nullable) NSString *loggingName; + +#pragma mark Internal +///////////////////////////////////////////////////////////////////////////////////////////// +// +// Properties below are used by the library and aren't typically needed by client apps. +// +///////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The URITemplate path segment. This is initialized in by the service generator. + */ +@property(atomic, readonly) NSString *pathURITemplate; + +/** + * The HTTP method to use for this query. This is initialized in by the service generator. + */ +@property(atomic, readonly, nullable) NSString *httpMethod; + +/** + * The parameters names that are in the URI Template. + * This is initialized in by the service generator. + * + * The service generator collects these via the discovery info instead of having to parse the + * template to figure out what is part of the path. + */ +@property(atomic, readonly, nullable) NSArray<NSString *> *pathParameterNames; + +/** + * The JSON dictionary of all the parameters set on this query. + * + * The JSON values are set by setting the query's properties. + */ +@property(nonatomic, strong, nullable) NSMutableDictionary<NSString *, id> *JSON; + +/** + * A custom URI template for resumable uploads. This is initialized by the service generator + * if needed. + */ +@property(atomic, copy, nullable) NSString *resumableUploadPathURITemplateOverride; + +/** + * A custom URI template for simple and multipart media uploads. This is initialized + * by the service generator. + */ +@property(atomic, copy, nullable) NSString *simpleUploadPathURITemplateOverride; + +/** + * The GTLRObject subclass expected for results. This is initialized by the service generator. + * + * This is needed if the object returned by the server lacks a known "kind" string. + */ +@property(atomic, assign, nullable) Class expectedObjectClass; + +/** + * Set when the query has been invalidated, meaning it was slated for execution so it's been copied + * and its callbacks were released, or it's a copy that has finished executing. + * + * Once a query has been invalidated, it cannot be executed, added to a batch, or copied. + */ +@property(atomic, assign, getter=isQueryInvalid) BOOL queryInvalid; + +/** + * Internal query init method. + * + * @param pathURITemplate URI template to be filled in with parameters. + * @param httpMethod The requests's http method. A nil method will execute as GET. + * @param pathParameterNames Names of parameters to be replaced in the template. + */ +- (instancetype)initWithPathURITemplate:(NSString *)pathURITemplate + HTTPMethod:(nullable NSString *)httpMethod + pathParameterNames:(nullable NSArray<NSString *> *)pathParameterNames NS_DESIGNATED_INITIALIZER; + +/** + * @return Auto-generated request ID string. + */ ++ (NSString *)nextRequestID; + +/** + * Overridden by subclasses. + * + * @return Substitute parameter names where needed for Objective-C or library compatibility. + */ ++ (nullable NSDictionary<NSString *, NSString *> *)parameterNameMap; + +/** + * Overridden by subclasses. + * + * @return Map of property keys to specifying the class of objects to be instantiated in arrays. + */ ++ (nullable NSDictionary<NSString *, Class> *)arrayPropertyToClassMap; + +- (instancetype)init NS_UNAVAILABLE; + +@end + +/** + * The library doesn't use GTLRQueryCollectionImpl, but it provides a concrete implementation + * of the protocol so the methods do not cause private method errors in Xcode/AppStore review. + */ +@interface GTLRQueryCollectionImpl : GTLRQuery <GTLRQueryCollectionProtocol> +@end + +NS_ASSUME_NONNULL_END diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRQuery.m b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRQuery.m @@ -0,0 +1,313 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !__has_feature(objc_arc) +#error "This file needs to be compiled with ARC enabled." +#endif + +#include <objc/runtime.h> + +#import "GTLRQuery.h" +#import "GTLRRuntimeCommon.h" +#import "GTLRService.h" +#import "GTLRUtilities.h" + +@interface GTLRQuery () <GTLRRuntimeCommon> +@end + +@implementation GTLRQuery { + NSMutableDictionary *_childCache; + GTLRServiceExecutionParameters *_executionParameters; +} + +@synthesize additionalURLQueryParameters = _additionalURLQueryParameters, + additionalHTTPHeaders = _additionalHTTPHeaders, + bodyObject = _bodyObject, + completionBlock = _completionBlock, + downloadAsDataObjectType = _downloadAsDataObjectType, + expectedObjectClass = _expectedObjectClass, + httpMethod = _httpMethod, + JSON = _json, + loggingName = _loggingName, + pathParameterNames = _pathParameterNames, + pathURITemplate = _pathURITemplate, + queryInvalid = _queryInvalid, + requestID = _requestID, + resumableUploadPathURITemplateOverride = _resumableUploadPathURITemplateOverride, + shouldSkipAuthorization = _shouldSkipAuthorization, + simpleUploadPathURITemplateOverride = _simpleUploadPathURITemplateOverride, + uploadParameters = _uploadParameters, + useMediaDownloadService = _useMediaDownloadService; + +#if DEBUG +- (instancetype)init { + [self doesNotRecognizeSelector:_cmd]; + self = nil; + return self; +} +#endif + +- (instancetype)initWithPathURITemplate:(NSString *)pathURITemplate + HTTPMethod:(nullable NSString *)httpMethod + pathParameterNames:(nullable NSArray<NSString *> *)pathParameterNames { + self = [super init]; + if (self) { + _requestID = [[self class] nextRequestID]; + + _pathURITemplate = [pathURITemplate copy]; + _httpMethod = [httpMethod copy]; + _pathParameterNames = [pathParameterNames copy]; + + if (_pathURITemplate.length == 0) { + self = nil; + } + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + GTLR_DEBUG_ASSERT(!self.queryInvalid, @"Cannot copy an executed query: %@", self); + + GTLRQuery *query = + [[[self class] allocWithZone:zone] initWithPathURITemplate:self.pathURITemplate + HTTPMethod:self.httpMethod + pathParameterNames:self.pathParameterNames]; + + if (_json.count > 0) { + // Deep copy the parameters + CFPropertyListRef ref = CFPropertyListCreateDeepCopy(kCFAllocatorDefault, + (__bridge CFPropertyListRef)(_json), + kCFPropertyListMutableContainers); + query.JSON = CFBridgingRelease(ref); + } + + // Using the executionParameters ivar avoids creating the object. + query.executionParameters = self.executionParameters; + + // Copied in the same order as synthesized above. + query.additionalHTTPHeaders = self.additionalHTTPHeaders; + query.additionalURLQueryParameters = self.additionalURLQueryParameters; + query.bodyObject = self.bodyObject; + query.completionBlock = self.completionBlock; + query.downloadAsDataObjectType = self.downloadAsDataObjectType; + query.expectedObjectClass = self.expectedObjectClass; + // http method passed to init above. + // JSON copied above. + query.loggingName = self.loggingName; + // pathParameterNames passed to init above. + // pathURITemplate passed to init above. + query.queryInvalid = self.queryInvalid; + query.requestID = self.requestID; + query.resumableUploadPathURITemplateOverride = self.resumableUploadPathURITemplateOverride; + query.shouldSkipAuthorization = self.shouldSkipAuthorization; + query.simpleUploadPathURITemplateOverride = self.simpleUploadPathURITemplateOverride; + query.uploadParameters = self.uploadParameters; + query.useMediaDownloadService = self.useMediaDownloadService; + + return query; +} + +#if DEBUG +- (NSString *)description { + NSArray *keys = self.JSON.allKeys; + NSArray *params = [keys sortedArrayUsingSelector:@selector(compare:)]; + NSString *paramsSummary = @""; + if (params.count > 0) { + paramsSummary = [NSString stringWithFormat:@" params:(%@)", + [params componentsJoinedByString:@","]]; + } + + NSString *invalidStr = @""; + if (self.queryInvalid) { + invalidStr = @" [callbacks released]"; + } + + keys = self.additionalURLQueryParameters.allKeys; + NSArray *urlQParams = [keys sortedArrayUsingSelector:@selector(compare:)]; + NSString *urlQParamsSummary = @""; + if (urlQParams.count > 0) { + urlQParamsSummary = [NSString stringWithFormat:@" urlQParams:(%@)", + [urlQParams componentsJoinedByString:@","]]; + } + + GTLRObject *bodyObj = self.bodyObject; + NSString *bodyObjSummary = @""; + if (bodyObj != nil) { + bodyObjSummary = [NSString stringWithFormat:@" bodyObject:%@", [bodyObj class]]; + } + + NSString *uploadStr = @""; + GTLRUploadParameters *uploadParams = self.uploadParameters; + if (uploadParams) { + uploadStr = [NSString stringWithFormat:@" %@", uploadParams]; + } + + NSString *httpMethod = self.httpMethod; + if (httpMethod == nil) { + httpMethod = @"GET"; + } + + NSString *dataObjectType = self.downloadAsDataObjectType; + NSString *downloadStr = @""; + if (dataObjectType.length > 0) { + downloadStr = + [NSString stringWithFormat:@" downloadDataAs:%@", dataObjectType]; + } + + return [NSString stringWithFormat:@"%@ %p:%@%@ {%@ pathTemplate:%@%@%@%@%@}", + [self class], self, invalidStr, downloadStr, + httpMethod, self.pathURITemplate, + paramsSummary, urlQParamsSummary, bodyObjSummary, uploadStr]; +} +#endif // DEBUG + +- (BOOL)isBatchQuery { + return NO; +} + +- (void)invalidateQuery { + self.queryInvalid = YES; + self.completionBlock = nil; + self.executionParameters = nil; +} + +- (GTLRServiceExecutionParameters *)executionParameters { + @synchronized(self) { + if (!_executionParameters) { + _executionParameters = [[GTLRServiceExecutionParameters alloc] init]; + } + return _executionParameters; + } +} + +- (void)setExecutionParameters:(nullable GTLRServiceExecutionParameters *)executionParameters { + @synchronized(self) { + _executionParameters = executionParameters; + } +} + +- (BOOL)hasExecutionParameters { + return self.executionParameters.hasParameters; +} + ++ (NSString *)nextRequestID { + static NSUInteger lastRequestID = 0; + NSString *result; + + @synchronized([GTLRQuery class]) { + ++lastRequestID; + result = [NSString stringWithFormat:@"gtlr_%tu", lastRequestID]; + } + return result; +} + +#pragma mark GTLRRuntimeCommon Support + +- (void)setJSONValue:(id)obj forKey:(NSString *)key { + NSMutableDictionary *dict = self.JSON; + if (dict == nil && obj != nil) { + dict = [NSMutableDictionary dictionaryWithCapacity:1]; + self.JSON = dict; + } + [dict setValue:obj forKey:key]; +} + +- (id)JSONValueForKey:(NSString *)key { + id obj = [self.JSON objectForKey:key]; + return obj; +} + +// There is no property for _childCache as there shouldn't be KVC/KVO +// support for it, since it's an implementation detail. + +- (void)setCacheChild:(id)obj forKey:(NSString *)key { + if (_childCache == nil && obj != nil) { + _childCache = [[NSMutableDictionary alloc] initWithObjectsAndKeys:obj, key, nil]; + } else { + [_childCache setValue:obj forKey:key]; + } +} + +- (id)cacheChildForKey:(NSString *)key { + id obj = [_childCache objectForKey:key]; + return obj; +} + +#pragma mark Methods for Subclasses to Override + ++ (NSDictionary<NSString *, NSString *> *)parameterNameMap { + return nil; +} + ++ (NSDictionary<NSString *, Class> *)arrayPropertyToClassMap { + return nil; +} + +#pragma mark Runtime Utilities + +static NSMutableDictionary *gQueryParameterNameMapCache = nil; +static NSMutableDictionary *gQueryArrayPropertyToClassMapCache = nil; + ++ (void)initialize { + // Note that +initialize is guaranteed by the runtime to be called in a thread-safe manner. + if (gQueryParameterNameMapCache == nil) { + gQueryParameterNameMapCache = [[NSMutableDictionary alloc] init]; + } + if (gQueryArrayPropertyToClassMapCache == nil) { + gQueryArrayPropertyToClassMapCache = [[NSMutableDictionary alloc] init]; + } +} + ++ (NSDictionary *)propertyToJSONKeyMapForClass:(Class<GTLRRuntimeCommon>)aClass { + NSDictionary *resultMap = + [GTLRRuntimeCommon mergedClassDictionaryForSelector:@selector(parameterNameMap) + startClass:aClass + ancestorClass:[GTLRQuery class] + cache:gQueryParameterNameMapCache]; + return resultMap; +} + ++ (NSDictionary *)arrayPropertyToClassMapForClass:(Class<GTLRRuntimeCommon>)aClass { + NSDictionary *resultMap = + [GTLRRuntimeCommon mergedClassDictionaryForSelector:@selector(arrayPropertyToClassMap) + startClass:aClass + ancestorClass:[GTLRQuery class] + cache:gQueryArrayPropertyToClassMapCache]; + return resultMap; +} + +#pragma mark Runtime Support + +- (id<GTLRObjectClassResolver>)objectClassResolver { + // Stub method just needed for RuntimeCommon. + return nil; +} + ++ (Class<GTLRRuntimeCommon>)ancestorClass { + return [GTLRQuery class]; +} + ++ (BOOL)resolveInstanceMethod:(SEL)sel { + BOOL resolved = [GTLRRuntimeCommon resolveInstanceMethod:sel onClass:self]; + if (resolved) return YES; + + return [super resolveInstanceMethod:sel]; +} + +@end + +@implementation GTLRQueryCollectionImpl +@dynamic pageToken; +@end diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRRuntimeCommon.h b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRRuntimeCommon.h @@ -0,0 +1,73 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#import "GTLRDefines.h" + +@protocol GTLRObjectClassResolver; + +NS_ASSUME_NONNULL_BEGIN + +// This protocol and support class are an internal implementation detail so +// GTLRObject and GTLRQuery can share some code. + +/** + * An internal protocol for the GTLR library. + * + * None of these methods should be used by client apps. + */ +@protocol GTLRRuntimeCommon <NSObject> +@required +// Get/Set properties +- (void)setJSONValue:(nullable id)obj forKey:(NSString *)key; +- (id)JSONValueForKey:(NSString *)key; +// Child cache +- (void)setCacheChild:(nullable id)obj forKey:(NSString *)key; +- (nullable id)cacheChildForKey:(NSString *)key; +// Object mapper. +- (nullable id<GTLRObjectClassResolver>)objectClassResolver; +// Key map ++ (nullable NSDictionary<NSString *, NSString *> *)propertyToJSONKeyMapForClass:(Class<GTLRRuntimeCommon>)aClass; +// Array item types ++ (nullable NSDictionary<NSString *, Class> *)arrayPropertyToClassMapForClass:(Class<GTLRRuntimeCommon>)aClass; +// The parent class for dynamic support ++ (nullable Class<GTLRRuntimeCommon>)ancestorClass; +@end + +/** + * An internal class for the GTLR library. + * + * None of these methods should be used by client apps. + */ +@interface GTLRRuntimeCommon : NSObject +// Wire things up. ++ (BOOL)resolveInstanceMethod:(SEL)sel onClass:(Class)onClass; +// Helpers ++ (nullable id)objectFromJSON:(id)json + defaultClass:(nullable Class)defaultClass + objectClassResolver:(id<GTLRObjectClassResolver>)objectClassResolver + isCacheable:(nullable BOOL *)isCacheable; ++ (nullable id)jsonFromAPIObject:(id)obj + expectedClass:(nullable Class)expectedClass + isCacheable:(nullable BOOL *)isCacheable; +// Walk up the class tree merging dictionaries and return the result. ++ (NSDictionary *)mergedClassDictionaryForSelector:(SEL)selector + startClass:(Class)startClass + ancestorClass:(Class)ancestorClass + cache:(NSMutableDictionary *)cache; +@end + +NS_ASSUME_NONNULL_END diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRRuntimeCommon.m b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRRuntimeCommon.m @@ -0,0 +1,1060 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !__has_feature(objc_arc) +#error "This file needs to be compiled with ARC enabled." +#endif + +#include <objc/runtime.h> +#include <TargetConditionals.h> + +#import "GTLRRuntimeCommon.h" + +#import "GTLRDateTime.h" +#import "GTLRDuration.h" +#import "GTLRObject.h" +#import "GTLRUtilities.h" + +// Note: NSObject's class is used as a marker for the expected/default class +// when Discovery says it can be any type of object. + +@implementation GTLRRuntimeCommon + +// Helper to generically convert JSON to an api object type. ++ (id)objectFromJSON:(id)json + defaultClass:(Class)defaultClass + objectClassResolver:(id<GTLRObjectClassResolver>)objectClassResolver + isCacheable:(BOOL*)isCacheable { + id result = nil; + BOOL canBeCached = YES; + + // TODO(TVL): use defaultClass to validate things like expectedClass is + // done in jsonFromAPIObject:expectedClass:isCacheable:? + + if ([json isKindOfClass:[NSDictionary class]]) { + // If no default, or the default was any object, then default to base + // object here (and hope there is a kind to get the right thing). + if ((defaultClass == Nil) || [defaultClass isEqual:[NSObject class]]) { + defaultClass = [GTLRObject class]; + } + result = [GTLRObject objectForJSON:json + defaultClass:defaultClass + objectClassResolver:objectClassResolver]; + } else if ([json isKindOfClass:[NSArray class]]) { + NSArray *jsonArray = json; + // make an object for each JSON dictionary in the array + NSMutableArray *resultArray = [NSMutableArray arrayWithCapacity:jsonArray.count]; + for (id jsonItem in jsonArray) { + id item = [self objectFromJSON:jsonItem + defaultClass:defaultClass + objectClassResolver:objectClassResolver + isCacheable:NULL]; + [resultArray addObject:item]; + } + result = resultArray; + } else if ([json isKindOfClass:[NSString class]]) { + // DateTimes and Durations live in JSON as strings, so convert. + if ([defaultClass isEqual:[GTLRDateTime class]]) { + result = [GTLRDateTime dateTimeWithRFC3339String:json]; + } else if ([defaultClass isEqual:[GTLRDuration class]]) { + result = [GTLRDuration durationWithJSONString:json]; + } else if ([defaultClass isEqual:[NSNumber class]]) { + result = GTLR_EnsureNSNumber(json); + canBeCached = NO; + } else { + result = json; + canBeCached = NO; + } + } else if ([json isKindOfClass:[NSNumber class]] || + [json isKindOfClass:[NSNull class]]) { + result = json; + canBeCached = NO; + } else { + GTLR_DEBUG_LOG(@"GTLRRuntimeCommon: unsupported class '%s' in objectFromJSON", + class_getName([json class])); + } + + if (isCacheable) { + *isCacheable = canBeCached; + } + return result; +} + +// Helper to generically convert an api object type to JSON. +// |expectedClass| is the type that was expected for |obj|. ++ (id)jsonFromAPIObject:(id)obj + expectedClass:(Class)expectedClass + isCacheable:(BOOL *)isCacheable { + id result = nil; + BOOL canBeCached = YES; + BOOL checkExpected = (expectedClass != Nil); + + if ([obj isKindOfClass:[NSString class]]) { + result = [obj copy]; + canBeCached = NO; + } else if ([obj isKindOfClass:[NSNumber class]] || + [obj isKindOfClass:[NSNull class]]) { + result = obj; + canBeCached = NO; + } else if ([obj isKindOfClass:[GTLRObject class]]) { + result = [(GTLRObject *)obj JSON]; + if (result == nil) { + // adding an empty object; it should have a JSON dictionary so it can + // hold future assignments + [(GTLRObject *)obj setJSON:[NSMutableDictionary dictionary]]; + result = [(GTLRObject *)obj JSON]; + } + } else if ([obj isKindOfClass:[NSArray class]]) { + checkExpected = NO; + NSArray *array = obj; + // get the JSON for each thing in the array + NSMutableArray *resultArray = [NSMutableArray arrayWithCapacity:array.count]; + for (id item in array) { + id itemJSON = [self jsonFromAPIObject:item + expectedClass:expectedClass + isCacheable:NULL]; + [resultArray addObject:itemJSON]; + } + result = resultArray; + } else if ([obj isKindOfClass:[GTLRDateTime class]]) { + // DateTimes live in JSON as strings, so convert. + GTLRDateTime *dateTime = obj; + result = dateTime.RFC3339String; + } else if ([obj isKindOfClass:[GTLRDuration class]]) { + // Durations live in JSON as strings, so convert. + GTLRDuration *duration = obj; + result = duration.jsonString; + } else { + checkExpected = NO; + if (obj) { + GTLR_DEBUG_LOG(@"GTLRRuntimeCommon: unsupported class '%s' in jsonFromAPIObject", + class_getName([obj class])); + } + } + + if (checkExpected) { + // If the default was any object, then clear it to skip validation checks. + if ([expectedClass isEqual:[NSObject class]] || + [obj isKindOfClass:[NSNull class]]) { + expectedClass = nil; + } + if (expectedClass && ![obj isKindOfClass:expectedClass]) { + GTLR_DEBUG_LOG(@"GTLRRuntimeCommon: jsonFromAPIObject expected class '%s' instead got '%s'", + class_getName(expectedClass), class_getName([obj class])); + } + } + + if (isCacheable) { + *isCacheable = canBeCached; + } + return result; +} + ++ (NSDictionary *)mergedClassDictionaryForSelector:(SEL)selector + startClass:(Class)startClass + ancestorClass:(Class)ancestorClass + cache:(NSMutableDictionary *)cache { + NSDictionary *result; + @synchronized(cache) { + result = [cache objectForKey:startClass]; + if (result == nil) { + // Collect the class's dictionary. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + NSDictionary *classDict = [startClass performSelector:selector]; +#pragma clang diagnostic pop + + // Collect the parent class's merged dictionary. + NSDictionary *parentClassMergedDict; + if ([startClass isEqual:ancestorClass]) { + parentClassMergedDict = nil; + } else { + Class parentClass = class_getSuperclass(startClass); + parentClassMergedDict = + [self mergedClassDictionaryForSelector:selector + startClass:parentClass + ancestorClass:ancestorClass + cache:cache]; + } + + // Merge this class's into the parent's so things properly override. + NSMutableDictionary *mergeDict; + if (parentClassMergedDict != nil) { + mergeDict = + [NSMutableDictionary dictionaryWithDictionary:parentClassMergedDict]; + } else { + mergeDict = [NSMutableDictionary dictionary]; + } + if (classDict != nil) { + [mergeDict addEntriesFromDictionary:classDict]; + } + + // Make an immutable version. + result = [NSDictionary dictionaryWithDictionary:mergeDict]; + + // Save it. + [cache setObject:result forKey:(id<NSCopying>)startClass]; + } + } + return result; +} + +#pragma mark Runtime lookup support + +static objc_property_t PropertyForSel(Class<GTLRRuntimeCommon> startClass, + SEL sel, BOOL isSetter, + Class<GTLRRuntimeCommon> *outFoundClass) { + const char *selName = sel_getName(sel); + const char *baseName = selName; + size_t baseNameLen = strlen(baseName); + if (isSetter) { + baseName += 3; // skip "set" + baseNameLen -= 4; // subtract "set" and the final colon + } + + // walk from this class up the hierarchy to the ancestor class + Class<GTLRRuntimeCommon> topClass = class_getSuperclass([startClass ancestorClass]); + for (Class currClass = startClass; + currClass != topClass; + currClass = class_getSuperclass(currClass)) { + // step through this class's properties + objc_property_t foundProp = NULL; + objc_property_t *properties = class_copyPropertyList(currClass, NULL); + if (properties) { + for (objc_property_t *prop = properties; *prop != NULL; ++prop) { + const char *propAttrs = property_getAttributes(*prop); + const char *dynamicMarker = strstr(propAttrs, ",D"); + if (!dynamicMarker || + (dynamicMarker[2] != 0 && dynamicMarker[2] != ',' )) { + // It isn't dynamic, skip it. + continue; + } + + if (!isSetter) { + // See if this property has an explicit getter=. (the attributes always start with a T, + // so we can check for the leading ','. + const char *getterMarker = strstr(propAttrs, ",G"); + if (getterMarker) { + const char *getterStart = getterMarker + 2; + const char *getterEnd = getterStart; + while ((*getterEnd != 0) && (*getterEnd != ',')) { + ++getterEnd; + } + size_t getterLen = (size_t)(getterEnd - getterStart); + if ((strncmp(selName, getterStart, getterLen) == 0) + && (selName[getterLen] == 0)) { + // return the actual property + foundProp = *prop; + // if requested, return the class containing the property + if (outFoundClass) *outFoundClass = currClass; + break; + } + } // if (getterMarker) + } // if (!isSetter) + + // Search for an exact-name match (a getter), but case-insensitive on the + // first character (in case baseName comes from a setter) + const char *propName = property_getName(*prop); + size_t propNameLen = strlen(propName); + if (baseNameLen == propNameLen + && strncasecmp(baseName, propName, 1) == 0 + && (baseNameLen <= 1 + || strncmp(baseName + 1, propName + 1, baseNameLen - 1) == 0)) { + // return the actual property + foundProp = *prop; + + // if requested, return the class containing the property + if (outFoundClass) *outFoundClass = currClass; + break; + } + } // for (prop in properties) + free(properties); + } + if (foundProp) return foundProp; + } + + // not found; this occasionally happens when the system looks for a method + // like "getFoo" or "descriptionWithLocale:indent:" + return NULL; +} + +typedef NS_ENUM(NSUInteger, GTLRPropertyType) { +#if !defined(__LP64__) || !__LP64__ + // These two only needed in 32bit builds since NSInteger in 64bit ends up in the LongLong paths. + GTLRPropertyTypeInt32 = 1, + GTLRPropertyTypeUInt32, +#endif + GTLRPropertyTypeLongLong = 3, + GTLRPropertyTypeULongLong, + GTLRPropertyTypeFloat, + GTLRPropertyTypeDouble, + GTLRPropertyTypeBool, + GTLRPropertyTypeNSString, + GTLRPropertyTypeNSNumber, + GTLRPropertyTypeGTLRDateTime, + GTLRPropertyTypeGTLRDuration, + GTLRPropertyTypeNSArray, + GTLRPropertyTypeNSObject, + GTLRPropertyTypeGTLRObject, +}; + +typedef struct { + const char *attributePrefix; + + GTLRPropertyType propertyType; + const char *setterEncoding; + const char *getterEncoding; + + // These are the "fixed" return classes, but some properties will require + // looking up the return class instead (because it is a subclass of + // GTLRObject). + const char *returnClassName; + Class returnClass; + BOOL extractReturnClass; + +} GTLRDynamicImpInfo; + +static const GTLRDynamicImpInfo *DynamicImpInfoForProperty(objc_property_t prop, + Class *outReturnClass) { + + if (outReturnClass) *outReturnClass = nil; + + // dynamic method resolution: + // http://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtDynamicResolution.html + // + // property runtimes: + // http://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html + + // Get and parse the property attributes, which look something like + // T@"NSString",&,D,P + // Ti,D -- NSInteger on 32bit + // Tq,D -- NSInteger on 64bit, long long on 32bit & 64bit + // TB,D -- BOOL comes as bool on 64bit iOS + // Tc,D -- BOOL comes as char otherwise + // T@"NSString",D + // T@"GTLRLink",D + // T@"NSArray",D + + + static GTLRDynamicImpInfo kImplInfo[] = { +#if !defined(__LP64__) || !__LP64__ + { // NSInteger on 32bit + "Ti", + GTLRPropertyTypeInt32, + "v@:i", + "i@:", + nil, nil, + NO + }, + { // NSUInteger on 32bit + "TI", + GTLRPropertyTypeUInt32, + "v@:I", + "I@:", + nil, nil, + NO + }, +#endif + { // NSInteger on 64bit, long long on 32bit and 64bit. + "Tq", + GTLRPropertyTypeLongLong, + "v@:q", + "q@:", + nil, nil, + NO + }, + { // NSUInteger on 64bit, long long on 32bit and 64bit. + "TQ", + GTLRPropertyTypeULongLong, + "v@:Q", + "Q@:", + nil, nil, + NO + }, + { // float + "Tf", + GTLRPropertyTypeFloat, + "v@:f", + "f@:", + nil, nil, + NO + }, + { // double + "Td", + GTLRPropertyTypeDouble, + "v@:d", + "d@:", + nil, nil, + NO + }, +#if defined(OBJC_BOOL_IS_BOOL) && OBJC_BOOL_IS_BOOL + { // BOOL as bool + "TB", + GTLRPropertyTypeBool, + "v@:B", + "B@:", + nil, nil, + NO + }, +#elif defined(OBJC_BOOL_IS_CHAR) && OBJC_BOOL_IS_CHAR + { // BOOL as char + "Tc", + GTLRPropertyTypeBool, + "v@:c", + "c@:", + nil, nil, + NO + }, +#else + #error unknown definition for ObjC BOOL type +#endif + { // NSString + "T@\"NSString\"", + GTLRPropertyTypeNSString, + "v@:@", + "@@:", + "NSString", nil, + NO + }, + { // NSNumber + "T@\"NSNumber\"", + GTLRPropertyTypeNSNumber, + "v@:@", + "@@:", + "NSNumber", nil, + NO + }, + { // GTLRDateTime + "T@\"" GTLR_CLASSNAME_CSTR(GTLRDateTime) "\"", + GTLRPropertyTypeGTLRDateTime, + "v@:@", + "@@:", + GTLR_CLASSNAME_CSTR(GTLRDateTime), nil, + NO + }, + { // GTLRDuration + "T@\"" GTLR_CLASSNAME_CSTR(GTLRDuration) "\"", + GTLRPropertyTypeGTLRDuration, + "v@:@", + "@@:", + GTLR_CLASSNAME_CSTR(GTLRDuration), nil, + NO + }, + { // NSArray with type + "T@\"NSArray\"", + GTLRPropertyTypeNSArray, + "v@:@", + "@@:", + "NSArray", nil, + NO + }, + { // id (any of the objects above) + "T@,", + GTLRPropertyTypeNSObject, + "v@:@", + "@@:", + "NSObject", nil, + NO + }, + { // GTLRObject - Last, cause it's a special case and prefix is general + "T@\"", + GTLRPropertyTypeGTLRObject, + "v@:@", + "@@:", + nil, nil, + YES + }, + }; + + static BOOL hasLookedUpClasses = NO; + if (!hasLookedUpClasses) { + // Unfortunately, you can't put [NSString class] into the static structure, + // so this lookup has to be done at runtime. + hasLookedUpClasses = YES; + for (uint32_t idx = 0; idx < sizeof(kImplInfo)/sizeof(kImplInfo[0]); ++idx) { + if (kImplInfo[idx].returnClassName) { + kImplInfo[idx].returnClass = objc_getClass(kImplInfo[idx].returnClassName); + NSCAssert1(kImplInfo[idx].returnClass != nil, + @"GTLRRuntimeCommon: class lookup failed: %s", kImplInfo[idx].returnClassName); + } + } + } + + const char *attr = property_getAttributes(prop); + + const char *dynamicMarker = strstr(attr, ",D"); + if (!dynamicMarker || + (dynamicMarker[2] != 0 && dynamicMarker[2] != ',' )) { + GTLR_DEBUG_LOG(@"GTLRRuntimeCommon: property %s isn't dynamic, attributes %s", + property_getName(prop), attr ? attr : "(nil)"); + return NULL; + } + + const GTLRDynamicImpInfo *result = NULL; + + // Cycle over the list + + for (uint32_t idx = 0; idx < sizeof(kImplInfo)/sizeof(kImplInfo[0]); ++idx) { + const char *attributePrefix = kImplInfo[idx].attributePrefix; + if (strncmp(attr, attributePrefix, strlen(attributePrefix)) == 0) { + result = &kImplInfo[idx]; + if (outReturnClass) *outReturnClass = result->returnClass; + break; + } + } + + if (result == NULL) { + GTLR_DEBUG_LOG(@"GTLRRuntimeCommon: unexpected attributes %s for property %s", + attr ? attr : "(nil)", property_getName(prop)); + return NULL; + } + + if (result->extractReturnClass && outReturnClass) { + + // add a null at the next quotation mark + char *attrCopy = strdup(attr); + char *classNameStart = attrCopy + 3; + char *classNameEnd = strstr(classNameStart, "\""); + if (classNameEnd) { + *classNameEnd = '\0'; + + // Lookup the return class + *outReturnClass = objc_getClass(classNameStart); + if (*outReturnClass == nil) { + GTLR_DEBUG_LOG(@"GTLRRuntimeCommon: did not find class with name \"%s\" " + @"for property \"%s\" with attributes \"%s\"", + classNameStart, property_getName(prop), attr); + } + } else { + GTLR_DEBUG_LOG(@"GTLRRuntimeCommon: Failed to find end of class name for " + @"property \"%s\" with attributes \"%s\"", + property_getName(prop), attr); + } + free(attrCopy); + } + + return result; +} + +// Helper to get the IMP for wiring up the getters. +// NOTE: Every argument passed in should be safe to capture in a block. Avoid +// passing something like selName instead of sel, because nothing says that +// pointer will be valid when it is finally used when the method IMP is invoked +// some time later. +static IMP GTLRRuntimeGetterIMP(SEL sel, + GTLRPropertyType propertyType, + NSString *jsonKey, + Class containedClass, + Class returnClass) { + // Only used in DEBUG logging. +#pragma unused(sel) + + IMP resultIMP; + switch (propertyType) { + +#if !defined(__LP64__) || !__LP64__ + case GTLRPropertyTypeInt32: { + resultIMP = imp_implementationWithBlock(^(id obj) { + NSNumber *num = [obj JSONValueForKey:jsonKey]; + num = GTLR_EnsureNSNumber(num); + NSInteger result = num.integerValue; + return result; + }); + break; + } + + case GTLRPropertyTypeUInt32: { + resultIMP = imp_implementationWithBlock(^(id obj) { + NSNumber *num = [obj JSONValueForKey:jsonKey]; + num = GTLR_EnsureNSNumber(num); + NSUInteger result = num.unsignedIntegerValue; + return result; + }); + break; + } +#endif // __LP64__ + + case GTLRPropertyTypeLongLong: { + resultIMP = imp_implementationWithBlock(^(id obj) { + NSNumber *num = [obj JSONValueForKey:jsonKey]; + num = GTLR_EnsureNSNumber(num); + long long result = num.longLongValue; + return result; + }); + break; + } + + case GTLRPropertyTypeULongLong: { + resultIMP = imp_implementationWithBlock(^(id obj) { + NSNumber *num = [obj JSONValueForKey:jsonKey]; + num = GTLR_EnsureNSNumber(num); + unsigned long long result = num.unsignedLongLongValue; + return result; + }); + break; + } + + case GTLRPropertyTypeFloat: { + resultIMP = imp_implementationWithBlock(^(id obj) { + NSNumber *num = [obj JSONValueForKey:jsonKey]; + num = GTLR_EnsureNSNumber(num); + float result = num.floatValue; + return result; + }); + break; + } + + case GTLRPropertyTypeDouble: { + resultIMP = imp_implementationWithBlock(^(id obj) { + NSNumber *num = [obj JSONValueForKey:jsonKey]; + num = GTLR_EnsureNSNumber(num); + double result = num.doubleValue; + return result; + }); + break; + } + + case GTLRPropertyTypeBool: { + resultIMP = imp_implementationWithBlock(^(id obj) { + NSNumber *num = [obj JSONValueForKey:jsonKey]; + BOOL flag = num.boolValue; + return flag; + }); + break; + } + + case GTLRPropertyTypeNSString: { + resultIMP = imp_implementationWithBlock(^(id obj) { + NSString *str = [obj JSONValueForKey:jsonKey]; + return str; + }); + break; + } + + case GTLRPropertyTypeGTLRDateTime: { + resultIMP = imp_implementationWithBlock(^GTLRDateTime *(GTLRObject<GTLRRuntimeCommon> *obj) { + // Return the cached object before creating on demand. + GTLRDateTime *cachedDateTime = [obj cacheChildForKey:jsonKey]; + if (cachedDateTime != nil) { + return cachedDateTime; + } + NSString *str = [obj JSONValueForKey:jsonKey]; + id cacheValue, resultValue; + if (![str isKindOfClass:[NSNull class]]) { + GTLRDateTime *dateTime = [GTLRDateTime dateTimeWithRFC3339String:str]; + + cacheValue = dateTime; + resultValue = dateTime; + } else { + cacheValue = nil; + resultValue = [NSNull null]; + } + [obj setCacheChild:cacheValue forKey:jsonKey]; + return resultValue; + }); + break; + } + + case GTLRPropertyTypeGTLRDuration: { + resultIMP = imp_implementationWithBlock(^GTLRDuration *(GTLRObject<GTLRRuntimeCommon> *obj) { + // Return the cached object before creating on demand. + GTLRDuration *cachedDuration = [obj cacheChildForKey:jsonKey]; + if (cachedDuration != nil) { + return cachedDuration; + } + NSString *str = [obj JSONValueForKey:jsonKey]; + id cacheValue, resultValue; + if (![str isKindOfClass:[NSNull class]]) { + GTLRDuration *duration = [GTLRDuration durationWithJSONString:str]; + + cacheValue = duration; + resultValue = duration; + } else { + cacheValue = nil; + resultValue = [NSNull null]; + } + [obj setCacheChild:cacheValue forKey:jsonKey]; + return resultValue; + }); + break; + } + + case GTLRPropertyTypeNSNumber: { + resultIMP = imp_implementationWithBlock(^(id obj) { + NSNumber *num = [obj JSONValueForKey:jsonKey]; + num = GTLR_EnsureNSNumber(num); + return num; + }); + break; + } + + case GTLRPropertyTypeGTLRObject: { + // Default return class to GTLRObject if it wasn't found. + if (returnClass == Nil) { + returnClass = [GTLRObject class]; + } + resultIMP = imp_implementationWithBlock(^GTLRObject *(GTLRObject<GTLRRuntimeCommon> *obj) { + // Return the cached object before creating on demand. + GTLRObject *cachedObj = [obj cacheChildForKey:jsonKey]; + if (cachedObj != nil) { + return cachedObj; + } + NSMutableDictionary *dict = [obj JSONValueForKey:jsonKey]; + if ([dict isKindOfClass:[NSMutableDictionary class]]) { + id<GTLRObjectClassResolver>objectClassResolver = [obj objectClassResolver]; + GTLRObject *subObj = [GTLRObject objectForJSON:dict + defaultClass:returnClass + objectClassResolver:objectClassResolver]; + [obj setCacheChild:subObj forKey:jsonKey]; + return subObj; + } else if ([dict isKindOfClass:[NSNull class]]) { + [obj setCacheChild:nil forKey:jsonKey]; + return (GTLRObject*)[NSNull null]; + } else if (dict != nil) { + // unexpected; probably got a string -- let the caller figure it out + GTLR_DEBUG_LOG(@"GTLRObject: unexpected JSON: %@.%@ should be a dictionary, actually is a %@:\n%@", + NSStringFromClass([obj class]), + NSStringFromSelector(sel), + NSStringFromClass([dict class]), dict); + return (GTLRObject *)dict; + } + return nil; + }); + break; + } + + case GTLRPropertyTypeNSArray: { + resultIMP = imp_implementationWithBlock(^(GTLRObject<GTLRRuntimeCommon> *obj) { + // Return the cached array before creating on demand. + NSMutableArray *cachedArray = [obj cacheChildForKey:jsonKey]; + if (cachedArray != nil) { + return cachedArray; + } + NSMutableArray *result = nil; + NSArray *array = [obj JSONValueForKey:jsonKey]; + if (array != nil) { + if ([array isKindOfClass:[NSArray class]]) { + id<GTLRObjectClassResolver>objectClassResolver = [obj objectClassResolver]; + result = [GTLRRuntimeCommon objectFromJSON:array + defaultClass:containedClass + objectClassResolver:objectClassResolver + isCacheable:NULL]; + } else { +#if DEBUG + if (![array isKindOfClass:[NSNull class]]) { + GTLR_DEBUG_LOG(@"GTLRObject: unexpected JSON: %@.%@ should be an array, actually is a %@:\n%@", + NSStringFromClass([obj class]), + NSStringFromSelector(sel), + NSStringFromClass([array class]), array); + } +#endif + result = (NSMutableArray *)array; + } + } + [obj setCacheChild:result forKey:jsonKey]; + return result; + }); + break; + } + + case GTLRPropertyTypeNSObject: { + resultIMP = imp_implementationWithBlock(^id(GTLRObject<GTLRRuntimeCommon> *obj) { + // Return the cached object before creating on demand. + id cachedObj = [obj cacheChildForKey:jsonKey]; + if (cachedObj != nil) { + return cachedObj; + } + id jsonObj = [obj JSONValueForKey:jsonKey]; + if (jsonObj != nil) { + BOOL shouldCache = NO; + id<GTLRObjectClassResolver>objectClassResolver = [obj objectClassResolver]; + id result = [GTLRRuntimeCommon objectFromJSON:jsonObj + defaultClass:nil + objectClassResolver:objectClassResolver + isCacheable:&shouldCache]; + + [obj setCacheChild:(shouldCache ? result : nil) + forKey:jsonKey]; + return result; + } + return nil; + }); + break; + } + } // switch(propertyType) + + return resultIMP; +} + +// Helper to get the IMP for wiring up the setters. +// NOTE: Every argument passed in should be safe to capture in a block. Avoid +// passing something like selName instead of sel, because nothing says that +// pointer will be valid when it is finally used when the method IMP is invoked +// some time later. +static IMP GTLRRuntimeSetterIMP(SEL sel, + GTLRPropertyType propertyType, + NSString *jsonKey, + Class containedClass, + Class returnClass) { +#pragma unused(sel, returnClass) + IMP resultIMP; + switch (propertyType) { + +#if !defined(__LP64__) || !__LP64__ + case GTLRPropertyTypeInt32: { + resultIMP = imp_implementationWithBlock(^(id obj, NSInteger val) { + [obj setJSONValue:@(val) forKey:jsonKey]; + }); + break; + } + + case GTLRPropertyTypeUInt32: { + resultIMP = imp_implementationWithBlock(^(id obj, NSUInteger val) { + [obj setJSONValue:@(val) forKey:jsonKey]; + }); + break; + } +#endif // __LP64__ + + case GTLRPropertyTypeLongLong: { + resultIMP = imp_implementationWithBlock(^(id obj, long long val) { + [obj setJSONValue:@(val) forKey:jsonKey]; + }); + break; + } + + case GTLRPropertyTypeULongLong: { + resultIMP = imp_implementationWithBlock(^(id obj, + unsigned long long val) { + [obj setJSONValue:@(val) forKey:jsonKey]; + }); + break; + } + + case GTLRPropertyTypeFloat: { + resultIMP = imp_implementationWithBlock(^(id obj, float val) { + [obj setJSONValue:@(val) forKey:jsonKey]; + }); + break; + } + + case GTLRPropertyTypeDouble: { + resultIMP = imp_implementationWithBlock(^(id obj, double val) { + [obj setJSONValue:@(val) forKey:jsonKey]; + }); + break; + } + + case GTLRPropertyTypeBool: { + resultIMP = imp_implementationWithBlock(^(id obj, BOOL val) { + NSNumber *numValue = (NSNumber *)(val ? kCFBooleanTrue : kCFBooleanFalse); + [obj setJSONValue:numValue forKey:jsonKey]; + }); + break; + } + + case GTLRPropertyTypeNSString: { + resultIMP = imp_implementationWithBlock(^(id obj, NSString *val) { + NSString *copiedStr = [val copy]; + [obj setJSONValue:copiedStr forKey:jsonKey]; + }); + break; + } + + case GTLRPropertyTypeGTLRDateTime: { + resultIMP = imp_implementationWithBlock(^(GTLRObject<GTLRRuntimeCommon> *obj, + GTLRDateTime *val) { + id cacheValue, jsonValue; + if (![val isKindOfClass:[NSNull class]]) { + jsonValue = val.RFC3339String; + cacheValue = val; + } else { + jsonValue = [NSNull null]; + cacheValue = nil; + } + + [obj setJSONValue:jsonValue forKey:jsonKey]; + [obj setCacheChild:cacheValue forKey:jsonKey]; + }); + break; + } + + case GTLRPropertyTypeGTLRDuration: { + resultIMP = imp_implementationWithBlock(^(GTLRObject<GTLRRuntimeCommon> *obj, + GTLRDuration *val) { + id cacheValue, jsonValue; + if (![val isKindOfClass:[NSNull class]]) { + jsonValue = val.jsonString; + cacheValue = val; + } else { + jsonValue = [NSNull null]; + cacheValue = nil; + } + + [obj setJSONValue:jsonValue forKey:jsonKey]; + [obj setCacheChild:cacheValue forKey:jsonKey]; + }); + break; + } + + case GTLRPropertyTypeNSNumber: { + resultIMP = imp_implementationWithBlock(^(id obj, NSNumber *val) { + [obj setJSONValue:val forKey:jsonKey]; + }); + break; + } + + case GTLRPropertyTypeGTLRObject: { + resultIMP = imp_implementationWithBlock(^(GTLRObject<GTLRRuntimeCommon> *obj, + GTLRObject *val) { + id cacheValue, jsonValue; + if (![val isKindOfClass:[NSNull class]]) { + NSMutableDictionary *dict = [val JSON]; + if (dict == nil && val != nil) { + // adding an empty object; it should have a JSON dictionary so it + // can hold future assignments + val.JSON = [NSMutableDictionary dictionary]; + jsonValue = val.JSON; + } else { + jsonValue = dict; + } + cacheValue = val; + } else { + jsonValue = [NSNull null]; + cacheValue = nil; + } + [obj setJSONValue:jsonValue forKey:jsonKey]; + [obj setCacheChild:cacheValue forKey:jsonKey]; + }); + break; + } + + case GTLRPropertyTypeNSArray: { + resultIMP = imp_implementationWithBlock(^(GTLRObject<GTLRRuntimeCommon> *obj, + NSMutableArray *val) { + id json = [GTLRRuntimeCommon jsonFromAPIObject:val + expectedClass:containedClass + isCacheable:NULL]; + [obj setJSONValue:json forKey:jsonKey]; + [obj setCacheChild:val forKey:jsonKey]; + }); + break; + } + + case GTLRPropertyTypeNSObject: { + resultIMP = imp_implementationWithBlock(^(GTLRObject<GTLRRuntimeCommon> *obj, + id val) { + BOOL shouldCache = NO; + id json = [GTLRRuntimeCommon jsonFromAPIObject:val + expectedClass:Nil + isCacheable:&shouldCache]; + [obj setJSONValue:json forKey:jsonKey]; + [obj setCacheChild:(shouldCache ? val : nil) + forKey:jsonKey]; + }); + break; + } + } // switch(propertyType) + + return resultIMP; +} + +#pragma mark Runtime - wiring point + ++ (BOOL)resolveInstanceMethod:(SEL)sel onClass:(Class<GTLRRuntimeCommon>)onClass { + // dynamic method resolution: + // http://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtDynamicResolution.html + // + // property runtimes: + // http://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html + + const char *selName = sel_getName(sel); + size_t selNameLen = strlen(selName); + char lastChar = selName[selNameLen - 1]; + BOOL isSetter = (lastChar == ':'); + + // look for a declared property matching this selector name exactly + Class<GTLRRuntimeCommon> foundClass = nil; + + objc_property_t prop = PropertyForSel(onClass, sel, isSetter, &foundClass); + if (prop == NULL || foundClass == nil) { + return NO; // No luck, out of here. + } + + Class returnClass = nil; + const GTLRDynamicImpInfo *implInfo = DynamicImpInfoForProperty(prop, + &returnClass); + if (implInfo == NULL) { + GTLR_DEBUG_LOG(@"GTLRRuntimeCommon: unexpected return type class %s for " + @"property \"%s\" of class \"%s\"", + returnClass ? class_getName(returnClass) : "<nil>", + property_getName(prop), + class_getName(onClass)); + return NO; // Failed to find our impl info, out of here. + } + + const char *propName = property_getName(prop); + NSString *propStr = @(propName); + + // replace the property name with the proper JSON key if it's + // special-cased with a map in the found class; otherwise, the property + // name is the JSON key + // NOTE: These caches that are built up could likely be dropped and do this + // lookup on demand from the class tree. Most are checked once when a method + // is first resolved, so eventually become wasted memory. + NSDictionary *keyMap = + [[foundClass ancestorClass] propertyToJSONKeyMapForClass:foundClass]; + NSString *jsonKey = [keyMap objectForKey:propStr]; + if (jsonKey == nil) { + jsonKey = propStr; + } + + // For arrays we need to look up what the contained class is. + Class containedClass = nil; + if (implInfo->propertyType == GTLRPropertyTypeNSArray) { + NSDictionary *classMap = + [[foundClass ancestorClass] arrayPropertyToClassMapForClass:foundClass]; + containedClass = [classMap objectForKey:jsonKey]; + if (containedClass == Nil) { + GTLR_DEBUG_LOG(@"GTLRRuntimeCommon: expected array item class for " + @"property \"%s\" of class \"%s\"", + property_getName(prop), class_getName(foundClass)); + } + } + + // Wire in the method. + IMP imp; + const char *encoding; + if (isSetter) { + imp = GTLRRuntimeSetterIMP(sel, implInfo->propertyType, + jsonKey, containedClass, returnClass); + encoding = implInfo->setterEncoding; + } else { + imp = GTLRRuntimeGetterIMP(sel, implInfo->propertyType, + jsonKey, containedClass, returnClass); + encoding = implInfo->getterEncoding; + } + if (class_addMethod(foundClass, sel, imp, encoding)) { + return YES; + } + // Not much we can do if this fails, but leave a breadcumb in the log. + GTLR_DEBUG_LOG(@"GTLRRuntimeCommon: Failed to wire %@ on %@ (encoding: %s).", + NSStringFromSelector(sel), + NSStringFromClass(foundClass), + encoding); + return NO; +} + +@end diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRService.h b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRService.h @@ -0,0 +1,879 @@ +/* Copyright (c) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Service object documentation: +// https://github.com/google/google-api-objectivec-client-for-rest/wiki#services-and-tickets + +#import <Foundation/Foundation.h> + +#import "GTLRDefines.h" +#import "GTLRBatchQuery.h" +#import "GTLRBatchResult.h" +#import "GTLRDateTime.h" +#import "GTLRDuration.h" +#import "GTLRErrorObject.h" +#import "GTLRObject.h" +#import "GTLRQuery.h" + +@class GTMSessionFetcher; +@class GTMSessionFetcherService; +@protocol GTMFetcherAuthorizationProtocol; + +NS_ASSUME_NONNULL_BEGIN + +/** + * The domain used used for NSErrors created by GTLRService query execution. + */ +extern NSString *const kGTLRServiceErrorDomain; + +typedef NS_ENUM(NSInteger, GTLRServiceError) { + GTLRServiceErrorQueryResultMissing = -3000, + GTLRServiceErrorBatchResponseUnexpected = -3001, + GTLRServiceErrorBatchResponseStatusCode = -3002 +}; + +/** + * The kGTLRServiceErrorDomain userInfo key for the server response body. + */ +extern NSString *const kGTLRServiceErrorBodyDataKey; + +/** + * The kGTLRServiceErrorDomain userInfo key for the response content ID, if appropriate. + */ +extern NSString *const kGTLRServiceErrorContentIDKey; + +/** + * The domain used for foundation errors created from GTLRErrorObjects that + * were not originally foundation errors. + */ +extern NSString *const kGTLRErrorObjectDomain; + +/** + * The userInfo key for a GTLRErrorObject for errors with domain kGTLRErrorObjectDomain + * when the error was created from a structured JSON error response body. + */ +extern NSString *const kGTLRStructuredErrorKey; + +/** + * A constant ETag for when updating or deleting a single entry, telling + * the server to replace the current value unconditionally. + * + * Do not use this in entries in a batch feed. + */ +extern NSString *const kGTLRETagWildcard; + +/** + * Notification of a ticket starting. The notification object is the ticket. + * This is posted on the main thread. + * + * Use the stopped notification to log all requests made by the library. + */ +extern NSString *const kGTLRServiceTicketStartedNotification; + +/** + * Notification of a ticket stopping. The notification object is the ticket. + * This is posted on the main thread. + */ +extern NSString *const kGTLRServiceTicketStoppedNotification; + +/** + * Notifications when parsing of a server response or entry begins. + * This is posted on the main thread. + */ +extern NSString *const kGTLRServiceTicketParsingStartedNotification; + +/** + * Notifications when parsing of a server response or entry ends. + * This is posted on the main thread. + */ +extern NSString *const kGTLRServiceTicketParsingStoppedNotification; + +/** + * The header name used to send an Application's Bundle Identifier. + * For more information on adding API restrictions see the docs: + * https://cloud.google.com/docs/authentication/api-keys#api_key_restrictions + */ +extern NSString *const kXIosBundleIdHeader; + +@class GTLRServiceTicket; + +/** + * Callback block for query execution. + * + * @param callbackTicket The ticket that tracked query execution. + * @param object The result of query execution. This will be derived from + * GTLRObject. The object may be nil for operations such as DELETE which + * do not return an object. The object will be a GTLRBatchResult for + * batch operations, and GTLRDataObject for media downloads. + * @param callbackError If non-nil, the query execution failed. For batch requests, + * this may be nil even if individual queries in the batch have failed. + */ +typedef void (^GTLRServiceCompletionHandler)(GTLRServiceTicket *callbackTicket, + id _Nullable object, + NSError * _Nullable callbackError); + +/** + * Callback block for upload of query data. + * + * @param progressTicket The ticket that tracks query execution. + * @param totalBytesUploaded Number of bytes uploaded so far. + * @param totalBytesExpectedToUpload Number of bytes expected to be uploaded. + */ +typedef void (^GTLRServiceUploadProgressBlock)(GTLRServiceTicket *progressTicket, + unsigned long long totalBytesUploaded, + unsigned long long totalBytesExpectedToUpload); + +/** + * Callback block invoked when an eror occurs during query execution. + * + * @param retryTicket The ticket that tracks query execution. + * @param suggestedWillRetry Flag indicating if the library would retry this without a retry block. + * @param fetchError The error that occurred. If the domain is + * kGTMSessionFetcherStatusDomain then the error's code is the server + * response status. Details on the error from the server are available + * in the userInfo via the keys kGTLRStructuredErrorKey and + * NSLocalizedDescriptionKey. + * + * @return YES if the request should be retried. + */ +typedef BOOL (^GTLRServiceRetryBlock)(GTLRServiceTicket *retryTicket, + BOOL suggestedWillRetry, + NSError * _Nullable fetchError); + +/** + * Block to be invoked by a test block. + * + * @param object The faked object, if any, to be passed to the test code's completion handler. + * @param error The faked error if any, to be passed to the test code's completion handler. + */ +typedef void (^GTLRServiceTestResponse)(id _Nullable object, NSError *_Nullable error); + +/** + * A test block enables testing of query execution without any network activity. + * + * The test block must finish by calling the response block, passing either an object + * (GTLRObject or GTLRBatchResult) or an NSError. + * + * The query is available to the test block code as testTicket.originalQuery. + * + * Because query execution is asynchronous, the test code must wait for a callback, + * either with GTLRService's waitForTicket:timeout:fetchedObject:error: or with + * XCTestCase's waitForExpectationsWithTimeout: + * + * Example usage is available in GTLRServiceTest. + * + * @param testTicket The ticket that tracks query execution. + * @param testResponse A block that must be invoked by the test block. This may be invoked + * synchronously or asynchornously. + */ +typedef void (^GTLRServiceTestBlock)(GTLRServiceTicket *testTicket, + GTLRServiceTestResponse testResponse); + +#pragma mark - + +/** + * Base class for the service that executes queries and manages tickets. + * + * Client apps will typically use a generated subclass of GTLRService. + */ +@interface GTLRService : NSObject + +#pragma mark Query Execution + +/** + * Executes the supplied query + * + * Success is indicated in the completion handler by a nil error parameter, not by a non-nil + * object parameter. + * + * The callback block is invoked exactly once unless the ticket is cancelled. + * The callback will be called on the service's callback queue. + * + * Various execution parameters will be taken from the service's properties, unless overridden + * in the query's @c executionParameters property. + * + * A query may only be executed a single time. To reuse a query, make a copy before executing + * it. + * + * To get a NSURLRequest that represents the query, use @c -[GTLRService requestForQuery:] + * + * @param query The API query, either a subclass of GTLRQuery, or a GTLRBatchQuery. + * @param handler The execution callback block. + * + * @return A ticket for tracking or canceling query execution. + */ +- (GTLRServiceTicket *)executeQuery:(id<GTLRQueryProtocol>)query + completionHandler:(nullable GTLRServiceCompletionHandler)handler; + +/** + * Executes the supplied query + * + * The callback is invoked exactly once unless the ticket is cancelled. + * The callback will be called on the service's callbackQueue. + * Various execution parameters will be taken from the service's properties, unless overridden + * in the query's @c executionParameters property. + * + * The selector should have a signature matching: + * @code + * - (void)serviceTicket:(GTLRServiceTicket *)callbackTicket + * finishedWithObject:(GTLRObject *)object + * error:(NSError *)callbackError + * @endcode + * + * @param query The API query, either a subclass of GTLRQuery, or a GTLRBatchQuery. + * @param delegate The object to be with the selector to be invoked upon completion. + * @param finishedSelector The selector to be invoked upon completion. + * + * @return A ticket for tracking or canceling query execution. + */ +- (GTLRServiceTicket *)executeQuery:(id<GTLRQueryProtocol>)query + delegate:(nullable id)delegate + didFinishSelector:(nullable SEL)finishedSelector; + + +/** + * Enable automatic pagination. + * + * A ticket can optionally do a sequence of fetches for queries where repeated requests + * with a @c nextPageToken query parameter is required to retrieve all pages of + * the response collection. The client's callback is invoked only when all items have + * been retrieved, or an error has occurred. + * + * The final object may be a combination of multiple page responses + * so it may not be the same as if all results had been returned in a single + * page. Some fields of the response may reflect only the final page's values. + * + * Automatic page fetches will return an error if more than 25 page fetches are + * required. For debug builds, this will log a warning to the console when more + * than 2 page fetches occur, as a reminder that the query's @c maxResults parameter + * should probably be increased to specify more items returned per page. + * + * Automatic page accumulation is available for query result objects that are derived + * from GTLRCollectionObject. + * + * This may also be specified for a single query in the query's @c executionParameters property. + * + * Default value is NO. + */ +@property(nonatomic, assign) BOOL shouldFetchNextPages; + +/** + * Some services require a developer key for quotas and limits. + * + * If you have enabled the iOS API Key Restriction, you will want + * to manually set the @c APIKeyRestrictionBundleID property, or + * use -setMainBundleIDRestrictionWithAPIKey: to set your API key + * and set the restriction to the main bundle's bundle id. + */ +@property(nonatomic, copy, nullable) NSString *APIKey; + +/** + * The Bundle Identifier to use for the API key restriction. This will be + * sent in an X-Ios-Bundle-Identifier header; for more information see + * the API key documentation + * https://cloud.google.com/docs/authentication/api-keys#api_key_restrictions + */ +@property(nonatomic, copy, nullable) NSString *APIKeyRestrictionBundleID; + +/** + * Helper method to set the @c APIKey to the given value and set the + * @c APIKeyRestrictionBundleID to the main bundle's bundle identifier. + */ +- (void)setMainBundleIDRestrictionWithAPIKey:(NSString *)apiKey; + +/** + * An authorizer adds user authentication headers to the request as needed. + * + * This may be overridden on individual queries with the @c shouldSkipAuthorization property. + */ +@property(nonatomic, retain, nullable) id <GTMFetcherAuthorizationProtocol> authorizer; + +/** + * Enable fetcher retry support. See the explanation of retry support in @c GTMSessionFetcher.h + * + * Default value is NO, but retry is also enabled if the retryBlock is not nil. + * + * This may also be specified for a single query in the query's @c executionParameters property. + */ +@property(nonatomic, assign, getter=isRetryEnabled) BOOL retryEnabled; + +/** + * A retry block may be provided to inspect and change retry criteria. + * + * This may also be specified for a single query in the query's @c executionParameters property. + */ +@property(atomic, copy, nullable) GTLRServiceRetryBlock retryBlock; + +/** + * The maximum retry interval. Retries occur at increasing intervals, up to the specified maximum. + * + * This may also be specified for a single query in the query's @c executionParameters property. + */ +@property(nonatomic, assign) NSTimeInterval maxRetryInterval; + +#pragma mark Fetch Object by Resource URL + +/** + * Fetch an object given the resource URL. This is appropriate when the object's + * full link is known, such as from a selfLink response property. + * + * @param resourceURL The URL of the object to be fetched. + * @param objectClass The GTLRObject subclass to be instantiated. If nil, the library + * will try to infer the class from the object's "kind" string property. + * @param executionParameters Values to override the service's properties when executing the + * ticket. + * @param handler The execution callback block. + * + * @return A ticket for tracking or canceling query execution. + */ +- (GTLRServiceTicket *)fetchObjectWithURL:(NSURL *)resourceURL + objectClass:(nullable Class)objectClass + executionParameters:(nullable GTLRServiceExecutionParameters *)executionParameters + completionHandler:(nullable GTLRServiceCompletionHandler)handler; + +#pragma mark Support for Client Tests + +/** + * A test block can be provided to test service calls without any network activity. + * + * See the description of @c GTLRServiceTestBlock for additional details. + * + * This may also be specified for a single query in the query's @c executionParameters property. + * + * A service instance for testing can also be created with @c +mockServiceWithFakedObject + */ +@property(nonatomic, copy, nullable) GTLRServiceTestBlock testBlock; + +#pragma mark Converting a Query to an NSURLRequest + +/** + * Creates a NSURLRequest from the query object and from properties on this service + * (additionalHTTPHeaders, additionalURLQueryParameters, APIKey) without executing + * it. This can be useful for using @c GTMSessionFetcher or @c NSURLSession to + * perform the fetch. + * + * For requests to non-public resources, the request will not yet be authorized; + * that can be done using the GTLR service's authorizer. Creating a @c GTMSessionFetcher + * from the GTLRService's @c fetcherService will take care of authorization as well. + * + * This works only for GET queries, and only for an individual query, not a batch query. + * + * @note @c Unlike executeQuery:, requestForQuery: does not release the query's callback blocks. + * + * @param query The query used to create the request. + * + * @return A request suitable for use with @c GTMSessionFetcher or @c NSURLSession + */ +- (NSMutableURLRequest *)requestForQuery:(GTLRQuery *)query; + +#pragma mark User Properties + +/** + * The service properties dictionary is copied to become the initial property dictionary + * for each ticket, augmented by a query's execution parameter's properties. + */ +@property(nonatomic, copy, nullable) NSDictionary<NSString *, id> *serviceProperties; + +#pragma mark JSON to GTLRObject Mapping + +/** + * Specifies subclasses to be created instead of standard library objects, allowing + * an app to add properties and methods to GTLR objects. + * + * This is just a helper method that sets the service's objectClassResolver:. + * + * Example: + * @code + * NSDictionary *surrogates = @{ + * [MyDriveFile class] : [GTLRDrive_File_Surrogate class], + * [MyDriveFileList class] : [GTLRDrive_FileList_Surrogate class] + * }; + * [service setSurrogates:surrogates]; + * @endcode + */ +- (void)setSurrogates:(NSDictionary <Class, Class>*)surrogates; + +/** + * Used to decide what GTLRObject subclass to make from the received JSON. + * + * This defaults to a resolver that will use any kindStringToClassMap the service + * provides. + * + * To use a standard resolver with a surrogates dictionary, invoke setSurrogates: instead + * of setting this property. + */ +@property(nonatomic, strong) id<GTLRObjectClassResolver> objectClassResolver; + +/** + * A dictionary mapping "kind" strings to the GTLObject subclasses that should + * be created for JSON with the given kind. + */ ++ (NSDictionary<NSString *, Class> *)kindStringToClassMap; + +#pragma mark Request Settings + +/** + * The queue used to invoked callbacks. By default, the main queue is used for callbacks. + */ +@property(nonatomic, retain) dispatch_queue_t callbackQueue; + +/** + * Allows the application to make non-SSL and localhost requests for testing. + * + * Default value is NO. + */ +@property(nonatomic, assign) BOOL allowInsecureQueries; + +/** + * The fetcher service creates the fetcher instances for this API service. + * + * Applications may set this to an authorized fetcher service created elsewhere + * in the app, or may take the fetcher service created by this GTLRService and use it + * to create fetchers independent of this service. + */ +@property(nonatomic, retain) GTMSessionFetcherService *fetcherService; + +#pragma mark Custom User Agents + +/** + * Applications needing an additional identifier in the server logs may specify one + * through this property and it will be added to the existing UserAgent. It should + * already be a valid identifier as no cleaning/validation is done. + */ +@property(nonatomic, copy, nullable) NSString *userAgentAddition; + +/** + * A base user-agent based on the application signature in the Info.plist settings. + * + * Most applications should not explicitly set this property. Any string provided will + * be cleaned of inappropriate characters. + */ +@property(nonatomic, copy, nullable) NSString *userAgent; + +/** + * The request user agent includes the library and OS version appended to the + * base userAgent, along with the optional addition string. + */ +@property(nonatomic, readonly, nullable) NSString *requestUserAgent; + +/** + * A precise base userAgent string identifying the application. No cleaning of characters + * is done. Library-specific details will be appended. + * + * @param userAgent A wire-ready user agent string. + */ +- (void)setExactUserAgent:(nullable NSString *)userAgent; + +/** + * A precise userAgent string to send on requests; no cleaning is done. When + * set, requestUserAgent will be exactly this, no library or system information + * will be auto added. + * + * @param requestUserAgent A wire-ready user agent string. + */ +- (void)overrideRequestUserAgent:(nullable NSString *)requestUserAgent; + +/** + * Any additional URL query parameters for the queries executed by this service. + * + * Individual queries may have additionalURLQueryParameters specified as well. + */ +@property(atomic, copy, nullable) NSDictionary<NSString *, NSString *> *additionalURLQueryParameters; + +/** + * Any additional HTTP headers for this queries executed by this service. + * + * Individual queries may have additionalHTTPHeaders specified as well. + */ +@property(atomic, copy, nullable) NSDictionary<NSString *, NSString *> *additionalHTTPHeaders; + +#pragma mark Request URL Construction + +/* + * The URL for where to send a Query is built out of these parts + * ( https://developers.google.com/discovery/v1/using#build-compose ) : + * + * service.rootURLString + service.servicePath + query.pathURITemplate + * + * Note: odds are these both should end in a '/', so make sure any value you + * provide will combine correctly with the above rules. + */ + +/** + * The scheme and host for the API server. This may be modified to point at a test server. + */ +@property(nonatomic, copy) NSString *rootURLString; + +/** + * The path for the specific API service instance, relative to the rootURLString. + */ +@property(nonatomic, copy) NSString *servicePath; + +/** + * A path fragment added in to URLs before "servicePath" to build + * the full URL used for resumable media uploads. + */ +@property(nonatomic, copy) NSString *resumableUploadPath; + +/** + * A path fragment added in to URLs before "servicePath" to build + * the full URL used for simple and multipart media uploads. + */ +@property(nonatomic, copy) NSString *simpleUploadPath; + +/** + * A path fragment added in to URLs before "servicePath" to build + * the full URL used for batch requests. + */ +@property(nonatomic, copy) NSString *batchPath; + +#pragma mark Resumable Uploads + +/** + * A block called to track upload progress. + * + * A query's service execution parameters may be used to override this. + */ +@property(nonatomic, copy, nullable) GTLRServiceUploadProgressBlock uploadProgressBlock; + +/** + * The default chunk size for resumable uploads. This defaults to kGTLRStandardUploadChunkSize + * for service subclasses that support chunked uploads. + */ +@property(nonatomic, assign) NSUInteger serviceUploadChunkSize; + +/** + * Service subclasses may override this to specify their own default chunk size for + * resumable uploads. + */ ++ (NSUInteger)defaultServiceUploadChunkSize; + +#pragma mark Internal +///////////////////////////////////////////////////////////////////////////////////////////// +// +// Properties below are used by the library and should not typically be set by client apps. +// +///////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The queue used for parsing JSON responses. + * + * Applications should typically not change this. + */ +@property(nonatomic, retain) dispatch_queue_t parseQueue; + +/** + * If this service supports pretty printing the JSON on the wire, these are + * the names of the query params that enable it. If there are any values, + * the library disables pretty printing to save on bandwidth. + * + * Applications should typically not need change this; the ServiceGenerator + * will set this up when generating the custom subclass. + */ +@property(nonatomic, strong, nullable) NSArray<NSString *> *prettyPrintQueryParameterNames; + +/** + * This indicates if the API requires a "data" JSON element to wrap the payload + * on requests and responses. + * + * Applications should typically not change this. + */ +@property(nonatomic, assign, getter=isDataWrapperRequired) BOOL dataWrapperRequired; + +@end + +@interface GTLRService (TestingSupport) + +/** + * Convenience method to create a mock GTLR service just for testing. + * + * Queries executed by this mock service will not perform any network operation, + * but will invoke callbacks and provide the supplied object or error to the + * completion handler. + * + * You can make more customized mocks by setting the test block property of a service + * or a query's execution parameters. The test block can inspect the query as ticket.originalQuery + * to customize test behavior. + * + * See the description of @c GTLRServiceTestBlock for more details on customized testing. + * + * Example usage is in the unit test method @c testService_MockService_Succeeding + * + * @param object An object derived from GTLRObject to be passed to query completion handlers. + * @param error An error to be passed to query completion handlers. + * + * @return A mock instance of the service, suitable for unit testing. + */ ++ (instancetype)mockServiceWithFakedObject:(nullable id)object + fakedError:(nullable NSError *)error; + +/** + * Wait synchronously for fetch to complete (strongly discouraged) + * + * This method is intended for use only in unit tests and command-line tools. + * Unit tests may also use XCTest's waitForExpectationsWithTimeout: instead of + * or after this method. + * + * This method just runs the current event loop until the fetch completes + * or the timout limit is reached. This may discard unexpected events + * that occur while spinning, so it's really not appropriate for use + * in serious applications. + * + * Returns YES if an object was successfully fetched. If the wait + * timed out, returns NO and the returned error is nil. + * + * @param ticket The ticket being executed. + * @param timeoutInSeconds Maximum duration to wait. + * + * @return YES if the ticket completed or was cancelled; NO if the wait timed out. + */ +- (BOOL)waitForTicket:(GTLRServiceTicket *)ticket + timeout:(NSTimeInterval)timeoutInSeconds; + +@end + +#pragma mark - + +/** + * Service execution parameters may be set on an individual query + * to alter the service's settings. + */ +@interface GTLRServiceExecutionParameters : NSObject<NSCopying> + +/** + * Override the service's property @c shouldFetchNextPages for automatic pagination. + * + * A BOOL value should be specified. + */ +@property(atomic, strong, nullable) NSNumber *shouldFetchNextPages; + +/** + * Override the service's property @c shouldFetchNextPages for enabling automatic retries. + * + * A BOOL value should be specified. + * + * Retry is also enabled if the retryBlock is not nil + */ +@property(atomic, strong, nullable, getter=isRetryEnabled) NSNumber *retryEnabled; + +/** + * Override the service's property @c retryBlock for customizing automatic retries. + */ +@property(atomic, copy, nullable) GTLRServiceRetryBlock retryBlock; + +/** + * Override the service's property @c maxRetryInterval for customizing automatic retries. + * + * A NSTimeInterval (double) value should be specified. + */ +@property(atomic, strong, nullable) NSNumber *maxRetryInterval; + +/** + * Override the service's property @c uploadProgressBlock for monitoring upload progress. + */ +@property(atomic, copy, nullable) GTLRServiceUploadProgressBlock uploadProgressBlock; + +/** + * Override the service's property @c callbackQueue for invoking callbacks. + */ +@property(atomic, retain, nullable) dispatch_queue_t callbackQueue; + +/** + * Override the service's property @c testBlock for simulating query execution. + * + * See the description of @c GTLRServiceTestBlock for additional details. + */ +@property(atomic, copy, nullable) GTLRServiceTestBlock testBlock; + +/** + * Override the service's property @c objectClassResolver for controlling object class selection. + */ +@property(atomic, strong, nullable) id<GTLRObjectClassResolver> objectClassResolver; + +/** + * The ticket's properties are the service properties, with the execution parameter's + * ticketProperties added (replacing any keys already present from the service.) + */ +@property(atomic, copy, nullable) NSDictionary<NSString *, id> *ticketProperties; + +/** + * Indicates if any of the execution parameters properties are set. + */ +@property(nonatomic, readonly) BOOL hasParameters; + +@end + +/** + * A ticket tracks the progress of a query being executed. + */ +@interface GTLRServiceTicket : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +/** + * The service that issued this ticket. + * + * This method may be invoked from any thread. + */ +@property(atomic, readonly) GTLRService *service; + +#pragma mark Execution Control + +/** + * Invoking cancelTicket stops the fetch if it is in progress. The query callbacks + * will not be invoked. + * + * This method may be invoked from any thread. + */ +- (void)cancelTicket; + +/** + * The time the ticket was created. + */ +@property(atomic, readonly) NSDate *creationDate; + +/** + * Pause the ticket execution. This is valid only for chunked, resumable upload queries. + */ +- (void)pauseUpload; + +/** + * Resume the ticket execution. This is valid only for chunked, resumable upload queries. + */ +- (void)resumeUpload; + +/** + * Checks if the ticket execution is paused. + */ +@property(nonatomic, readonly, getter=isUploadPaused) BOOL uploadPaused; + +/** + * The request being fetched for the query. + */ +@property(nonatomic, readonly, nullable) NSURLRequest *fetchRequest; + +/** + * The fetcher being used for the query request. + */ +@property(atomic, readonly, nullable) GTMSessionFetcher *objectFetcher; + +/** + * The queue used for query callbacks. + */ +@property(atomic, readonly) dispatch_queue_t callbackQueue; + +/** + * The API key used for the query requeat. + */ +@property(atomic, readonly, nullable) NSString *APIKey; + +/** + * The Bundle Identifier to use for the API key restriciton. + */ +@property(atomic, readonly, nullable) NSString *APIKeyRestrictionBundleID; + +#pragma mark Status + +/** + * The server's response status for the query's fetch, if available. + */ +@property(nonatomic, readonly) NSInteger statusCode; + +/** + * The error resulting from the query's fetch, if available. + */ +@property(nonatomic, readonly, nullable) NSError *fetchError; + +/** + * A flag indicating if the query's callbacks have been invoked. + */ +@property(nonatomic, readonly) BOOL hasCalledCallback; + +/** + * A flag indicating if the query execution was cancelled by the client app. + */ +@property(atomic, readonly, getter=isCancelled) BOOL cancelled; + +#pragma mark Pagination + +/** + * A flag indicating if automatic pagination is enabled for the query. + */ +@property(nonatomic, readonly) BOOL shouldFetchNextPages; + +/** + * The number of pages fetched, if automatic pagination is enabled for the query and multiple + * pages have been fetched. + */ +@property(nonatomic, readonly) NSUInteger pagesFetchedCounter; + +#pragma mark User Properties + +/** + * Ticket properties a way to pass values via the ticket for the convenience of the client app. + * + * Ticket properties are initialized from serviceProperties and augmented by the ticketProperties + * of the query's execution parameters. + */ +@property(nonatomic, readonly, nullable) NSDictionary<NSString *, id> *ticketProperties; + +#pragma mark Payload + +/** + * The object being uploaded via POST, PUT, or PATCH. + */ +@property(nonatomic, readonly, nullable) GTLRObject *postedObject; + +/** + * The object downloaded for the query, after parsing. + */ +@property(nonatomic, readonly, nullable) GTLRObject *fetchedObject; + +/** + * The query currently being fetched by this ticket. This may not be the original query when + * fetching a second or later pages. + */ +@property(atomic, readonly, nullable) id<GTLRQueryProtocol> executingQuery; + +/** + * The query used to create this ticket + */ +@property(atomic, readonly, nullable) id<GTLRQueryProtocol> originalQuery; + +/** + * The @c GTLRObjectClassResolver for controlling object class selection. + */ +@property(atomic, readonly, strong) id<GTLRObjectClassResolver> objectClassResolver; + +/** + * The query from within the ticket's batch request with the given ID. + * + * @param requestID The desired ticket's request ID. + * + * @return The query with the specified ID, if found. + */ +- (nullable GTLRQuery *)queryForRequestID:(NSString *)requestID; + +@end + +/** + * The library doesn't use GTLRObjectCollectionImpl, but it provides a concrete implementation + * so the methods do not cause private method errors in Xcode/AppStore review. + */ +@interface GTLRObjectCollectionImpl : GTLRObject +@property(nonatomic, copy) NSString *nextPageToken; +@end + +NS_ASSUME_NONNULL_END diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRService.m b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRService.m @@ -0,0 +1,2883 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !__has_feature(objc_arc) +#error "This file needs to be compiled with ARC enabled." +#endif + +#import <TargetConditionals.h> + +#if TARGET_OS_IPHONE +#import <UIKit/UIKit.h> +#endif + +#if !defined(GTLR_USE_FRAMEWORK_IMPORTS) + #if defined(COCOAPODS) && COCOAPODS + #define GTLR_USE_FRAMEWORK_IMPORTS 1 + #else + #define GTLR_USE_FRAMEWORK_IMPORTS 0 + #endif +#endif + +#import "GTLRService.h" + +#import "GTLRFramework.h" +#import "GTLRURITemplate.h" +#import "GTLRUtilities.h" + +#if GTLR_USE_FRAMEWORK_IMPORTS + #import <GTMSessionFetcher/GTMSessionFetcher.h> + #import <GTMSessionFetcher/GTMSessionFetcherService.h> + #import <GTMSessionFetcher/GTMMIMEDocument.h> +#else + #import "GTMSessionFetcher.h" + #import "GTMSessionFetcherService.h" + #import "GTMMIMEDocument.h" +#endif // GTLR_USE_FRAMEWORK_IMPORTS + + +#ifndef STRIP_GTM_FETCH_LOGGING + #error GTMSessionFetcher headers should have defaulted this if it wasn't already defined. +#endif + +NSString *const kGTLRServiceErrorDomain = @"com.google.GTLRServiceDomain"; +NSString *const kGTLRErrorObjectDomain = @"com.google.GTLRErrorObjectDomain"; +NSString *const kGTLRServiceErrorBodyDataKey = @"body"; +NSString *const kGTLRServiceErrorContentIDKey = @"contentID"; +NSString *const kGTLRStructuredErrorKey = @"GTLRStructuredError"; +NSString *const kGTLRETagWildcard = @"*"; + +NSString *const kGTLRServiceTicketStartedNotification = @"kGTLRServiceTicketStartedNotification"; +NSString *const kGTLRServiceTicketStoppedNotification = @"kGTLRServiceTicketStoppedNotification"; +NSString *const kGTLRServiceTicketParsingStartedNotification = @"kGTLRServiceTicketParsingStartedNotification"; +NSString *const kGTLRServiceTicketParsingStoppedNotification = @"kGTLRServiceTicketParsingStoppedNotification"; + +NSString *const kXIosBundleIdHeader = @"X-Ios-Bundle-Identifier"; + +static NSString *const kDeveloperAPIQueryParamKey = @"key"; + +static const NSUInteger kMaxNumberOfNextPagesFetched = 25; + +static const NSUInteger kMaxGETURLLength = 2048; + +// we'll enforce 50K chunks minimum just to avoid the server getting hit +// with too many small upload chunks +static const NSUInteger kMinimumUploadChunkSize = 50000; + +// Helper to get the ETag if it is defined on an object. +static NSString *ETagIfPresent(GTLRObject *obj) { + NSString *result = [obj.JSON objectForKey:@"etag"]; + return result; +} + +// Merge two dictionaries. Either may be nil. +// If both are nil, return nil. +// In case of a key collision, values of the second dictionary prevail. +static NSDictionary *MergeDictionaries(NSDictionary *recessiveDict, NSDictionary *dominantDict) { + if (!dominantDict) return recessiveDict; + if (!recessiveDict) return dominantDict; + + NSMutableDictionary *worker = [recessiveDict mutableCopy]; + [worker addEntriesFromDictionary:dominantDict]; + return worker; +} + +@interface GTLRServiceTicket () + +- (instancetype)initWithService:(GTLRService *)service + executionParameters:(GTLRServiceExecutionParameters *)params NS_DESIGNATED_INITIALIZER; + +// Thread safety: ticket properties are all publicly exposed as read-only. +// +// Service execution of a ticket is serial (started by the app, then executing on the fetcher +// callback queue and then the parse queue), so we don't need to worry about synchronization. +// +// One important exception is when the user invoked cancelTicket. During cancellation, ticket +// properties are released. This should be harmless even during the fetch start-parse-callback +// phase because nothing released in cancelTicket is used to begin a fetch, and the cancellation +// flag will prevent any application callbacks from being invoked. +// +// The cancel and objectFetcher properties are synchronized on the ticket. + +// Ticket properties exposed publicly as readonly. +@property(atomic, readwrite, nullable) id<GTLRQueryProtocol> originalQuery; +@property(atomic, readwrite, nullable) id<GTLRQueryProtocol> executingQuery; +@property(atomic, readwrite, nullable) GTMSessionFetcher *objectFetcher; +@property(nonatomic, readwrite, nullable) NSURLRequest *fetchRequest; +@property(nonatomic, readwrite, nullable) GTLRObject *postedObject; +@property(nonatomic, readwrite, nullable) GTLRObject *fetchedObject; +@property(nonatomic, readwrite, nullable) NSError *fetchError; +@property(nonatomic, readwrite) BOOL hasCalledCallback; +@property(nonatomic, readwrite) NSUInteger pagesFetchedCounter; +@property(readwrite, atomic, strong) id<GTLRObjectClassResolver> objectClassResolver; + +// Internal properties copied from the service. +@property(nonatomic, assign) BOOL allowInsecureQueries; +@property(nonatomic, strong) GTMSessionFetcherService *fetcherService; +@property(nonatomic, strong, nullable) id<GTMFetcherAuthorizationProtocol> authorizer; + +// Internal properties copied from serviceExecutionParameters. +@property(nonatomic, getter=isRetryEnabled) BOOL retryEnabled; +@property(nonatomic, readwrite) NSTimeInterval maxRetryInterval; +@property(nonatomic, strong, nullable) GTLRServiceRetryBlock retryBlock; +@property(nonatomic, strong, nullable) GTLRServiceUploadProgressBlock uploadProgressBlock; +@property(nonatomic, strong, nullable) GTLRServiceTestBlock testBlock; +@property(nonatomic, readwrite) BOOL shouldFetchNextPages; + +// Internal properties used by the service. +#if GTM_BACKGROUND_TASK_FETCHING +// Access to backgroundTaskIdentifier should be protected by @synchronized(self). +@property(nonatomic, assign) UIBackgroundTaskIdentifier backgroundTaskIdentifier; +#endif // GTM_BACKGROUND_TASK_FETCHING + +// Dispatch group enabling waitForTicket: to delay until async callbacks and notifications +// related to the ticket have completed. +@property(nonatomic, readonly) dispatch_group_t callbackGroup; + +// startBackgroundTask and endBackgroundTask do nothing if !GTM_BACKGROUND_TASK_FETCHING +- (void)startBackgroundTask; +- (void)endBackgroundTask; + +- (void)notifyStarting:(BOOL)isStarting; +- (void)releaseTicketCallbacks; + +// Posts a notification on the main queue using the ticket's dispatch group. +- (void)postNotificationOnMainThreadWithName:(NSString *)name + object:(id)object + userInfo:(NSDictionary *)userInfo; +@end + +#if !defined(GTLR_HAS_SESSION_UPLOAD_FETCHER_IMPORT) + #if defined(COCOAPODS) && COCOAPODS + #define GTLR_HAS_SESSION_UPLOAD_FETCHER_IMPORT 1 + #else + #define GTLR_HAS_SESSION_UPLOAD_FETCHER_IMPORT 0 + #endif +#endif + +#if GTLR_HAS_SESSION_UPLOAD_FETCHER_IMPORT + #if GTLR_USE_FRAMEWORK_IMPORTS + #import <GTMSessionFetcher/GTMSessionUploadFetcher.h> + #else + #import "GTMSessionUploadFetcher.h" + #endif // GTLR_USE_FRAMEWORK_IMPORTS +#else +// If the upload fetcher class is available, it can be used for chunked uploads +// +// We locally declare some methods of the upload fetcher so we +// do not need to import the header, as some projects may not have it available +@interface GTMSessionUploadFetcher : GTMSessionFetcher + ++ (instancetype)uploadFetcherWithRequest:(NSURLRequest *)request + uploadMIMEType:(NSString *)uploadMIMEType + chunkSize:(int64_t)chunkSize + fetcherService:(GTM_NULLABLE GTMSessionFetcherService *)fetcherServiceOrNil; + ++ (instancetype)uploadFetcherWithLocation:(NSURL *)uploadLocationURL + uploadMIMEType:(NSString *)uploadMIMEType + chunkSize:(int64_t)chunkSize + fetcherService:(GTM_NULLABLE GTMSessionFetcherService *)fetcherServiceOrNil; + +@property(strong) NSURL *uploadLocationURL; +@property(strong) NSData *uploadData; +@property(strong) NSURL *uploadFileURL; +@property(strong) NSFileHandle *uploadFileHandle; + +- (void)pauseFetching; +- (void)resumeFetching; +- (BOOL)isPaused; +@end +#endif // GTLR_HAS_SESSION_UPLOAD_FETCHER_IMPORT + + +@interface GTLRObject (StandardProperties) +// Common properties on GTLRObject that are invoked below. +@property(nonatomic, copy) NSString *nextPageToken; +@end + +// This class encapsulates the pieces of a single batch response, including +// inner http response code and message, inner headers, JSON body (parsed as a dictionary), +// or parsing NSError. +// +// See responsePartsWithMIMEParts: for an example of the wire format data used +// to populate this object. +@interface GTLRBatchResponsePart : NSObject +@property(nonatomic, copy) NSString *contentID; +@property(nonatomic, assign) NSInteger statusCode; +@property(nonatomic, copy) NSString *statusString; +@property(nonatomic, strong) NSDictionary *headers; +@property(nonatomic, strong) NSDictionary *JSON; +@property(nonatomic, strong) NSError *parseError; +@end + +@implementation GTLRBatchResponsePart +@synthesize contentID = _contentID, + headers = _headers, + JSON = _JSON, + parseError = _parseError, + statusCode = _statusCode, + statusString = _statusString; +#if DEBUG +- (NSString *)description { + return [NSString stringWithFormat:@"%@ %p: %@\n%ld %@\nheaders:%@\nJSON:%@\nerror:%@", + [self class], self, self.contentID, (long)self.statusCode, self.statusString, + self.headers, self.JSON, self.parseError]; +} +#endif +@end + +// GTLRResourceURLQuery is an internal class used as a query object placeholder +// when fetchObjectWithURL: is invoked by the client app. This lets the service's +// plumbing treat the request like other queries, without allowing users to +// set arbitrary query properties that may not work as anticipated. +@interface GTLRResourceURLQuery : GTLRQuery + +@property(nonatomic, strong, nullable) NSURL *resourceURL; + ++ (instancetype)queryWithResourceURL:(NSURL *)resourceURL + objectClass:(nullable Class)objectClass; + +@end + +@implementation GTLRService { + NSString *_userAgent; + NSString *_overrideUserAgent; + NSDictionary *_serviceProperties; // Properties retained for the convenience of the client app. + NSUInteger _uploadChunkSize; // Only applies to resumable chunked uploads. +} + +@synthesize additionalHTTPHeaders = _additionalHTTPHeaders, + additionalURLQueryParameters = _additionalURLQueryParameters, + allowInsecureQueries = _allowInsecureQueries, + callbackQueue = _callbackQueue, + APIKey = _apiKey, + APIKeyRestrictionBundleID = _apiKeyRestrictionBundleID, + batchPath = _batchPath, + dataWrapperRequired = _dataWrapperRequired, + fetcherService = _fetcherService, + maxRetryInterval = _maxRetryInterval, + parseQueue = _parseQueue, + prettyPrintQueryParameterNames = _prettyPrintQueryParameterNames, + resumableUploadPath = _resumableUploadPath, + retryBlock = _retryBlock, + retryEnabled = _retryEnabled, + rootURLString = _rootURLString, + servicePath = _servicePath, + shouldFetchNextPages = _shouldFetchNextPages, + simpleUploadPath = _simpleUploadPath, + objectClassResolver = _objectClassResolver, + testBlock = _testBlock, + uploadProgressBlock = _uploadProgressBlock, + userAgentAddition = _userAgentAddition; + ++ (Class)ticketClass { + return [GTLRServiceTicket class]; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _parseQueue = dispatch_queue_create("com.google.GTLRServiceParse", DISPATCH_QUEUE_SERIAL); + _callbackQueue = dispatch_get_main_queue(); + _fetcherService = [[GTMSessionFetcherService alloc] init]; + + // Make the session fetcher use a background delegate queue instead of bouncing + // through the main queue for its callbacks from NSURLSession. This should improve + // performance, and eventually be the default behavior for the fetcher. + NSOperationQueue *delegateQueue = [[NSOperationQueue alloc] init]; + delegateQueue.maxConcurrentOperationCount = 1; + delegateQueue.name = @"com.google.GTLRServiceFetcherDelegate"; + _fetcherService.sessionDelegateQueue = delegateQueue; + + NSDictionary<NSString *, Class> *kindMap = [[self class] kindStringToClassMap]; + _objectClassResolver = [GTLRObjectClassResolver resolverWithKindMap:kindMap]; + } + return self; +} + +- (NSString *)requestUserAgent { + if (_overrideUserAgent != nil) { + return _overrideUserAgent; + } + + NSString *userAgent = self.userAgent; + if (userAgent.length == 0) { + // The service instance is missing an explicit user-agent; use the bundle ID + // or process name. Don't use the bundle ID of the library's framework. + NSBundle *owningBundle = [NSBundle bundleForClass:[self class]]; + if (owningBundle == nil + || [owningBundle.bundleIdentifier isEqual:@"com.google.GTLR"]) { + owningBundle = [NSBundle mainBundle]; + } + userAgent = GTMFetcherApplicationIdentifier(owningBundle); + } + + NSString *requestUserAgent = userAgent; + + // if the user agent already specifies the library version, we'll + // use it verbatim in the request + NSString *libraryString = @"google-api-objc-client"; + NSRange libRange = [userAgent rangeOfString:libraryString + options:NSCaseInsensitiveSearch]; + if (libRange.location == NSNotFound) { + // the user agent doesn't specify the client library, so append that + // information, and the system version + NSString *libVersionString = GTLRFrameworkVersionString(); + + NSString *systemString = GTMFetcherSystemVersionString(); + + // We don't clean this with GTMCleanedUserAgentString so spaces are + // preserved + NSString *userAgentAddition = self.userAgentAddition; + NSString *customString = userAgentAddition ? + [@" " stringByAppendingString:userAgentAddition] : @""; + + // Google servers look for gzip in the user agent before sending gzip- + // encoded responses. See Service.java + requestUserAgent = [NSString stringWithFormat:@"%@ %@/%@ %@%@ (gzip)", + userAgent, libraryString, libVersionString, systemString, customString]; + } + return requestUserAgent; +} + +- (void)setMainBundleIDRestrictionWithAPIKey:(NSString *)apiKey { + self.APIKey = apiKey; + self.APIKeyRestrictionBundleID = [[NSBundle mainBundle] bundleIdentifier]; +} + +- (NSMutableURLRequest *)requestForURL:(NSURL *)url + ETag:(NSString *)etag + httpMethod:(NSString *)httpMethod + ticket:(GTLRServiceTicket *)ticket { + + // subclasses may add headers to this + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url + cachePolicy:NSURLRequestReloadIgnoringCacheData + timeoutInterval:60]; + NSString *requestUserAgent = self.requestUserAgent; + [request setValue:requestUserAgent forHTTPHeaderField:@"User-Agent"]; + + if (httpMethod.length > 0) { + [request setHTTPMethod:httpMethod]; + } + + if (etag.length > 0) { + + // it's rather unexpected for an etagged object to be provided for a GET, + // but we'll check for an etag anyway, similar to HttpGDataRequest.java, + // and if present use it to request only an unchanged resource + + BOOL isDoingHTTPGet = (httpMethod == nil + || [httpMethod caseInsensitiveCompare:@"GET"] == NSOrderedSame); + + if (isDoingHTTPGet) { + + // set the etag header, even if weak, indicating we don't want + // another copy of the resource if it's the same as the object + [request setValue:etag forHTTPHeaderField:@"If-None-Match"]; + + } else { + + // if we're doing PUT or DELETE, set the etag header indicating + // we only want to update the resource if our copy matches the current + // one (unless the etag is weak and so shouldn't be a constraint at all) + BOOL isWeakETag = [etag hasPrefix:@"W/"]; + + BOOL isModifying = + [httpMethod caseInsensitiveCompare:@"PUT"] == NSOrderedSame + || [httpMethod caseInsensitiveCompare:@"DELETE"] == NSOrderedSame + || [httpMethod caseInsensitiveCompare:@"PATCH"] == NSOrderedSame; + + if (isModifying && !isWeakETag) { + [request setValue:etag forHTTPHeaderField:@"If-Match"]; + } + } + } + + return request; +} + +// objectRequestForURL returns an NSMutableURLRequest for a GTLRObject +// +// the object is the object being sent to the server, or nil; +// the http method may be nil for get, or POST, PUT, DELETE + +- (NSMutableURLRequest *)objectRequestForURL:(NSURL *)url + object:(GTLRObject *)object + contentType:(NSString *)contentType + contentLength:(NSString *)contentLength + ETag:(NSString *)etag + httpMethod:(NSString *)httpMethod + additionalHeaders:(NSDictionary *)additionalHeaders + ticket:(GTLRServiceTicket *)ticket { + if (object) { + // if the object being sent has an etag, add it to the request header to + // avoid retrieving a duplicate or to avoid writing over an updated + // version of the resource on the server + // + // Typically, delete requests will provide an explicit ETag parameter, and + // other requests will have the ETag carried inside the object being updated + if (etag == nil) { + etag = ETagIfPresent(object); + } + } + + NSMutableURLRequest *request = [self requestForURL:url + ETag:etag + httpMethod:httpMethod + ticket:ticket]; + [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; + [request setValue:contentType forHTTPHeaderField:@"Content-Type"]; + + [request setValue:@"no-cache" forHTTPHeaderField:@"Cache-Control"]; + + if (contentLength) { + [request setValue:contentLength forHTTPHeaderField:@"Content-Length"]; + } + + // Add the additional http headers from the service, and then from the query + NSDictionary *headers = self.additionalHTTPHeaders; + for (NSString *key in headers) { + NSString *value = [headers objectForKey:key]; + [request setValue:value forHTTPHeaderField:key]; + } + + headers = additionalHeaders; + for (NSString *key in headers) { + NSString *value = [headers objectForKey:key]; + [request setValue:value forHTTPHeaderField:key]; + } + + return request; +} + +#pragma mark - + +- (NSMutableURLRequest *)requestForQuery:(GTLRQuery *)query { + GTLR_DEBUG_ASSERT(query.bodyObject == nil, + @"requestForQuery: supports only GET methods, but was passed: %@", query); + GTLR_DEBUG_ASSERT(query.uploadParameters == nil, + @"requestForQuery: does not support uploads, but was passed: %@", query); + + NSURL *url = [self URLFromQueryObject:query + usePartialPaths:NO + includeServiceURLQueryParams:YES]; + + // If there is a developer key, add it onto the url. + NSString *apiKey = self.APIKey; + if (apiKey.length > 0) { + NSDictionary *queryParameters; + queryParameters = @{ kDeveloperAPIQueryParamKey : apiKey }; + url = [GTLRService URLWithString:url.absoluteString + queryParameters:queryParameters]; + } + + NSMutableURLRequest *request = [self requestForURL:url + ETag:nil + httpMethod:query.httpMethod + ticket:nil]; + NSString *apiRestriction = self.APIKeyRestrictionBundleID; + if ([apiRestriction length] > 0) { + [request setValue:apiRestriction forHTTPHeaderField:kXIosBundleIdHeader]; + } + + NSDictionary *headers = self.additionalHTTPHeaders; + for (NSString *key in headers) { + NSString *value = [headers objectForKey:key]; + [request setValue:value forHTTPHeaderField:key]; + } + + headers = query.additionalHTTPHeaders; + for (NSString *key in headers) { + NSString *value = [headers objectForKey:key]; + [request setValue:value forHTTPHeaderField:key]; + } + + return request; +} + +// common fetch starting method + +- (GTLRServiceTicket *)fetchObjectWithURL:(NSURL *)targetURL + objectClass:(Class)objectClass + bodyObject:(GTLRObject *)bodyObject + dataToPost:(NSData *)dataToPost + ETag:(NSString *)etag + httpMethod:(NSString *)httpMethod + mayAuthorize:(BOOL)mayAuthorize + completionHandler:(GTLRServiceCompletionHandler)completionHandler + executingQuery:(id<GTLRQueryProtocol>)executingQuery + ticket:(GTLRServiceTicket *)ticket { + // Once inside this method, we should not access any service properties that may reasonably + // be changed by the app, as this method may execute multiple times during query execution + // and we want consistent behavior. Service properties should be copied to the ticket. + + GTLR_DEBUG_ASSERT(executingQuery != nil, + @"no query? service additionalURLQueryParameters needs to be added to targetURL"); + + GTLR_DEBUG_ASSERT(targetURL != nil, @"no url?"); + if (targetURL == nil) return nil; + + BOOL hasExecutionParams = [executingQuery hasExecutionParameters]; + GTLRServiceExecutionParameters *executionParams = (hasExecutionParams ? + executingQuery.executionParameters : nil); + + // We need to create a ticket unless one was created earlier (like during authentication.) + if (!ticket) { + ticket = [[[[self class] ticketClass] alloc] initWithService:self + executionParameters:executionParams]; + [ticket notifyStarting:YES]; + } + + // If there is a developer key, add it onto the URL. + NSString *apiKey = ticket.APIKey; + if (apiKey.length > 0) { + NSDictionary *queryParameters; + queryParameters = @{ kDeveloperAPIQueryParamKey : apiKey }; + targetURL = [GTLRService URLWithString:targetURL.absoluteString + queryParameters:queryParameters]; + } + + NSString *contentType = @"application/json; charset=utf-8"; + NSString *contentLength; // nil except for single-request uploads. + + if ([executingQuery isBatchQuery]) { + contentType = [NSString stringWithFormat:@"multipart/mixed; boundary=%@", + ((GTLRBatchQuery *)executingQuery).boundary]; + } + + + GTLRUploadParameters *uploadParams = executingQuery.uploadParameters; + + if (uploadParams.shouldUploadWithSingleRequest) { + NSData *uploadData = uploadParams.data; + NSString *uploadMIMEType = uploadParams.MIMEType; + if (!uploadData) { + GTLR_DEBUG_ASSERT(0, @"Uploading with a single request requires bytes to upload as NSData"); + } else { + if (uploadParams.shouldSendUploadOnly) { + contentType = uploadMIMEType; + dataToPost = uploadData; + contentLength = @(dataToPost.length).stringValue; + } else { + GTMMIMEDocument *mimeDoc = [GTMMIMEDocument MIMEDocument]; + if (dataToPost) { + // Include the object as metadata with the upload. + [mimeDoc addPartWithHeaders:@{ @"Content-Type" : contentType } + body:dataToPost]; + } + [mimeDoc addPartWithHeaders:@{ @"Content-Type" : uploadMIMEType } + body:uploadData]; + + dispatch_data_t mimeDispatchData; + unsigned long long mimeLength; + NSString *mimeBoundary; + [mimeDoc generateDispatchData:&mimeDispatchData + length:&mimeLength + boundary:&mimeBoundary]; + + contentType = [NSString stringWithFormat:@"multipart/related; boundary=%@", mimeBoundary]; + dataToPost = (NSData *)mimeDispatchData; + contentLength = @(mimeLength).stringValue; + } + } + } + + NSDictionary *additionalHeaders = nil; + NSString *restriction = self.APIKeyRestrictionBundleID; + if ([restriction length] > 0) { + additionalHeaders = @{ kXIosBundleIdHeader : restriction }; + } + + NSDictionary *queryAdditionalHeaders = executingQuery.additionalHTTPHeaders; + if (queryAdditionalHeaders) { + if (additionalHeaders) { + NSMutableDictionary *builder = [additionalHeaders mutableCopy]; + [builder addEntriesFromDictionary:queryAdditionalHeaders]; + additionalHeaders = builder; + } else { + additionalHeaders = queryAdditionalHeaders; + } + } + + NSURLRequest *request = [self objectRequestForURL:targetURL + object:bodyObject + contentType:contentType + contentLength:contentLength + ETag:etag + httpMethod:httpMethod + additionalHeaders:additionalHeaders + ticket:ticket]; + ticket.postedObject = bodyObject; + ticket.executingQuery = executingQuery; + + GTLRQuery *originalQuery = (GTLRQuery *)ticket.originalQuery; + if (originalQuery == nil) { + originalQuery = (GTLRQuery *)executingQuery; + ticket.originalQuery = originalQuery; + } + + // Some proxy servers (and some web servers) have issues with GET URLs being + // too long, trap that and move the query parameters into the body. The + // uploadParams and dataToPost should be nil for a GET, but playing it safe + // and confirming. + NSString *requestHTTPMethod = request.HTTPMethod; + BOOL isDoingHTTPGet = + (requestHTTPMethod == nil + || [requestHTTPMethod caseInsensitiveCompare:@"GET"] == NSOrderedSame); + if (isDoingHTTPGet && + (request.URL.absoluteString.length >= kMaxGETURLLength) && + (uploadParams == nil) && + (dataToPost == nil)) { + NSString *urlString = request.URL.absoluteString; + NSRange range = [urlString rangeOfString:@"?"]; + if (range.location != NSNotFound) { + NSURL *trimmedURL = [NSURL URLWithString:[urlString substringToIndex:range.location]]; + NSString *urlArgsString = [urlString substringFromIndex:(range.location + 1)]; + if (trimmedURL && (urlArgsString.length > 0)) { + dataToPost = [urlArgsString dataUsingEncoding:NSUTF8StringEncoding]; + NSMutableURLRequest *mutableRequest = [request mutableCopy]; + mutableRequest.URL = trimmedURL; + mutableRequest.HTTPMethod = @"POST"; + [mutableRequest setValue:@"GET" forHTTPHeaderField:@"X-HTTP-Method-Override"]; + [mutableRequest setValue:@"application/x-www-form-urlencoded" + forHTTPHeaderField:@"Content-Type"]; + [mutableRequest setValue:@(dataToPost.length).stringValue + forHTTPHeaderField:@"Content-Length"]; + request = mutableRequest; + } + } + } + ticket.fetchRequest = request; + + GTLRServiceTestBlock testBlock = ticket.testBlock; + if (testBlock) { + [self simulateFetchWithTicket:ticket + testBlock:testBlock + dataToPost:dataToPost + completionHandler:completionHandler]; + return ticket; + } + + GTMSessionFetcherService *fetcherService = ticket.fetcherService; + GTMSessionFetcher *fetcher; + + if (uploadParams == nil || uploadParams.shouldUploadWithSingleRequest) { + // Create a single-request fetcher. + fetcher = [fetcherService fetcherWithRequest:request]; + } else { + fetcher = [self uploadFetcherWithRequest:request + fetcherService:fetcherService + params:uploadParams]; + } + + if (ticket.allowInsecureQueries) { + fetcher.allowLocalhostRequest = YES; + fetcher.allowedInsecureSchemes = @[ @"http" ]; + } + + NSString *loggingName = executingQuery.loggingName; + if (loggingName.length > 0) { + NSUInteger pageNumber = ticket.pagesFetchedCounter + 1; + if (pageNumber > 1) { + loggingName = [loggingName stringByAppendingFormat:@", page %tu", pageNumber]; + } + fetcher.comment = loggingName; + } + + if (!mayAuthorize) { + fetcher.authorizer = nil; + } else { + fetcher.authorizer = ticket.authorizer; + } + + // copy the ticket's retry settings into the fetcher + fetcher.retryEnabled = ticket.retryEnabled; + fetcher.maxRetryInterval = ticket.maxRetryInterval; + + BOOL shouldExamineRetries = (ticket.retryBlock != nil); + if (shouldExamineRetries) { + GTLR_DEBUG_ASSERT(ticket.retryEnabled, @"Setting retry block without retry enabled."); + + fetcher.retryBlock = ^(BOOL suggestedWillRetry, NSError *error, + GTMSessionFetcherRetryResponse response) { + // The object fetcher may call into this retry block; this one invokes the + // selector provided by the user. + GTLRServiceRetryBlock retryBlock = ticket.retryBlock; + if (!retryBlock) { + response(suggestedWillRetry); + } else { + dispatch_group_async(ticket.callbackGroup, ticket.callbackQueue, ^{ + if (ticket.cancelled) { + response(NO); + return; + } + BOOL willRetry = retryBlock(ticket, suggestedWillRetry, error); + response(willRetry); + }); + } + }; + } + + // Remember the object fetcher in the ticket. + ticket.objectFetcher = fetcher; + + // Set the upload data. + fetcher.bodyData = dataToPost; + + // Have the fetcher call back on the parse queue. + fetcher.callbackQueue = self.parseQueue; + + // If this ticket is paging, end any ongoing background task immediately, and + // rely on the fetcher's background task now instead. + [ticket endBackgroundTask]; + + [fetcher beginFetchWithCompletionHandler:^(NSData * _Nullable data, NSError * _Nullable error) { + // We now have the JSON data for an object, or an error. + GTLR_ASSERT_CURRENT_QUEUE_DEBUG(self.parseQueue); + + // Until now, the only async operation has been the fetch, and we rely on the fetcher's + // background task on iOS to get us here if the app was backgrounded. + // + // Now we'll let the ticket create a background task so that the async parsing and call back to + // the app will happen if the app is sent to the background. The ticket is responsible for + // ending the background task. + [ticket startBackgroundTask]; + + if (ticket.cancelled) { + // If the user cancels the ticket, then cancelTicket will stop the fetcher so this + // callback probably won't occur. + // + // But just for safety, if we get here, skip any parsing steps by fabricating an error. + data = nil; + error = [NSError errorWithDomain:NSURLErrorDomain + code:NSURLErrorCancelled + userInfo:nil]; + } + + if (error == nil) { + // Successful fetch. + if (data.length > 0) { + [self prepareToParseObjectForFetcher:fetcher + executingQuery:executingQuery + ticket:ticket + error:error + defaultClass:objectClass + completionHandler:completionHandler]; + } else { + // no data (such as when deleting) + [self handleParsedObjectForFetcher:fetcher + executingQuery:executingQuery + ticket:ticket + error:nil + parsedObject:nil + hasSentParsingStartNotification:NO + completionHandler:completionHandler]; + } + return; + } + + // Failed fetch. + NSInteger status = [error code]; + if (status >= 300) { + // Return the HTTP error status code along with a more descriptive error + // from within the HTTP response payload. + NSData *responseData = fetcher.downloadedData; + if (responseData.length > 0) { + NSDictionary *responseHeaders = fetcher.responseHeaders; + NSString *responseContentType = [responseHeaders objectForKey:@"Content-Type"]; + + if (data.length > 0) { + if ([responseContentType hasPrefix:@"application/json"]) { + NSError *parseError = nil; + NSMutableDictionary *jsonWrapper = + [NSJSONSerialization JSONObjectWithData:(NSData * _Nonnull)data + options:NSJSONReadingMutableContainers + error:&parseError]; + if (parseError) { + // We could not parse the JSON payload + error = parseError; + } else { + // HTTP Streaming defined by Google services is is an array + // of requests and replies. This code never makes one of + // these requests; but, some GET apis can actually be to + // a Streaming result (for media?), so the errors can still + // come back in an array. + if ([jsonWrapper isKindOfClass:[NSArray class]]) { + NSArray *jsonWrapperAsArray = (NSArray *)jsonWrapper; +#if DEBUG + if (jsonWrapperAsArray.count > 1) { + GTLR_DEBUG_LOG(@"Got error array with >1 item, only using first. Full list: %@", + jsonWrapperAsArray); + } +#endif + // Use the first. + jsonWrapper = [jsonWrapperAsArray firstObject]; + } + + // Convert the JSON error payload into a structured error + NSMutableDictionary *errorJSON = [jsonWrapper valueForKey:@"error"]; + if (errorJSON) { + GTLRErrorObject *errorObject = [GTLRErrorObject objectWithJSON:errorJSON]; + error = [errorObject foundationError]; + } + } + } else { + // No structured JSON error was available; make a plaintext server + // error response visible in the error object. + NSString *reasonStr = [[NSString alloc] initWithData:(NSData * _Nonnull)data + encoding:NSUTF8StringEncoding]; + NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : reasonStr }; + error = [NSError errorWithDomain:kGTMSessionFetcherStatusDomain + code:status + userInfo:userInfo]; + } + } else { + // Response data length is zero; we'll settle for returning the + // fetcher's error. + } + } + } + + [self handleParsedObjectForFetcher:fetcher + executingQuery:executingQuery + ticket:ticket + error:error + parsedObject:nil + hasSentParsingStartNotification:NO + completionHandler:completionHandler]; + }]; // fetcher completion handler + + // If something weird happens and the networking callbacks have been called + // already synchronously, we don't want to return the ticket since the caller + // will never know when to stop retaining it, so we'll make sure the + // success/failure callbacks have not yet been called by checking the + // ticket + if (ticket.hasCalledCallback) { + return nil; + } + + return ticket; +} + +- (GTMSessionUploadFetcher *)uploadFetcherWithRequest:(NSURLRequest *)request + fetcherService:(GTMSessionFetcherService *)fetcherService + params:(GTLRUploadParameters *)uploadParams { + // Hang on to the user's requested chunk size, and ensure it's not tiny + NSUInteger uploadChunkSize = [self serviceUploadChunkSize]; + if (uploadChunkSize < kMinimumUploadChunkSize) { + uploadChunkSize = kMinimumUploadChunkSize; + } + + NSString *uploadClassName = GTLR_CLASSNAME_STR(GTMSessionUploadFetcher); + Class uploadClass = NSClassFromString(uploadClassName); + GTLR_ASSERT(uploadClass != nil, @"GTMSessionUploadFetcher needed"); + + NSString *uploadMIMEType = uploadParams.MIMEType; + NSData *uploadData = uploadParams.data; + NSURL *uploadFileURL = uploadParams.fileURL; + NSFileHandle *uploadFileHandle = uploadParams.fileHandle; + NSURL *uploadLocationURL = uploadParams.uploadLocationURL; + + // Create the upload fetcher. + GTMSessionUploadFetcher *fetcher; + if (uploadLocationURL) { + // Resuming with the session fetcher and a file URL. + GTLR_DEBUG_ASSERT(uploadFileURL != nil, @"Resume requires a file URL"); + fetcher = [uploadClass uploadFetcherWithLocation:uploadLocationURL + uploadMIMEType:uploadMIMEType + chunkSize:(int64_t)uploadChunkSize + fetcherService:fetcherService]; + fetcher.uploadFileURL = uploadFileURL; + } else { + fetcher = [uploadClass uploadFetcherWithRequest:request + uploadMIMEType:uploadMIMEType + chunkSize:(int64_t)uploadChunkSize + fetcherService:fetcherService]; + if (uploadFileURL) { + fetcher.uploadFileURL = uploadFileURL; + } else if (uploadData) { + fetcher.uploadData = uploadData; + } else if (uploadFileHandle) { +#if DEBUG + if (uploadParams.useBackgroundSession) { + GTLR_DEBUG_LOG(@"Warning: GTLRUploadParameters should be supplied an uploadFileURL rather" + @" than a file handle to support background uploads.\n %@", uploadParams); + } +#endif + fetcher.uploadFileHandle = uploadFileHandle; + } + } + fetcher.useBackgroundSession = uploadParams.useBackgroundSession; + return fetcher; +} + +#pragma mark - + +- (GTLRServiceTicket *)executeBatchQuery:(GTLRBatchQuery *)batchObj + completionHandler:(GTLRServiceCompletionHandler)completionHandler + ticket:(GTLRServiceTicket *)ticket { + // Copy the original batch object and each query inside so our working queries cannot be modified + // by the caller, and release the callback blocks from the supplied query objects. + GTLRBatchQuery *batchCopy = [batchObj copy]; + [batchObj invalidateQuery]; + + NSArray *queries = batchCopy.queries; + NSUInteger numberOfQueries = queries.count; + if (numberOfQueries == 0) return nil; + + // Create the batch of REST calls. + NSMutableSet *requestIDs = [NSMutableSet setWithCapacity:numberOfQueries]; + NSMutableSet *loggingNames = [NSMutableSet set]; + GTMMIMEDocument *mimeDoc = [GTMMIMEDocument MIMEDocument]; + + // Each batch part has two "header" sections, an outer and inner. + // The inner headers are preceded by a line specifying the http request. + // So a part looks like this: + // + // --END_OF_PART + // Content-ID: gtlr_3 + // Content-Transfer-Encoding: binary + // Content-Type: application/http + // + // POST https://www.googleapis.com/drive/v3/files/ + // Content-Length: 0 + // Content-Type: application/json + // + // { + // "id": "04109509152946699072k" + // } + + for (GTLRQuery *query in queries) { + GTLRObject *bodyObject = query.bodyObject; + NSDictionary *bodyJSON = bodyObject.JSON; + NSString *requestID = query.requestID; + + if (requestID.length == 0) { + GTLR_DEBUG_ASSERT(0, @"Invalid query ID: %@", [query class]); + return nil; + } + + if ([requestIDs containsObject:requestID]) { + GTLR_DEBUG_ASSERT(0, @"Duplicate request ID in batch: %@", requestID); + return nil; + } + [requestIDs addObject:requestID]; + + // Create the inner request, body, and headers. + + NSURL *requestURL = [self URLFromQueryObject:query + usePartialPaths:YES + includeServiceURLQueryParams:NO]; + NSString *requestURLString = requestURL.absoluteString; + + NSError *error = nil; + NSData *bodyData; + if (bodyJSON) { + bodyData = [NSJSONSerialization dataWithJSONObject:bodyJSON + options:0 + error:&error]; + if (bodyData == nil) { + GTLR_DEBUG_ASSERT(0, @"JSON generation error: %@\n JSON: %@", error, bodyJSON); + return nil; + } + } + + NSString *httpRequestString = [NSString stringWithFormat:@"%@ %@\r\n", + query.httpMethod ?: @"GET", requestURLString]; + NSDictionary *innerPartHeaders = @{ @"Content-Type" : @"application/json", + @"Content-Length" : @(bodyData.length).stringValue }; + + innerPartHeaders = MergeDictionaries(query.additionalHTTPHeaders, innerPartHeaders); + + NSData *innerPartHeadersData = [GTMMIMEDocument dataWithHeaders:innerPartHeaders]; + + NSMutableData *innerData = + [[httpRequestString dataUsingEncoding:NSUTF8StringEncoding] mutableCopy]; + [innerData appendData:innerPartHeadersData]; + if (bodyData) { + [innerData appendData:bodyData]; + } + + // Combine the outer headers with the inner headers and body data. + NSDictionary *outerPartHeaders = @{ @"Content-Type" : @"application/http", + @"Content-ID" : requestID, + @"Content-Transfer-Encoding" : @"binary" }; + + [mimeDoc addPartWithHeaders:outerPartHeaders + body:innerData]; + + NSString *loggingName = query.loggingName ?: [[query class] description]; + [loggingNames addObject:loggingName]; + } + +#if !STRIP_GTM_FETCH_LOGGING + // Set the fetcher log comment. + if (!batchCopy.loggingName) { + NSUInteger pageNumber = ticket.pagesFetchedCounter; + NSString *pageStr = @""; + if (pageNumber > 0) { + pageStr = [NSString stringWithFormat:@"page %tu, ", pageNumber + 1]; + } + batchCopy.loggingName = [NSString stringWithFormat:@"batch: %@ (%@%tu queries)", + [loggingNames.allObjects componentsJoinedByString:@", "], + pageStr, numberOfQueries]; + } +#endif + + dispatch_data_t mimeDispatchData; + unsigned long long mimeLength; + NSString *mimeBoundary; + [mimeDoc generateDispatchData:&mimeDispatchData + length:&mimeLength + boundary:&mimeBoundary]; + + batchCopy.boundary = mimeBoundary; + + BOOL mayAuthorize = (batchCopy ? !batchCopy.shouldSkipAuthorization : YES); + + NSString *rootURLString = self.rootURLString; + NSString *batchPath = self.batchPath ?: @""; + NSString *batchURLString = [rootURLString stringByAppendingString:batchPath]; + + GTLR_DEBUG_ASSERT(![batchPath hasPrefix:@"/"], + @"batchPath shouldn't start with a slash: %@", + batchPath); + + // Query parameters override service parameters. + NSDictionary *mergedQueryParams = MergeDictionaries(self.additionalURLQueryParameters, + batchObj.additionalURLQueryParameters); + NSURL *batchURL; + if (mergedQueryParams.count > 0) { + batchURL = [GTLRService URLWithString:batchURLString + queryParameters:mergedQueryParams]; + } else { + batchURL = [NSURL URLWithString:batchURLString]; + } + + GTLRServiceTicket *resultTicket = [self fetchObjectWithURL:batchURL + objectClass:[GTLRBatchResult class] + bodyObject:nil + dataToPost:(NSData *)mimeDispatchData + ETag:nil + httpMethod:@"POST" + mayAuthorize:mayAuthorize + completionHandler:completionHandler + executingQuery:batchCopy + ticket:ticket]; + return resultTicket; +} + +#pragma mark - + +// Raw REST fetch method. + +- (GTLRServiceTicket *)fetchObjectWithURL:(NSURL *)targetURL + objectClass:(Class)objectClass + bodyObject:(GTLRObject *)bodyObject + ETag:(NSString *)etag + httpMethod:(NSString *)httpMethod + mayAuthorize:(BOOL)mayAuthorize + completionHandler:(GTLRServiceCompletionHandler)completionHandler + executingQuery:(id<GTLRQueryProtocol>)executingQuery + ticket:(GTLRServiceTicket *)ticket { + // if no URL was supplied, treat this as if the fetch failed (below) + // and immediately return a nil ticket, skipping the callbacks + // + // this might be considered normal (say, updating a read-only entry + // that lacks an edit link) though higher-level calls may assert or + // return errors depending on the specific usage + if (targetURL == nil) return nil; + + NSData *dataToPost = nil; + if (bodyObject != nil && !executingQuery.uploadParameters.shouldSendUploadOnly) { + NSError *error = nil; + + NSDictionary *whatToSend; + NSDictionary *json = bodyObject.JSON; + if (json == nil) { + // Since a body object was provided, we'll ensure there's at least an empty dictionary. + json = [NSDictionary dictionary]; + } + if (_dataWrapperRequired) { + // create the top-level "data" object + whatToSend = @{ @"data" : json }; + } else { + whatToSend = json; + } + dataToPost = [NSJSONSerialization dataWithJSONObject:whatToSend + options:0 + error:&error]; + if (dataToPost == nil) { + GTLR_DEBUG_LOG(@"JSON generation error: %@", error); + } + } + + return [self fetchObjectWithURL:targetURL + objectClass:objectClass + bodyObject:bodyObject + dataToPost:dataToPost + ETag:etag + httpMethod:httpMethod + mayAuthorize:mayAuthorize + completionHandler:completionHandler + executingQuery:executingQuery + ticket:ticket]; +} + +- (void)invokeProgressCallbackForTicket:(GTLRServiceTicket *)ticket + deliveredBytes:(unsigned long long)numReadSoFar + totalBytes:(unsigned long long)total { + + GTLRServiceUploadProgressBlock block = ticket.uploadProgressBlock; + if (block) { + dispatch_group_async(ticket.callbackGroup, ticket.callbackQueue, ^{ + if (ticket.cancelled) return; + + block(ticket, numReadSoFar, total); + }); + } +} + +// Three methods handle parsing of the fetched JSON data: +// - prepareToParse posts a start notification and then spawns off parsing +// on the operation queue (if there's an operation queue) +// - parseObject does the parsing of the JSON string +// - handleParsedObject posts the stop notification and calls the callback +// with the parsed object or an error +// +// The middle method may run on a separate thread. + +- (void)prepareToParseObjectForFetcher:(GTMSessionFetcher *)fetcher + executingQuery:(id<GTLRQueryProtocol>)executingQuery + ticket:(GTLRServiceTicket *)ticket + error:(NSError *)error + defaultClass:(Class)defaultClass + completionHandler:(GTLRServiceCompletionHandler)completionHandler { + GTLR_ASSERT_CURRENT_QUEUE_DEBUG(self.parseQueue); + + [ticket postNotificationOnMainThreadWithName:kGTLRServiceTicketParsingStartedNotification + object:ticket + userInfo:nil]; + + // For unit tests to cancel during parsing, we need a synchronous notification posted. + // Because this notification is intended only for unit tests, there is no public symbol + // for the notification name. + NSNotificationCenter *nc =[NSNotificationCenter defaultCenter]; + [nc postNotificationName:@"kGTLRServiceTicketParsingStartedForTestNotification" + object:ticket + userInfo:nil]; + + NSDictionary *batchClassMap; + if ([executingQuery isBatchQuery]) { + // build a dictionary of expected classes for the batch responses + GTLRBatchQuery *batchQuery = (GTLRBatchQuery *)executingQuery; + NSArray *queries = batchQuery.queries; + batchClassMap = [NSMutableDictionary dictionaryWithCapacity:queries.count]; + for (GTLRQuery *singleQuery in queries) { + [batchClassMap setValue:singleQuery.expectedObjectClass + forKey:singleQuery.requestID]; + } + } + + [self parseObjectFromDataOfFetcher:fetcher + executingQuery:executingQuery + ticket:ticket + error:error + defaultClass:defaultClass + batchClassMap:batchClassMap + hasSentParsingStartNotification:YES + completionHandler:completionHandler]; +} + +- (void)parseObjectFromDataOfFetcher:(GTMSessionFetcher *)fetcher + executingQuery:(id<GTLRQueryProtocol>)executingQuery + ticket:(GTLRServiceTicket *)ticket + error:(NSError *)error + defaultClass:(Class)defaultClass + batchClassMap:(NSDictionary *)batchClassMap + hasSentParsingStartNotification:(BOOL)hasSentParsingStartNotification + completionHandler:(GTLRServiceCompletionHandler)completionHandler { + GTLR_ASSERT_CURRENT_QUEUE_DEBUG(self.parseQueue); + + NSError *fetchError = error; + + NSString *downloadAsDataObjectType = nil; + if (![executingQuery isBatchQuery]) { + GTLRQuery *singleQuery = (GTLRQuery *)executingQuery; + downloadAsDataObjectType = singleQuery.downloadAsDataObjectType; + } + + NSDictionary *responseHeaders = fetcher.responseHeaders; + NSString *contentType = [responseHeaders objectForKey:@"Content-Type"]; + NSData *data = fetcher.downloadedData; + BOOL hasData = data.length > 0; + BOOL isJSON = [contentType hasPrefix:@"application/json"]; + GTLRObject *parsedObject; + + if (hasData) { +#if GTLR_LOG_PERFORMANCE + NSTimeInterval secs1, secs2; + secs1 = [NSDate timeIntervalSinceReferenceDate]; +#endif + id<GTLRObjectClassResolver> objectClassResolver = ticket.objectClassResolver; + + if ((downloadAsDataObjectType.length != 0) && fetchError == nil) { + GTLRDataObject *dataObject = [GTLRDataObject object]; + dataObject.data = data; + dataObject.contentType = contentType; + parsedObject = dataObject; + } else if (isJSON) { + NSError *parseError = nil; + NSMutableDictionary *jsonWrapper = + [NSJSONSerialization JSONObjectWithData:data + options:NSJSONReadingMutableContainers + error:&parseError]; + if (jsonWrapper == nil) { + fetchError = parseError; + } else { + NSMutableDictionary *json; + + if (_dataWrapperRequired) { + json = [jsonWrapper valueForKey:@"data"]; + } else { + json = jsonWrapper; + } + + if (json != nil) { + parsedObject = [GTLRObject objectForJSON:json + defaultClass:defaultClass + objectClassResolver:objectClassResolver]; + } + } + } else { + // Has non-JSON data; it may be batch data. + NSString *boundary; + BOOL isBatchResponse = [self isContentTypeMultipart:contentType + boundary:&boundary]; + if (isBatchResponse) { + NSArray *mimeParts = [GTMMIMEDocument MIMEPartsWithBoundary:boundary + data:data]; + NSArray *responseParts = [self responsePartsWithMIMEParts:mimeParts]; + GTLRBatchResult *batchResult = [self batchResultWithResponseParts:responseParts + batchClassMap:batchClassMap + objectClassResolver:objectClassResolver]; + parsedObject = batchResult; + } else { + GTLR_DEBUG_ASSERT(0, @"Got unexpected content type '%@'", contentType); + } + } // isJSON + +#if GTLR_LOG_PERFORMANCE + secs2 = [NSDate timeIntervalSinceReferenceDate]; + NSLog(@"allocation of %@ took %f seconds", objectClass, secs2 - secs1); +#endif + } + + [self handleParsedObjectForFetcher:fetcher + executingQuery:executingQuery + ticket:ticket + error:fetchError + parsedObject:parsedObject + hasSentParsingStartNotification:hasSentParsingStartNotification + completionHandler:completionHandler]; +} + +- (void)handleParsedObjectForFetcher:(GTMSessionFetcher *)fetcher + executingQuery:(id<GTLRQueryProtocol>)executingQuery + ticket:(GTLRServiceTicket *)ticket + error:(NSError *)error + parsedObject:(GTLRObject *)object + hasSentParsingStartNotification:(BOOL)hasSentParsingStartNotification + completionHandler:(GTLRServiceCompletionHandler)completionHandler { + GTLR_ASSERT_CURRENT_QUEUE_DEBUG(self.parseQueue); + + BOOL isResourceURLQuery = [executingQuery isKindOfClass:[GTLRResourceURLQuery class]]; + + // There may not be an object due to a fetch or parsing error + BOOL shouldFetchNextPages = ticket.shouldFetchNextPages && !isResourceURLQuery; + GTLRObject *previousObject = ticket.fetchedObject; + BOOL isFirstPage = (previousObject == nil); + + if (shouldFetchNextPages && !isFirstPage && (object != nil)) { + // Accumulate new results + object = [self mergedNewResultObject:object + oldResultObject:previousObject + forQuery:executingQuery + ticket:ticket]; + } + + ticket.fetchedObject = object; + ticket.fetchError = error; + + if (hasSentParsingStartNotification) { + // we want to always balance the start and stop notifications + [ticket postNotificationOnMainThreadWithName:kGTLRServiceTicketParsingStoppedNotification + object:ticket + userInfo:nil]; + } + + BOOL shouldCallCallbacks = YES; + + if (error == nil) { + ++ticket.pagesFetchedCounter; + + // Use the nextPageToken to fetch any later pages for non-batch queries + // + // This assumes a pagination model where objects have entries in a known "items" + // field and a "nextPageToken" field, and queries support a "pageToken" + // parameter. + + if (shouldFetchNextPages) { + // Determine if we should fetch more pages of results + + GTLRQuery *nextPageQuery = + (GTLRQuery *)[self nextPageQueryForQuery:executingQuery + result:object + ticket:ticket]; + if (nextPageQuery) { + BOOL isFetchingMore = [self fetchNextPageWithQuery:nextPageQuery + completionHandler:completionHandler + ticket:ticket]; + if (isFetchingMore) { + shouldCallCallbacks = NO; + } + } else { + // nextPageQuery == nil; no more page tokens are present + #if DEBUG && !GTLR_SKIP_PAGES_WARNING + // Each next page followed to accumulate all pages of a feed takes up to + // a few seconds. When multiple pages are being fetched, that + // usually indicates that a larger page size (that is, more items per + // feed fetched) should be requested. + // + // To avoid fetching many pages, set query.maxResults so the feed + // requested is large enough to rarely need to follow next links. + NSUInteger pageCount = ticket.pagesFetchedCounter; + if (pageCount > 2) { + NSString *queryLabel; + if ([executingQuery isBatchQuery]) { + queryLabel = @"batch query"; + } else { + queryLabel = [[executingQuery class] description]; + } + GTLR_DEBUG_LOG(@"Executing %@ query required fetching %tu pages; use a query with" + @" a larger maxResults for faster results", queryLabel, pageCount); + } + #endif + } // nextPageQuery + } else { + // !ticket.shouldFetchNextPages + #if DEBUG && !GTLR_SKIP_PAGES_WARNING + // Let the developer know that there were additional pages that would have been + // fetched if shouldFetchNextPages was enabled. + // + // The client may specify a larger page size with the query's maxResults property, + // or enable automatic pagination by turning on shouldFetchNextPages on the service + // or on the query's executionParameters. + if ([executingQuery respondsToSelector:@selector(pageToken)] + && [object isKindOfClass:[GTLRCollectionObject class]] + && [object respondsToSelector:@selector(nextPageToken)] + && object.nextPageToken.length > 0) { + GTLR_DEBUG_LOG(@"Executing %@ has additional pages of results not fetched because" + @" shouldFetchNextPages is not enabled", [executingQuery class]); + } + #endif + } // ticket.shouldFetchNextPages + } // error == nil + + if (!isFirstPage) { + // Release callbacks from this completed page's query. + [executingQuery invalidateQuery]; + } + + // We no longer care about the queries for page 2 or later, so for the client + // inspecting the ticket in the callback, the executing query should be + // the original one + ticket.executingQuery = ticket.originalQuery; + + if (!shouldCallCallbacks) { + // More fetches are happening. + } else { + dispatch_group_async(ticket.callbackGroup, ticket.callbackQueue, ^{ + // First, call query-specific callback blocks. We do this before the + // fetch callback to let applications do any final clean-up (or update + // their UI) in the fetch callback. + GTLRQuery *originalQuery = (GTLRQuery *)ticket.originalQuery; + + if (!ticket.cancelled) { + if (![originalQuery isBatchQuery]) { + // Single query + GTLRServiceCompletionHandler completionBlock = originalQuery.completionBlock; + if (completionBlock) { + completionBlock(ticket, object, error); + } + } else { + [self invokeBatchCompletionsWithTicket:ticket + batchQuery:(GTLRBatchQuery *)originalQuery + batchResult:(GTLRBatchResult *)object + error:error]; + } + + if (completionHandler) { + completionHandler(ticket, object, error); + } + ticket.hasCalledCallback = YES; + } // !ticket.cancelled + + [ticket releaseTicketCallbacks]; + [ticket endBackgroundTask]; + + // Even if the ticket has been cancelled, it should notify that it's stopped. + [ticket notifyStarting:NO]; + + // Release query callback blocks. + [originalQuery invalidateQuery]; + }); + } +} + +- (BOOL)isContentTypeMultipart:(NSString *)contentType + boundary:(NSString **)outBoundary { + NSScanner *scanner = [NSScanner scannerWithString:contentType]; + // By default, the scanner skips leading whitespace. + if ([scanner scanString:@"multipart/mixed; boundary=" intoString:NULL] + && [scanner scanUpToCharactersFromSet:[NSCharacterSet newlineCharacterSet] + intoString:outBoundary]) { + return YES; + } + return NO; +} + +- (NSArray <GTLRBatchResponsePart *>*)responsePartsWithMIMEParts:(NSArray <GTMMIMEDocumentPart *>*)mimeParts { + NSMutableArray *resultParts = [NSMutableArray arrayWithCapacity:mimeParts.count]; + + for (GTMMIMEDocumentPart *mimePart in mimeParts) { + GTLRBatchResponsePart *responsePart = [self responsePartWithMIMEPart:mimePart]; + [resultParts addObject:responsePart]; + } + return resultParts; +} + +- (GTLRBatchResponsePart *)responsePartWithMIMEPart:(GTMMIMEDocumentPart *)mimePart { + // The MIME part body looks like + // + // Headers (from the MIME part): + // Content-Type: application/http + // Content-ID: response-gtlr_5 + // + // Body (including inner headers): + // HTTP/1.1 200 OK + // Content-Type: application/json; charset=UTF-8 + // Date: Sat, 16 Jan 2016 18:57:05 GMT + // Expires: Sat, 16 Jan 2016 18:57:05 GMT + // Cache-Control: private, max-age=0 + // Content-Length: 13459 + // + // {"kind":"drive#fileList", ...} + + GTLRBatchResponsePart *responsePart = [[GTLRBatchResponsePart alloc] init]; + + // The only header in the actual (outer) MIME multipart headers we want is Content-ID. + // + // The content ID in the response looks like + // + // Content-ID: response-gtlr_5 + // + // but we will strip the "response-" prefix. + NSDictionary *mimeHeaders = mimePart.headers; + NSString *responseContentID = mimeHeaders[@"Content-ID"]; + if ([responseContentID hasPrefix:@"response-"]) { + responseContentID = [responseContentID substringFromIndex:@"response-".length]; + } + responsePart.contentID = responseContentID; + + // Split the body from the inner headers at the first CRLFCRLF. + NSArray <NSNumber *>*offsets; + NSData *mimePartBody = mimePart.body; + [GTMMIMEDocument searchData:mimePartBody + targetBytes:"\r\n\r\n" + targetLength:4 + foundOffsets:&offsets]; + if (offsets.count == 0) { + // Parse error. + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + [userInfo setValue:mimePartBody forKey:kGTLRServiceErrorBodyDataKey]; + [userInfo setValue:responseContentID forKey:kGTLRServiceErrorContentIDKey]; + responsePart.parseError = [NSError errorWithDomain:kGTLRServiceErrorDomain + code:GTLRServiceErrorBatchResponseUnexpected + userInfo:userInfo]; + } else { + // Separate the status/inner headers and the actual body. + NSUInteger partBodyLength = mimePartBody.length; + NSUInteger separatorOffset = offsets[0].unsignedIntegerValue; + NSData *innerHeaderData = + [mimePartBody subdataWithRange:NSMakeRange(0, (NSUInteger)separatorOffset)]; + + NSData *partBodyData; + if (separatorOffset + 4 < partBodyLength) { + NSUInteger offsetToBodyData = separatorOffset + 4; + NSUInteger bodyLength = mimePartBody.length - offsetToBodyData; + partBodyData = [mimePartBody subdataWithRange:NSMakeRange(offsetToBodyData, bodyLength)]; + } + + // Parse to separate the status line and the inner headers (though we don't + // really do much with either.) + [GTMMIMEDocument searchData:innerHeaderData + targetBytes:"\r\n" + targetLength:2 + foundOffsets:&offsets]; + if (offsets.count < 2) { + // Lack of status line and inner headers is strange, but not fatal since + // if the JSON was delivered. + GTLR_DEBUG_LOG(@"GTLRService: Batch result cannot parse headers for request %@:\n%@", + responseContentID, + [[NSString alloc] initWithData:innerHeaderData + encoding:NSUTF8StringEncoding]); + } else { + NSString *statusString; + NSInteger statusCode; + [self getResponseLineFromData:innerHeaderData + statusCode:&statusCode + statusString:&statusString]; + responsePart.statusCode = statusCode; + responsePart.statusString = statusString; + + NSUInteger actualInnerHeaderOffset = offsets[0].unsignedIntegerValue + 2; + NSData *actualInnerHeaderData; + if (innerHeaderData.length - actualInnerHeaderOffset > 0) { + NSRange actualInnerHeaderRange = + NSMakeRange(actualInnerHeaderOffset, + innerHeaderData.length - actualInnerHeaderOffset); + actualInnerHeaderData = [innerHeaderData subdataWithRange:actualInnerHeaderRange]; + } + responsePart.headers = [GTMMIMEDocument headersWithData:actualInnerHeaderData]; + } + + // Create JSON from the body. + NSError *parseError = nil; + NSMutableDictionary *json; + if (partBodyData) { + json = [NSJSONSerialization JSONObjectWithData:partBodyData + options:NSJSONReadingMutableContainers + error:&parseError]; + } else { + parseError = [NSError errorWithDomain:kGTLRServiceErrorDomain + code:GTLRServiceErrorBatchResponseUnexpected + userInfo:nil]; + } + responsePart.JSON = json; + + if (!json) { + // Add our content ID and part body data to the parse error. + NSMutableDictionary *userInfo = + [NSMutableDictionary dictionaryWithDictionary:parseError.userInfo]; + [userInfo setValue:mimePartBody forKey:kGTLRServiceErrorBodyDataKey]; + [userInfo setValue:responseContentID forKey:kGTLRServiceErrorContentIDKey]; + responsePart.parseError = [NSError errorWithDomain:parseError.domain + code:parseError.code + userInfo:userInfo]; + } + } + return responsePart; +} + +- (void)getResponseLineFromData:(NSData *)data + statusCode:(NSInteger *)outStatusCode + statusString:(NSString **)outStatusString { + // Sample response line: + // HTTP/1.1 200 OK + + *outStatusCode = -1; + *outStatusString = @"???"; + NSString *responseLine = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + if (!responseLine) return; + + NSScanner *scanner = [NSScanner scannerWithString:responseLine]; + // Scanner by default skips whitespace when locating the start of the next characters to + // scan. + NSCharacterSet *wsSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; + NSCharacterSet *newlineSet = [NSCharacterSet newlineCharacterSet]; + NSString *httpVersion; + if ([scanner scanUpToCharactersFromSet:wsSet intoString:&httpVersion] + && [scanner scanInteger:outStatusCode] + && [scanner scanUpToCharactersFromSet:newlineSet intoString:outStatusString]) { + // Got it all. + } +} + +- (GTLRBatchResult *)batchResultWithResponseParts:(NSArray <GTLRBatchResponsePart *>*)parts + batchClassMap:(NSDictionary *)batchClassMap + objectClassResolver:(id<GTLRObjectClassResolver>)objectClassResolver { + // Allow the resolver to override the batch rules class also. + Class resultClass = + GTLRObjectResolveClass(objectClassResolver, + [NSDictionary dictionary], + [GTLRBatchResult class]); + GTLRBatchResult *batchResult = [resultClass object]; + + NSMutableDictionary *successes = [NSMutableDictionary dictionary]; + NSMutableDictionary *failures = [NSMutableDictionary dictionary]; + NSMutableDictionary *responseHeaders = [NSMutableDictionary dictionary]; + + for (GTLRBatchResponsePart *responsePart in parts) { + NSString *contentID = responsePart.contentID; + NSDictionary *json = responsePart.JSON; + NSError *parseError = responsePart.parseError; + NSInteger statusCode = responsePart.statusCode; + [responseHeaders setValue:responsePart.headers forKey:contentID]; + + if (parseError) { + GTLRErrorObject *parseErrorObject = [GTLRErrorObject objectWithFoundationError:parseError]; + [failures setValue:parseErrorObject forKey:contentID]; + } else { + // There is JSON. + NSMutableDictionary *errorJSON = [json objectForKey:@"error"]; + if (errorJSON) { + // A JSON error body should be the most informative error. + GTLRErrorObject *errorObject = [GTLRErrorObject objectWithJSON:errorJSON]; + [failures setValue:errorObject forKey:contentID]; + } else if (statusCode < 200 || statusCode > 399) { + // Report a fetch failure for this part that lacks a JSON error. + NSString *errorStr = responsePart.statusString; + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey : (errorStr ?: @"<unknown>"), + }; + NSError *httpError = [NSError errorWithDomain:kGTLRServiceErrorDomain + code:GTLRServiceErrorBatchResponseStatusCode + userInfo:userInfo]; + GTLRErrorObject *httpErrorObject = [GTLRErrorObject objectWithFoundationError:httpError]; + [failures setValue:httpErrorObject forKey:contentID]; + } else { + // The JSON represents a successful response. + Class defaultClass = batchClassMap[contentID]; + id resultObject = [GTLRObject objectForJSON:[json mutableCopy] + defaultClass:defaultClass + objectClassResolver:objectClassResolver]; + if (resultObject == nil) { + // Methods like delete return no object. + resultObject = [NSNull null]; + } + [successes setValue:resultObject forKey:contentID]; + } // errorJSON + } // parseError + } // for + batchResult.successes = successes; + batchResult.failures = failures; + batchResult.responseHeaders = responseHeaders; + return batchResult; +} + +- (void)invokeBatchCompletionsWithTicket:(GTLRServiceTicket *)ticket + batchQuery:(GTLRBatchQuery *)batchQuery + batchResult:(GTLRBatchResult *)batchResult + error:(NSError *)error { + // Batch query + // + // We'll step through the queries of the original batch, not of the + // batch result + GTLR_ASSERT_CURRENT_QUEUE_DEBUG(ticket.callbackQueue); + + NSDictionary *successes = batchResult.successes; + NSDictionary *failures = batchResult.failures; + + for (GTLRQuery *oneQuery in batchQuery.queries) { + GTLRServiceCompletionHandler completionBlock = oneQuery.completionBlock; + if (completionBlock) { + // If there was no networking error, look for a query-specific + // error or result + GTLRObject *oneResult = nil; + NSError *oneError = error; + if (oneError == nil) { + NSString *requestID = [oneQuery requestID]; + GTLRErrorObject *gtlrError = [failures objectForKey:requestID]; + if (gtlrError) { + oneError = [gtlrError foundationError]; + } else { + oneResult = [successes objectForKey:requestID]; + if (oneResult == nil) { + // We found neither a success nor a failure for this query, unexpectedly. + GTLR_DEBUG_LOG(@"GTLRService: Batch result missing for request %@", + requestID); + oneError = [NSError errorWithDomain:kGTLRServiceErrorDomain + code:GTLRServiceErrorQueryResultMissing + userInfo:nil]; + } + } + } + completionBlock(ticket, oneResult, oneError); + } + } +} + +- (void)simulateFetchWithTicket:(GTLRServiceTicket *)ticket + testBlock:(GTLRServiceTestBlock)testBlock + dataToPost:(NSData *)dataToPost + completionHandler:(GTLRServiceCompletionHandler)completionHandler { + + GTLRQuery *originalQuery = (GTLRQuery *)ticket.originalQuery; + ticket.executingQuery = originalQuery; + + testBlock(ticket, ^(id testObject, NSError *testError) { + dispatch_group_async(ticket.callbackGroup, ticket.callbackQueue, ^{ + if (!ticket.cancelled) { + if (testError) { + // During simulation, we invoke any retry block, but ignore the result. + const BOOL willRetry = NO; + GTLRServiceRetryBlock retryBlock = ticket.retryBlock; + if (retryBlock) { + (void)retryBlock(ticket, willRetry, testError); + } + } else { + // Simulate upload progress, calling back up to three times. + if (ticket.uploadProgressBlock) { + GTLRQuery *query = (GTLRQuery *)ticket.originalQuery; + unsigned long long uploadLength = [self simulatedUploadLengthForQuery:query + dataToPost:dataToPost]; + unsigned long long sendReportSize = uploadLength / 3 + 1; + unsigned long long totalSentSoFar = 0; + while (totalSentSoFar < uploadLength) { + unsigned long long bytesRemaining = uploadLength - totalSentSoFar; + sendReportSize = MIN(sendReportSize, bytesRemaining); + totalSentSoFar += sendReportSize; + + [self invokeProgressCallbackForTicket:ticket + deliveredBytes:(unsigned long long)totalSentSoFar + totalBytes:(unsigned long long)uploadLength]; + } + [ticket postNotificationOnMainThreadWithName:kGTLRServiceTicketParsingStartedNotification + object:ticket + userInfo:nil]; + [ticket postNotificationOnMainThreadWithName:kGTLRServiceTicketParsingStoppedNotification + object:ticket + userInfo:nil]; + } + } + + if (![originalQuery isBatchQuery]) { + // Single query + GTLRServiceCompletionHandler completionBlock = originalQuery.completionBlock; + if (completionBlock) { + completionBlock(ticket, testObject, testError); + } + } else { + // Batch query + GTLR_DEBUG_ASSERT(!testObject || [testObject isKindOfClass:[GTLRBatchResult class]], + @"Batch queries should have result objects of type GTLRBatchResult (not %@)", + [testObject class]); + + [self invokeBatchCompletionsWithTicket:ticket + batchQuery:(GTLRBatchQuery *)originalQuery + batchResult:(GTLRBatchResult *)testObject + error:testError]; + } // isBatchQuery + + if (completionHandler) { + completionHandler(ticket, testObject, testError); + } + ticket.hasCalledCallback = YES; + } // !ticket.cancelled + + // Even if the ticket has been cancelled, it should notify that it's stopped. + [ticket notifyStarting:NO]; + + // Release query callback blocks. + [originalQuery invalidateQuery]; + }); // dispatch_group_async + }); // testBlock +} + +- (unsigned long long)simulatedUploadLengthForQuery:(GTLRQuery *)query + dataToPost:(NSData *)dataToPost { + // We're uploading the body object and other posted metadata, plus optionally the + // data or file specified in the upload parameters. + unsigned long long uploadLength = dataToPost.length; + + GTLRUploadParameters *uploadParameters = query.uploadParameters; + if (uploadParameters) { + NSData *uploadData = uploadParameters.data; + if (uploadData) { + uploadLength += uploadData.length; + } else { + NSURL *fileURL = uploadParameters.fileURL; + if (fileURL) { + NSError *fileError = nil; + NSNumber *fileSizeNum = nil; + if ([fileURL getResourceValue:&fileSizeNum + forKey:NSURLFileSizeKey + error:&fileError]) { + uploadLength += fileSizeNum.unsignedLongLongValue; + } + } else { + NSFileHandle *fileHandle = uploadParameters.fileHandle; + unsigned long long fileLength = [fileHandle seekToEndOfFile]; + uploadLength += fileLength; + } + } + } + return uploadLength; +} + +#pragma mark - + +// Given a single or batch query and its result, make a new query +// for the next pages, if any. Returns nil if there's no additional +// query to make. +// +// This method calls itself recursively to make the individual next page +// queries for a batch query. +- (id <GTLRQueryProtocol>)nextPageQueryForQuery:(id<GTLRQueryProtocol>)query + result:(GTLRObject *)object + ticket:(GTLRServiceTicket *)ticket { + if (![query isBatchQuery]) { + // This is a single query + GTLRQuery *currentPageQuery = (GTLRQuery *)query; + + // Determine if we should fetch more pages of results + GTLRQuery *nextPageQuery = nil; + NSString *nextPageToken = nil; + + if ([object respondsToSelector:@selector(nextPageToken)] + && [currentPageQuery respondsToSelector:@selector(pageToken)]) { + nextPageToken = [object performSelector:@selector(nextPageToken)]; + } + + if (nextPageToken && [object isKindOfClass:[GTLRCollectionObject class]]) { + NSString *itemsKey = [[object class] collectionItemsKey]; + GTLR_DEBUG_ASSERT(itemsKey != nil, @"Missing accumulation items key for %@", [object class]); + + SEL itemsSel = NSSelectorFromString(itemsKey); + if ([object respondsToSelector:itemsSel]) { + // Make a query for the next page, preserving the request ID + nextPageQuery = [currentPageQuery copy]; + nextPageQuery.requestID = currentPageQuery.requestID; + + [nextPageQuery performSelector:@selector(setPageToken:) + withObject:nextPageToken]; + } else { + GTLR_DEBUG_ASSERT(0, @"%@ does not implement its collection items property \"%@\"", + [object class], itemsKey); + } + } + return nextPageQuery; + } else { + // This is a batch query + // + // Check if there's a next page to fetch for any of the success + // results by invoking this method recursively on each of those results + GTLRBatchResult *batchResult = (GTLRBatchResult *)object; + GTLRBatchQuery *nextPageBatchQuery = nil; + NSDictionary *successes = batchResult.successes; + + for (NSString *requestID in successes) { + GTLRObject *singleObject = [successes objectForKey:requestID]; + GTLRQuery *singleQuery = [ticket queryForRequestID:requestID]; + + GTLRQuery *newQuery = + (GTLRQuery *)[self nextPageQueryForQuery:singleQuery + result:singleObject + ticket:ticket]; + if (newQuery) { + // There is another query to fetch + if (nextPageBatchQuery == nil) { + nextPageBatchQuery = [GTLRBatchQuery batchQuery]; + } + [nextPageBatchQuery addQuery:newQuery]; + } + } + return nextPageBatchQuery; + } +} + +// When a ticket is set to fetch more pages for feeds, this routine +// initiates the fetch for each additional feed page +// +// Returns YES if fetching of the next page has started. +- (BOOL)fetchNextPageWithQuery:(GTLRQuery *)query + completionHandler:(GTLRServiceCompletionHandler)handler + ticket:(GTLRServiceTicket *)ticket { + // Sanity check the number of pages fetched already + if (ticket.pagesFetchedCounter > kMaxNumberOfNextPagesFetched) { + // Sanity check failed: way too many pages were fetched, so the query's + // page size should be bigger to avoid driving up networking and server + // overhead. + // + // The client should be querying with a higher max results per page + // to avoid this. + GTLR_DEBUG_ASSERT(0, @"Fetched too many next pages executing %@;" + @" increase maxResults page size to avoid this.", + [query class]); + return NO; + } + + GTLRServiceTicket *newTicket; + if ([query isBatchQuery]) { + newTicket = [self executeBatchQuery:(GTLRBatchQuery *)query + completionHandler:handler + ticket:ticket]; + } else { + BOOL mayAuthorize = !query.shouldSkipAuthorization; + NSURL *url = [self URLFromQueryObject:query + usePartialPaths:NO + includeServiceURLQueryParams:YES]; + newTicket = [self fetchObjectWithURL:url + objectClass:query.expectedObjectClass + bodyObject:query.bodyObject + ETag:nil + httpMethod:query.httpMethod + mayAuthorize:mayAuthorize + completionHandler:handler + executingQuery:query + ticket:ticket]; + } + + // In the bizarre case that the fetch didn't begin, newTicket will be + // nil. So long as the new ticket is the same as the ticket we're + // continuing, then we're happy. + GTLR_ASSERT(newTicket == ticket || newTicket == nil, + @"Pagination should not create an additional ticket: %@", newTicket); + + BOOL isFetchingNextPageWithCurrentTicket = (newTicket == ticket); + return isFetchingNextPageWithCurrentTicket; +} + +// Given a new single or batch result (meaning additional pages for a previous +// query result), merge it into the old result, and return the updated object. +// +// For a single result, this inserts the old result items into the new result. +// For batch results, this replaces some of the old items with new items. +// +// This method changes the objects passed in (the old result for batches, the new result +// for individual objects.) +- (GTLRObject *)mergedNewResultObject:(GTLRObject *)newResult + oldResultObject:(GTLRObject *)oldResult + forQuery:(id<GTLRQueryProtocol>)query + ticket:(GTLRServiceTicket *)ticket { + GTLR_DEBUG_ASSERT([oldResult isMemberOfClass:[newResult class]], + @"Trying to merge %@ and %@", [oldResult class], [newResult class]); + + if ([query isBatchQuery]) { + // Batch query result + // + // The new batch results are a subset of the old result's queries, since + // not all queries in the batch necessarily have additional pages. + // + // New success objects replace old success objects, with the old items + // prepended; new failure objects replace old success objects. + // We will update the old batch results with accumulated items, using the + // new objects, and return the old batch. + // + // We reuse the old batch results object because it may include some earlier + // results which did not have additional pages. + GTLRBatchResult *newBatchResult = (GTLRBatchResult *)newResult; + GTLRBatchResult *oldBatchResult = (GTLRBatchResult *)oldResult; + + NSDictionary *newSuccesses = newBatchResult.successes; + if (newSuccesses.count > 0) { + NSDictionary *oldSuccesses = oldBatchResult.successes; + NSMutableDictionary *mutableOldSuccesses = [oldSuccesses mutableCopy]; + + for (NSString *requestID in newSuccesses) { + GTLRObject *newObj = [newSuccesses objectForKey:requestID]; + GTLRObject *oldObj = [oldSuccesses objectForKey:requestID]; + + GTLRQuery *thisQuery = [ticket queryForRequestID:requestID]; + + // Recursively merge the single query's result object, appending new items to the old items. + GTLRObject *updatedObj = [self mergedNewResultObject:newObj + oldResultObject:oldObj + forQuery:thisQuery + ticket:ticket]; + + // In the old batch, replace the old result object with the new one. + [mutableOldSuccesses setObject:updatedObj forKey:requestID]; + } // for requestID + oldBatchResult.successes = mutableOldSuccesses; + } // newSuccesses.count > 0 + + NSDictionary *newFailures = newBatchResult.failures; + if (newFailures.count > 0) { + NSMutableDictionary *mutableOldSuccesses = [oldBatchResult.successes mutableCopy]; + NSMutableDictionary *mutableOldFailures = [oldBatchResult.failures mutableCopy]; + for (NSString *requestID in newFailures) { + // In the old batch, replace old successes or failures with the new failure. + GTLRErrorObject *newError = [newFailures objectForKey:requestID]; + [mutableOldFailures setObject:newError forKey:requestID]; + + [mutableOldSuccesses removeObjectForKey:requestID]; + } + oldBatchResult.failures = mutableOldFailures; + oldBatchResult.successes = mutableOldSuccesses; + } // newFailures.count > 0 + return oldBatchResult; + } else { + // Single query result + // + // Merge the items into the new object, and return the new object. + NSString *itemsKey = [[oldResult class] collectionItemsKey]; + + GTLR_DEBUG_ASSERT([oldResult respondsToSelector:NSSelectorFromString(itemsKey)], + @"Collection items key \"%@\" not implemented by %@", itemsKey, oldResult); + if (itemsKey) { + // Append the new items to the old items. + NSArray *oldItems = [oldResult valueForKey:itemsKey]; + NSArray *newItems = [newResult valueForKey:itemsKey]; + NSMutableArray *items = [NSMutableArray arrayWithArray:oldItems]; + [items addObjectsFromArray:newItems]; + [newResult setValue:items forKey:itemsKey]; + } else { + // This shouldn't happen. + newResult = oldResult; + } + return newResult; + } +} + +#pragma mark - + +// GTLRQuery methods. + +// Helper to create the URL from the parts. +- (NSURL *)URLFromQueryObject:(GTLRQuery *)query + usePartialPaths:(BOOL)usePartialPaths + includeServiceURLQueryParams:(BOOL)includeServiceURLQueryParams { + NSString *rootURLString = self.rootURLString; + + // Skip URI template expansion if the resource URL was provided. + if ([query isKindOfClass:[GTLRResourceURLQuery class]]) { + // Because the query is created by the service rather than by the user, + // query.additionalURLQueryParameters must be nil, and usePartialPaths + // is irrelevant as the query is not in a batch. + GTLR_DEBUG_ASSERT(!usePartialPaths, + @"Batch not supported with resource URL fetch"); + GTLR_DEBUG_ASSERT(!query.uploadParameters && !query.useMediaDownloadService + && !query.downloadAsDataObjectType && !query.additionalURLQueryParameters, + @"Unsupported query properties"); + NSURL *result = ((GTLRResourceURLQuery *)query).resourceURL; + if (includeServiceURLQueryParams) { + NSDictionary *additionalParams = self.additionalURLQueryParameters; + if (additionalParams.count) { + result = [GTLRService URLWithString:result.absoluteString + queryParameters:additionalParams]; + } + } + return result; + } + + // This is all the dance needed due to having query and path parameters for + // REST based queries. + NSDictionary *params = query.JSON; + NSString *queryFilledPathURI = [GTLRURITemplate expandTemplate:query.pathURITemplate + values:params]; + + // Per https://developers.google.com/discovery/v1/using#build-compose and + // https://developers.google.com/discovery/v1/using#discovery-doc-methods-mediadownload + // glue together the parts. + NSString *servicePath = self.servicePath ?: @""; + NSString *uploadPath = @""; + NSString *downloadPath = @""; + + GTLR_DEBUG_ASSERT([rootURLString hasSuffix:@"/"], + @"rootURLString should end in a slash: %@", rootURLString); + GTLR_DEBUG_ASSERT(((servicePath.length == 0) || + (![servicePath hasPrefix:@"/"] && [servicePath hasSuffix:@"/"])), + @"servicePath shouldn't start with a slash but should end with one: %@", + servicePath); + GTLR_DEBUG_ASSERT(![query.pathURITemplate hasPrefix:@"/"], + @"the queries's pathURITemplate should not start with a slash: %@", + query.pathURITemplate); + + GTLRUploadParameters *uploadParameters = query.uploadParameters; + if (uploadParameters != nil) { + // If there is an override, clear all the parts and just use it with the + // the rootURLString. + NSString *override = (uploadParameters.shouldUploadWithSingleRequest + ? query.simpleUploadPathURITemplateOverride + : query.resumableUploadPathURITemplateOverride); + if (override.length > 0) { + GTLR_DEBUG_ASSERT(![override hasPrefix:@"/"], + @"The query's %@UploadPathURITemplateOverride should not start with a slash: %@", + (uploadParameters.shouldUploadWithSingleRequest ? @"simple" : @"resumable"), + override); + queryFilledPathURI = [GTLRURITemplate expandTemplate:override + values:params]; + servicePath = @""; + } else { + if (uploadParameters.shouldUploadWithSingleRequest) { + uploadPath = self.simpleUploadPath ?: @""; + } else { + uploadPath = self.resumableUploadPath ?: @""; + } + GTLR_DEBUG_ASSERT(((uploadPath.length == 0) || + (![uploadPath hasPrefix:@"/"] && + [uploadPath hasSuffix:@"/"])), + @"%@UploadPath shouldn't start with a slash but should end with one: %@", + (uploadParameters.shouldUploadWithSingleRequest ? @"simple" : @"resumable"), + uploadPath); + } + } + + if (query.useMediaDownloadService && + (query.downloadAsDataObjectType.length > 0)) { + downloadPath = @"download/"; + GTLR_DEBUG_ASSERT(uploadPath.length == 0, + @"Uploading while also downloading via mediaDownService" + @" is not well defined."); + } + + if (usePartialPaths) rootURLString = @"/"; + + NSString *urlString = + [NSString stringWithFormat:@"%@%@%@%@%@", + rootURLString, downloadPath, uploadPath, servicePath, queryFilledPathURI]; + + // Remove the path parameters from the dictionary. + NSMutableDictionary *workingQueryParams = [NSMutableDictionary dictionaryWithDictionary:params]; + + NSArray *pathParameterNames = query.pathParameterNames; + if (pathParameterNames.count > 0) { + [workingQueryParams removeObjectsForKeys:pathParameterNames]; + } + + // Note: A developer can override the uploadType and alt query parameters via + // query.additionalURLQueryParameters since those are added afterwards. + if (uploadParameters.shouldUploadWithSingleRequest) { + NSString *uploadType = uploadParameters.shouldSendUploadOnly ? @"media" : @"multipart"; + [workingQueryParams setObject:uploadType forKey:@"uploadType"]; + } + NSString *downloadAsDataObjectType = query.downloadAsDataObjectType; + if (downloadAsDataObjectType.length > 0) { + [workingQueryParams setObject:downloadAsDataObjectType + forKey:@"alt"]; + } + + // Add any parameters the user added directly to the query. + NSDictionary *mergedParams = MergeDictionaries(workingQueryParams, + query.additionalURLQueryParameters); + if (includeServiceURLQueryParams) { + // Query parameters override service parameters. + mergedParams = MergeDictionaries(self.additionalURLQueryParameters, mergedParams); + } + + NSURL *result = [GTLRService URLWithString:urlString + queryParameters:mergedParams]; + return result; +} + +- (GTLRServiceTicket *)executeQuery:(id<GTLRQueryProtocol>)queryObj + delegate:(id)delegate + didFinishSelector:(SEL)finishedSelector { + GTMSessionFetcherAssertValidSelector(delegate, finishedSelector, + @encode(GTLRServiceTicket *), @encode(GTLRObject *), @encode(NSError *), 0); + GTLRServiceCompletionHandler completionHandler = ^(GTLRServiceTicket *ticket, + id object, + NSError *error) { + if (delegate && finishedSelector) { + NSMethodSignature *sig = [delegate methodSignatureForSelector:finishedSelector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:(SEL)finishedSelector]; + [invocation setTarget:delegate]; + [invocation setArgument:&ticket atIndex:2]; + [invocation setArgument:&object atIndex:3]; + [invocation setArgument:&error atIndex:4]; + [invocation invoke]; + } + }; + return [self executeQuery:queryObj completionHandler:completionHandler]; +} + +- (GTLRServiceTicket *)executeQuery:(id<GTLRQueryProtocol>)queryObj + completionHandler:(void (^)(GTLRServiceTicket *ticket, id object, + NSError *error))handler { + if ([queryObj isBatchQuery]) { + GTLR_DEBUG_ASSERT([queryObj isKindOfClass:[GTLRBatchQuery class]], + @"GTLRBatchQuery required for batches (passed %@)", + [queryObj class]); + return [self executeBatchQuery:(GTLRBatchQuery *)queryObj + completionHandler:handler + ticket:nil]; + } + GTLR_DEBUG_ASSERT([queryObj isKindOfClass:[GTLRQuery class]], + @"GTLRQuery required for single queries (passed %@)", + [queryObj class]); + + // Copy the original query so our working query cannot be modified by the caller, + // and release the callback blocks from the supplied query object. + GTLRQuery *query = [(GTLRQuery *)queryObj copy]; + + GTLR_DEBUG_ASSERT(!query.queryInvalid, @"Query has already been executed: %@", query); + [queryObj invalidateQuery]; + + // For individual queries, we rely on the fetcher's log formatting so pretty-printing + // is not needed. Developers may override this in the query's additionalURLQueryParameters. + NSArray *prettyPrintNames = self.prettyPrintQueryParameterNames; + NSString *firstPrettyPrintName = prettyPrintNames.firstObject; + if (firstPrettyPrintName && (query.downloadAsDataObjectType.length == 0) + && ![query isKindOfClass:[GTLRResourceURLQuery class]]) { + NSDictionary *queryParams = query.additionalURLQueryParameters; + BOOL foundOne = NO; + for (NSString *name in prettyPrintNames) { + if ([queryParams objectForKey:name] != nil) { + foundOne = YES; + break; + } + } + if (!foundOne) { + NSMutableDictionary *worker = + [NSMutableDictionary dictionaryWithDictionary:queryParams]; + [worker setObject:@"false" forKey:firstPrettyPrintName]; + query.additionalURLQueryParameters = worker; + } + } + + BOOL mayAuthorize = !query.shouldSkipAuthorization; + NSURL *url = [self URLFromQueryObject:query + usePartialPaths:NO + includeServiceURLQueryParams:YES]; + + return [self fetchObjectWithURL:url + objectClass:query.expectedObjectClass + bodyObject:query.bodyObject + ETag:nil + httpMethod:query.httpMethod + mayAuthorize:mayAuthorize + completionHandler:handler + executingQuery:query + ticket:nil]; +} + +- (GTLRServiceTicket *)fetchObjectWithURL:(NSURL *)resourceURL + objectClass:(nullable Class)objectClass + executionParameters:(nullable GTLRServiceExecutionParameters *)executionParameters + completionHandler:(nullable GTLRServiceCompletionHandler)handler { + GTLRResourceURLQuery *query = [GTLRResourceURLQuery queryWithResourceURL:resourceURL + objectClass:objectClass]; + query.executionParameters = executionParameters; + + return [self executeQuery:query + completionHandler:handler]; +} + +#pragma mark - + +- (NSString *)userAgent { + return _userAgent; +} + +- (void)setExactUserAgent:(NSString *)userAgent { + _userAgent = [userAgent copy]; +} + +- (void)setUserAgent:(NSString *)userAgent { + // remove whitespace and unfriendly characters + NSString *str = GTMFetcherCleanedUserAgentString(userAgent); + [self setExactUserAgent:str]; +} + +- (void)overrideRequestUserAgent:(nullable NSString *)requestUserAgent { + _overrideUserAgent = [requestUserAgent copy]; +} + +#pragma mark - + ++ (NSDictionary<NSString *, Class> *)kindStringToClassMap { + // Generated services will provide custom ones. + return [NSDictionary dictionary]; +} + +#pragma mark - + +// The service properties becomes the initial value for each future ticket's +// properties +- (void)setServiceProperties:(NSDictionary *)dict { + _serviceProperties = [dict copy]; +} + +- (NSDictionary *)serviceProperties { + // be sure the returned pointer has the life of the autorelease pool, + // in case self is released immediately + __autoreleasing id props = _serviceProperties; + return props; +} + +- (void)setAuthorizer:(id <GTMFetcherAuthorizationProtocol>)authorizer { + self.fetcherService.authorizer = authorizer; +} + +- (id <GTMFetcherAuthorizationProtocol>)authorizer { + return self.fetcherService.authorizer; +} + ++ (NSUInteger)defaultServiceUploadChunkSize { + // Subclasses may override this method. + + // The upload server prefers multiples of 256K. + const NSUInteger kMegabyte = 4 * 256 * 1024; + +#if TARGET_OS_IPHONE + // For iOS, we're balancing a large upload size with limiting the memory + // used for the upload data buffer. + return 4 * kMegabyte; +#else + // A large upload chunk size minimizes http overhead and server effort. + return 25 * kMegabyte; +#endif +} + +- (NSUInteger)serviceUploadChunkSize { + if (_uploadChunkSize > 0) { + return _uploadChunkSize; + } + return [[self class] defaultServiceUploadChunkSize]; +} + +- (void)setServiceUploadChunkSize:(NSUInteger)val { + _uploadChunkSize = val; +} + +- (void)setSurrogates:(NSDictionary <Class, Class>*)surrogates { + NSDictionary *kindMap = [[self class] kindStringToClassMap]; + + self.objectClassResolver = [GTLRObjectClassResolver resolverWithKindMap:kindMap + surrogates:surrogates]; +} + +#pragma mark - Internal helper + +// If there are already query parameters on urlString, the new ones are simply +// appended after them. ++ (NSURL *)URLWithString:(NSString *)urlString + queryParameters:(NSDictionary *)queryParameters { + if (urlString.length == 0) return nil; + + NSString *fullURLString; + if (queryParameters.count > 0) { + // Use GTLRURITemplate by building up a template and then feeding in the + // values. The template is query expansion ('?'), and any key that is + // an array or dictionary gets tagged to explode them ('+'). + NSArray *sortedQueryParamKeys = + [queryParameters.allKeys sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; + + NSMutableString *template = [@"{" mutableCopy]; + char joiner = '?'; + for (NSString *key in sortedQueryParamKeys) { + [template appendFormat:@"%c%@", joiner, key]; + id value = [queryParameters objectForKey:key]; + if ([value isKindOfClass:[NSArray class]] || + [value isKindOfClass:[NSDictionary class]]) { + [template appendString:@"+"]; + } + joiner = ','; + } + [template appendString:@"}"]; + NSString *urlArgs = + [GTLRURITemplate expandTemplate:template + values:queryParameters]; + urlArgs = [urlArgs substringFromIndex:1]; // Drop the '?' and use the joiner. + + BOOL missingQMark = ([urlString rangeOfString:@"?"].location == NSNotFound); + joiner = missingQMark ? '?' : '&'; + fullURLString = + [NSString stringWithFormat:@"%@%c%@", urlString, joiner, urlArgs]; + } else { + fullURLString = urlString; + } + NSURL *result = [NSURL URLWithString:fullURLString]; + return result; +} + +@end + +@implementation GTLRService (TestingSupport) + ++ (instancetype)mockServiceWithFakedObject:(id)objectOrNil + fakedError:(NSError *)errorOrNil { + GTLRService *service = [[GTLRService alloc] init]; + service.rootURLString = @"https://example.invalid/"; + service.testBlock = ^(GTLRServiceTicket *ticket, GTLRServiceTestResponse testResponse) { + testResponse(objectOrNil, errorOrNil); + }; + return service; +} + +- (BOOL)waitForTicket:(GTLRServiceTicket *)ticket + timeout:(NSTimeInterval)timeoutInSeconds { + // Loop until the fetch completes or is cancelled, or until the timeout has expired. + NSDate *giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds]; + + BOOL hasTimedOut = NO; + while (1) { + int64_t delta = (int64_t)(100 * NSEC_PER_MSEC); // 100 ms + BOOL areCallbacksPending = + (dispatch_group_wait(ticket.callbackGroup, dispatch_time(DISPATCH_TIME_NOW, delta)) != 0); + + if (!areCallbacksPending && (ticket.hasCalledCallback || ticket.cancelled)) break; + + hasTimedOut = (giveUpDate.timeIntervalSinceNow <= 0); + if (hasTimedOut) { + if (areCallbacksPending) { + // A timeout while waiting for the dispatch group to finish is seriously unexpected. + GTLR_DEBUG_LOG(@"%s timed out while waiting for the dispatch group", __PRETTY_FUNCTION__); + } else { + GTLR_DEBUG_LOG(@"%s timed out without callbacks pending", __PRETTY_FUNCTION__); + } + break; + } + + // Run the current run loop 1/1000 of a second to give the networking + // code a chance to work. + NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:0.001]; + [[NSRunLoop currentRunLoop] runUntilDate:stopDate]; + } + return !hasTimedOut; +} + +@end + +@implementation GTLRServiceTicket { + GTLRService *_service; + NSDictionary *_ticketProperties; + GTLRServiceUploadProgressBlock _uploadProgressBlock; + BOOL _needsStopNotification; +} + +@synthesize APIKey = _apiKey, + APIKeyRestrictionBundleID = _apiKeyRestrictionBundleID, + allowInsecureQueries = _allowInsecureQueries, + authorizer = _authorizer, + cancelled = _cancelled, + callbackGroup = _callbackGroup, + callbackQueue = _callbackQueue, + creationDate = _creationDate, + executingQuery = _executingQuery, + fetchedObject = _fetchedObject, + fetchError = _fetchError, + fetchRequest = _fetchRequest, + fetcherService = _fetcherService, + hasCalledCallback = _hasCalledCallback, + maxRetryInterval = _maxRetryInterval, + objectFetcher = _objectFetcher, + originalQuery = _originalQuery, + pagesFetchedCounter = _pagesFetchedCounter, + postedObject = _postedObject, + retryBlock = _retryBlock, + retryEnabled = _retryEnabled, + shouldFetchNextPages = _shouldFetchNextPages, + objectClassResolver = _objectClassResolver, + testBlock = _testBlock; + +#if GTM_BACKGROUND_TASK_FETCHING +@synthesize backgroundTaskIdentifier = _backgroundTaskIdentifier; +#endif + +#if DEBUG +- (instancetype)init { + [self doesNotRecognizeSelector:_cmd]; + self = nil; + return self; +} +#endif + +#if GTM_BACKGROUND_TASK_FETCHING && DEBUG +- (void)dealloc { + GTLR_DEBUG_ASSERT(_backgroundTaskIdentifier == UIBackgroundTaskInvalid, + @"Background task not ended"); +} +#endif // GTM_BACKGROUND_TASK_FETCHING && DEBUG + + +- (instancetype)initWithService:(GTLRService *)service + executionParameters:(GTLRServiceExecutionParameters *)params { + self = [super init]; + if (self) { + // ivars set at init time and never changed are exposed as atomic readonly properties. + _service = service; + _fetcherService = service.fetcherService; + _authorizer = service.authorizer; + + _ticketProperties = MergeDictionaries(service.serviceProperties, params.ticketProperties); + + _objectClassResolver = params.objectClassResolver ?: service.objectClassResolver; + + _retryEnabled = ((params.retryEnabled != nil) ? params.retryEnabled.boolValue : service.retryEnabled); + _maxRetryInterval = ((params.maxRetryInterval != nil) ? + params.maxRetryInterval.doubleValue : service.maxRetryInterval); + _shouldFetchNextPages = ((params.shouldFetchNextPages != nil)? + params.shouldFetchNextPages.boolValue : service.shouldFetchNextPages); + + GTLRServiceUploadProgressBlock uploadProgressBlock = + params.uploadProgressBlock ?: service.uploadProgressBlock; + _uploadProgressBlock = [uploadProgressBlock copy]; + + GTLRServiceRetryBlock retryBlock = params.retryBlock ?: service.retryBlock; + _retryBlock = [retryBlock copy]; + if (_retryBlock) { + _retryEnabled = YES; + } + + _testBlock = params.testBlock ?: service.testBlock; + + _callbackQueue = ((_Nonnull dispatch_queue_t)params.callbackQueue) ?: service.callbackQueue; + _callbackGroup = dispatch_group_create(); + + _apiKey = [service.APIKey copy]; + _apiKeyRestrictionBundleID = [service.APIKeyRestrictionBundleID copy]; + _allowInsecureQueries = service.allowInsecureQueries; + +#if GTM_BACKGROUND_TASK_FETCHING + _backgroundTaskIdentifier = UIBackgroundTaskInvalid; +#endif + + _creationDate = [NSDate date]; + } + return self; +} + +- (NSString *)description { + NSString *devKeyInfo = @""; + if (_apiKey != nil) { + devKeyInfo = [NSString stringWithFormat:@" devKey:%@", _apiKey]; + } + NSString *keyRestrictionInfo = @""; + if (_apiKeyRestrictionBundleID != nil) { + keyRestrictionInfo = [NSString stringWithFormat:@" restriction:%@", + _apiKeyRestrictionBundleID]; + } + + NSString *authorizerInfo = @""; + id <GTMFetcherAuthorizationProtocol> authorizer = self.objectFetcher.authorizer; + if (authorizer != nil) { + authorizerInfo = [NSString stringWithFormat:@" authorizer:%@", authorizer]; + } + + return [NSString stringWithFormat:@"%@ %p: {service:%@%@%@%@ fetcher:%@ }", + [self class], self, + _service, devKeyInfo, keyRestrictionInfo, authorizerInfo, _objectFetcher]; +} + +- (void)postNotificationOnMainThreadWithName:(NSString *)name + object:(id)object + userInfo:(NSDictionary *)userInfo { + // We always post these async to ensure they remain in order. + dispatch_group_async(self.callbackGroup, dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:name + object:object + userInfo:userInfo]; + }); +} + +- (void)pauseUpload { + GTMSessionFetcher *fetcher = self.objectFetcher; + BOOL canPause = [fetcher respondsToSelector:@selector(pauseFetching)]; + GTLR_DEBUG_ASSERT(canPause, @"tickets can be paused only for chunked resumable uploads"); + + if (canPause) { + [(GTMSessionUploadFetcher *)fetcher pauseFetching]; + } +} + +- (void)resumeUpload { + GTMSessionFetcher *fetcher = self.objectFetcher; + BOOL canResume = [fetcher respondsToSelector:@selector(resumeFetching)]; + GTLR_DEBUG_ASSERT(canResume, @"tickets can be resumed only for chunked resumable uploads"); + + if (canResume) { + [(GTMSessionUploadFetcher *)fetcher resumeFetching]; + } +} + +- (BOOL)isUploadPaused { + BOOL isPausable = [_objectFetcher respondsToSelector:@selector(isPaused)]; + GTLR_DEBUG_ASSERT(isPausable, @"tickets can be paused only for chunked resumable uploads"); + + if (isPausable) { + return [(GTMSessionUploadFetcher *)_objectFetcher isPaused]; + } + return NO; +} + +- (BOOL)isCancelled { + @synchronized(self) { + return _cancelled; + } +} + +- (void)cancelTicket { + @synchronized(self) { + _cancelled = YES; + } + + [_objectFetcher stopFetching]; + + self.objectFetcher = nil; + self.fetchRequest = nil; + _ticketProperties = nil; + + [self releaseTicketCallbacks]; + [self endBackgroundTask]; + + [self.executingQuery invalidateQuery]; + + id<GTLRQueryProtocol> originalQuery = self.originalQuery; + self.executingQuery = originalQuery; + [originalQuery invalidateQuery]; + + _service = nil; + _fetcherService = nil; + _authorizer = nil; + _testBlock = nil; +} + +#if GTM_BACKGROUND_TASK_FETCHING +// When the fetcher's substitute UIApplication object is present, GTLRService +// will use that instead of UIApplication. This is just to reduce duplicating +// that plumbing for testing. ++ (nullable id<GTMUIApplicationProtocol>)fetcherUIApplication { + id<GTMUIApplicationProtocol> app = [GTMSessionFetcher substituteUIApplication]; + if (app) return app; + + static Class applicationClass = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + BOOL isAppExtension = [[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"]; + if (!isAppExtension) { + Class cls = NSClassFromString(@"UIApplication"); + if (cls && [cls respondsToSelector:NSSelectorFromString(@"sharedApplication")]) { + applicationClass = cls; + } + } + }); + + if (applicationClass) { + app = (id<GTMUIApplicationProtocol>)[applicationClass sharedApplication]; + } + return app; +} +#endif // GTM_BACKGROUND_TASK_FETCHING + +- (void)startBackgroundTask { +#if GTM_BACKGROUND_TASK_FETCHING + GTLR_DEBUG_ASSERT(self.backgroundTaskIdentifier == UIBackgroundTaskInvalid, + @"Redundant GTLRService background task: %tu", self.backgroundTaskIdentifier); + + NSString *taskName = [[self.executingQuery class] description]; + + id<GTMUIApplicationProtocol> app = [[self class] fetcherUIApplication]; + + // We'll use a locally-scoped task ID variable so the expiration block is guaranteed + // to refer to this task rather than to whatever task the property has. + __block UIBackgroundTaskIdentifier bgTaskID = + [app beginBackgroundTaskWithName:taskName + expirationHandler:^{ + // Background task expiration callback. This block is always invoked by + // UIApplication on the main thread. + if (bgTaskID != UIBackgroundTaskInvalid) { + @synchronized(self) { + if (bgTaskID == self.backgroundTaskIdentifier) { + self.backgroundTaskIdentifier = UIBackgroundTaskInvalid; + } + } + // This explicitly ends the captured bgTaskID rather than the backgroundTaskIdentifier + // property to ensure expiration is handled even if the property has changed. + [app endBackgroundTask:bgTaskID]; + } + }]; + @synchronized(self) { + self.backgroundTaskIdentifier = bgTaskID; + } +#endif // GTM_BACKGROUND_TASK_FETCHING +} + +- (void)endBackgroundTask { +#if GTM_BACKGROUND_TASK_FETCHING + // Whenever the connection stops or a next page is about to be fetched, + // tell UIApplication we're done. + UIBackgroundTaskIdentifier bgTaskID; + @synchronized(self) { + bgTaskID = self.backgroundTaskIdentifier; + self.backgroundTaskIdentifier = UIBackgroundTaskInvalid; + } + if (bgTaskID != UIBackgroundTaskInvalid) { + [[[self class] fetcherUIApplication] endBackgroundTask:bgTaskID]; + } +#endif // GTM_BACKGROUND_TASK_FETCHING +} + +- (void)releaseTicketCallbacks { + self.uploadProgressBlock = nil; + self.retryBlock = nil; +} + +- (void)notifyStarting:(BOOL)isStarting { + GTLR_DEBUG_ASSERT(!GTLR_AreBoolsEqual(isStarting, _needsStopNotification), + @"Notification mismatch (isStarting=%d)", isStarting); + if (GTLR_AreBoolsEqual(isStarting, _needsStopNotification)) return; + + NSString *name; + if (isStarting) { + name = kGTLRServiceTicketStartedNotification; + _needsStopNotification = YES; + } else { + name = kGTLRServiceTicketStoppedNotification; + _needsStopNotification = NO; + } + [self postNotificationOnMainThreadWithName:name + object:self + userInfo:nil]; +} + +- (id)service { + return _service; +} + +- (void)setObjectFetcher:(GTMSessionFetcher *)fetcher { + @synchronized(self) { + _objectFetcher = fetcher; + } + + [self updateObjectFetcherProgressCallbacks]; +} + +- (GTMSessionFetcher *)objectFetcher { + @synchronized(self) { + return _objectFetcher; + } +} + +- (NSDictionary *)ticketProperties { + // be sure the returned pointer has the life of the autorelease pool, + // in case self is released immediately + __autoreleasing id props = _ticketProperties; + return props; +} + +- (GTLRServiceUploadProgressBlock)uploadProgressBlock { + return _uploadProgressBlock; +} + +- (void)setUploadProgressBlock:(GTLRServiceUploadProgressBlock)block { + if (_uploadProgressBlock != block) { + _uploadProgressBlock = [block copy]; + + [self updateObjectFetcherProgressCallbacks]; + } +} + +- (void)updateObjectFetcherProgressCallbacks { + // Internal method. Do not override. + GTMSessionFetcher *fetcher = [self objectFetcher]; + + if (_uploadProgressBlock) { + // Use a local block variable to avoid a spurious retain cycle warning. + GTMSessionFetcherSendProgressBlock fetcherSentDataBlock = ^(int64_t bytesSent, + int64_t totalBytesSent, + int64_t totalBytesExpectedToSend) { + [self->_service invokeProgressCallbackForTicket:self + deliveredBytes:(unsigned long long)totalBytesSent + totalBytes:(unsigned long long)totalBytesExpectedToSend]; + }; + + fetcher.sendProgressBlock = fetcherSentDataBlock; + } else { + fetcher.sendProgressBlock = nil; + } +} + +- (NSInteger)statusCode { + return [_objectFetcher statusCode]; +} + +- (GTLRQuery *)queryForRequestID:(NSString *)requestID { + id<GTLRQueryProtocol> queryObj = self.executingQuery; + if ([queryObj isBatchQuery]) { + GTLRBatchQuery *batch = (GTLRBatchQuery *)queryObj; + GTLRQuery *result = [batch queryForRequestID:requestID]; + return result; + } else { + GTLR_DEBUG_ASSERT(0, @"just use ticket.executingQuery"); + return nil; + } +} + +@end + +@implementation GTLRServiceExecutionParameters + +@synthesize maxRetryInterval = _maxRetryInterval, + retryEnabled = _retryEnabled, + retryBlock = _retryBlock, + shouldFetchNextPages = _shouldFetchNextPages, + objectClassResolver = _objectClassResolver, + testBlock = _testBlock, + ticketProperties = _ticketProperties, + uploadProgressBlock = _uploadProgressBlock, + callbackQueue = _callbackQueue; + +- (id)copyWithZone:(NSZone *)zone { + GTLRServiceExecutionParameters *newObject = [[self class] allocWithZone:zone]; + newObject.maxRetryInterval = self.maxRetryInterval; + newObject.retryEnabled = self.retryEnabled; + newObject.retryBlock = self.retryBlock; + newObject.shouldFetchNextPages = self.shouldFetchNextPages; + newObject.objectClassResolver = self.objectClassResolver; + newObject.testBlock = self.testBlock; + newObject.ticketProperties = self.ticketProperties; + newObject.uploadProgressBlock = self.uploadProgressBlock; + newObject.callbackQueue = self.callbackQueue; + return newObject; +} + +- (BOOL)hasParameters { + if (self.maxRetryInterval != nil) return YES; + if (self.retryEnabled != nil) return YES; + if (self.retryBlock) return YES; + if (self.shouldFetchNextPages != nil) return YES; + if (self.objectClassResolver) return YES; + if (self.testBlock) return YES; + if (self.ticketProperties) return YES; + if (self.uploadProgressBlock) return YES; + if (self.callbackQueue) return YES; + return NO; +} + +@end + + +@implementation GTLRResourceURLQuery + +@synthesize resourceURL = _resourceURL; + ++ (instancetype)queryWithResourceURL:(NSURL *)resourceURL + objectClass:(Class)objectClass { + GTLRResourceURLQuery *query = [[self alloc] initWithPathURITemplate:@"_usingGTLRResourceURLQuery_" + HTTPMethod:nil + pathParameterNames:nil]; + query.expectedObjectClass = objectClass; + query.resourceURL = resourceURL; + return query; +} + +- (instancetype)copyWithZone:(NSZone *)zone { + GTLRResourceURLQuery *result = [super copyWithZone:zone]; + result->_resourceURL = self->_resourceURL; + return result; +} + +// TODO: description + +@end + +@implementation GTLRObjectCollectionImpl +@dynamic nextPageToken; +@end diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRUploadParameters.h b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRUploadParameters.h @@ -0,0 +1,124 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Uploading documentation: +// https://github.com/google/google-api-objectivec-client-for-rest/wiki#uploading-files + +#import <Foundation/Foundation.h> + +#import "GTLRDefines.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Upload parameters are required for chunked-resumable or simple/multipart uploads. + * + * The MIME type and one source for data (@c NSData, file URL, or @c NSFileHandle) must + * be specified. + */ +@interface GTLRUploadParameters : NSObject <NSCopying> + +/** + * The type of media being uploaded. + */ +@property(atomic, copy, nullable) NSString *MIMEType; + +/** + * The media to be uploaded, represented as @c NSData. + */ +@property(atomic, retain, nullable) NSData *data; + +/** + * The URL for the local file to be uploaded. + */ +@property(atomic, retain, nullable) NSURL *fileURL; + +/** + * The media to be uploaded, represented as @c NSFileHandle. + * + * @note This property is provided for compatibility with older code. + * Uploading using @c fileURL is preferred over @c fileHandle + */ +@property(atomic, retain, nullable) NSFileHandle *fileHandle; + +/** + * Resuming an in-progress resumable, chunked upload is done with the upload location URL, + * and requires a file URL or file handle for uploading. + */ +@property(atomic, retain, nullable) NSURL *uploadLocationURL; + +/** + * Small uploads (for example, under 200K) can be done with a single multipart upload + * request. The upload body must be provided as NSData, not a file URL or file handle. + * + * Default value is NO. + */ +@property(atomic, assign) BOOL shouldUploadWithSingleRequest; + +/** + * Uploads may be done without a JSON body as metadata in the initial request. + * + * Default value is NO. + */ +@property(atomic, assign) BOOL shouldSendUploadOnly; + +/** + * Uploads may use a background session when uploading via GTMSessionUploadFetcher. + * Since background session fetches are slower than foreground fetches, this defaults + * to NO. + * + * It's reasonable for an application to set this to YES for a rare upload of a large file. + * + * Default value is NO. + * + * For more information about the hazards of background sessions, see the header comments for + * the GTMSessionFetcher useBackgroundSession property. + */ +@property(atomic, assign) BOOL useBackgroundSession; + +/** + * Constructor for uploading from @c NSData. + * + * @param data The data to uploaded. + * @param mimeType The media's type. + * + * @return The upload parameters object. + */ ++ (instancetype)uploadParametersWithData:(NSData *)data + MIMEType:(NSString *)mimeType; + +/** + * Constructor for uploading from a file URL. + * + * @param fileURL The file to upload. + * @param mimeType The media's type. + * + * @return The upload parameters object. + */ ++ (instancetype)uploadParametersWithFileURL:(NSURL *)fileURL + MIMEType:(NSString *)mimeType; + +/** + * Constructor for uploading from a file handle. + * + * @note This method is provided for compatibility with older code. To upload files, + * use a file URL. + */ ++ (instancetype)uploadParametersWithFileHandle:(NSFileHandle *)fileHandle + MIMEType:(NSString *)mimeType; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Pods/GoogleAPIClientForREST/Source/Objects/GTLRUploadParameters.m b/Pods/GoogleAPIClientForREST/Source/Objects/GTLRUploadParameters.m @@ -0,0 +1,119 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !__has_feature(objc_arc) +#error "This file needs to be compiled with ARC enabled." +#endif + +#include <objc/runtime.h> + +#import "GTLRUploadParameters.h" + +@implementation GTLRUploadParameters + +@synthesize MIMEType = _MIMEType, + data = _data, + fileHandle = _fileHandle, + uploadLocationURL = _uploadLocationURL, + fileURL = _fileURL, + shouldUploadWithSingleRequest = _shouldUploadWithSingleRequest, + shouldSendUploadOnly = _shouldSendUploadOnly, + useBackgroundSession = _useBackgroundSession; + ++ (instancetype)uploadParametersWithData:(NSData *)data + MIMEType:(NSString *)mimeType { + GTLRUploadParameters *params = [[self alloc] init]; + params.data = data; + params.MIMEType = mimeType; + return params; +} + ++ (instancetype)uploadParametersWithFileHandle:(NSFileHandle *)fileHandle + MIMEType:(NSString *)mimeType { + GTLRUploadParameters *params = [[self alloc] init]; + params.fileHandle = fileHandle; + params.MIMEType = mimeType; + return params; +} + ++ (instancetype)uploadParametersWithFileURL:(NSURL *)fileURL + MIMEType:(NSString *)mimeType { + GTLRUploadParameters *params = [[self alloc] init]; + params.fileURL = fileURL; + params.MIMEType = mimeType; + return params; +} + +- (id)copyWithZone:(NSZone *)zone { + GTLRUploadParameters *newParams = [[[self class] allocWithZone:zone] init]; + newParams.MIMEType = self.MIMEType; + newParams.data = self.data; + newParams.fileHandle = self.fileHandle; + newParams.fileURL = self.fileURL; + newParams.uploadLocationURL = self.uploadLocationURL; + newParams.shouldUploadWithSingleRequest = self.shouldUploadWithSingleRequest; + newParams.shouldSendUploadOnly = self.shouldSendUploadOnly; + newParams.useBackgroundSession = self.useBackgroundSession; + return newParams; +} + +#if DEBUG +- (NSString *)description { + NSMutableArray *array = [NSMutableArray array]; + NSString *str = [NSString stringWithFormat:@"MIMEType:%@", _MIMEType]; + [array addObject:str]; + + if (_data) { + str = [NSString stringWithFormat:@"data:%llu bytes", + (unsigned long long)_data.length]; + [array addObject:str]; + } + + if (_fileHandle) { + str = [NSString stringWithFormat:@"fileHandle:%@", _fileHandle]; + [array addObject:str]; + } + + if (_fileURL) { + str = [NSString stringWithFormat:@"file:%@", [_fileURL path]]; + [array addObject:str]; + } + + if (_uploadLocationURL) { + str = [NSString stringWithFormat:@"uploadLocation:%@", + [_uploadLocationURL absoluteString]]; + [array addObject:str]; + } + + if (_shouldSendUploadOnly) { + [array addObject:@"shouldSendUploadOnly"]; + } + + if (_shouldUploadWithSingleRequest) { + [array addObject:@"uploadWithSingleRequest"]; + } + + if (_useBackgroundSession) { + [array addObject:@"useBackgroundSession"]; + } + + NSString *descStr = [array componentsJoinedByString:@", "]; + str = [NSString stringWithFormat:@"%@ %p: {%@}", + [self class], self, descStr]; + return str; +} +#endif // DEBUG + +@end diff --git a/Pods/GoogleAPIClientForREST/Source/Utilities/GTLRBase64.h b/Pods/GoogleAPIClientForREST/Source/Utilities/GTLRBase64.h @@ -0,0 +1,29 @@ +/* Copyright (c) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +NSData * _Nullable GTLRDecodeBase64(NSString * _Nullable base64Str); +NSString * _Nullable GTLREncodeBase64(NSData * _Nullable data); + +// "Web-safe" encoding substitutes - and _ for + and / in the encoding table, +// per http://www.ietf.org/rfc/rfc4648.txt section 5. + +NSData * _Nullable GTLRDecodeWebSafeBase64(NSString * _Nullable base64Str); +NSString * _Nullable GTLREncodeWebSafeBase64(NSData * _Nullable data); + +NS_ASSUME_NONNULL_END diff --git a/Pods/GoogleAPIClientForREST/Source/Utilities/GTLRBase64.m b/Pods/GoogleAPIClientForREST/Source/Utilities/GTLRBase64.m @@ -0,0 +1,143 @@ +/* Copyright (c) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !__has_feature(objc_arc) +#error "This file needs to be compiled with ARC enabled." +#endif + +#import "GTLRBase64.h" + +// Based on Cyrus Najmabadi's elegent little encoder and decoder from +// http://www.cocoadev.com/index.pl?BaseSixtyFour + +static char gStandardEncodingTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +static char gWebSafeEncodingTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +#pragma mark Encode + +static NSString *EncodeBase64StringCommon(NSData *data, const char *table) { + if (data == nil) return nil; + + const uint8_t* input = data.bytes; + NSUInteger length = data.length; + + NSUInteger bufferSize = ((length + 2) / 3) * 4; + NSMutableData* buffer = [NSMutableData dataWithLength:bufferSize]; + + int8_t *output = buffer.mutableBytes; + + for (NSUInteger i = 0; i < length; i += 3) { + NSUInteger value = 0; + for (NSUInteger j = i; j < (i + 3); j++) { + value <<= 8; + + if (j < length) { + value |= (0xFF & input[j]); + } + } + + NSInteger idx = (i / 3) * 4; + output[idx + 0] = table[(value >> 18) & 0x3F]; + output[idx + 1] = table[(value >> 12) & 0x3F]; + output[idx + 2] = (i + 1) < length ? table[(value >> 6) & 0x3F] : '='; + output[idx + 3] = (i + 2) < length ? table[(value >> 0) & 0x3F] : '='; + } + + NSString *result = [[NSString alloc] initWithData:buffer + encoding:NSASCIIStringEncoding]; + return result; +} + +NSString *GTLREncodeBase64(NSData *data) { + return EncodeBase64StringCommon(data, gStandardEncodingTable); +} + +NSString *GTLREncodeWebSafeBase64(NSData *data) { + return EncodeBase64StringCommon(data, gWebSafeEncodingTable); +} + +#pragma mark Decode + +static void CreateDecodingTable(const char *encodingTable, + size_t encodingTableSize, char *decodingTable) { + memset(decodingTable, 0, 128); + for (unsigned int i = 0; i < encodingTableSize; i++) { + decodingTable[(unsigned int) encodingTable[i]] = (char)i; + } +} + +static NSData *DecodeBase64StringCommon(NSString *base64Str, + char *decodingTable) { + // The input string should be plain ASCII + const char *cString = [base64Str cStringUsingEncoding:NSASCIIStringEncoding]; + if (cString == nil) return nil; + + NSInteger inputLength = (NSInteger)strlen(cString); + if (inputLength % 4 != 0) return nil; + if (inputLength == 0) return [NSData data]; + + while (inputLength > 0 && cString[inputLength - 1] == '=') { + inputLength--; + } + + NSInteger outputLength = inputLength * 3 / 4; + NSMutableData* data = [NSMutableData dataWithLength:(NSUInteger)outputLength]; + uint8_t *output = data.mutableBytes; + + NSInteger inputPoint = 0; + NSInteger outputPoint = 0; + char *table = decodingTable; + + while (inputPoint < inputLength) { + int i0 = cString[inputPoint++]; + int i1 = cString[inputPoint++]; + int i2 = inputPoint < inputLength ? cString[inputPoint++] : 'A'; // 'A' will decode to \0 + int i3 = inputPoint < inputLength ? cString[inputPoint++] : 'A'; + + output[outputPoint++] = (uint8_t)((table[i0] << 2) | (table[i1] >> 4)); + if (outputPoint < outputLength) { + output[outputPoint++] = (uint8_t)(((table[i1] & 0xF) << 4) | (table[i2] >> 2)); + } + if (outputPoint < outputLength) { + output[outputPoint++] = (uint8_t)(((table[i2] & 0x3) << 6) | table[i3]); + } + } + + return data; +} + +NSData *GTLRDecodeBase64(NSString *base64Str) { + static char decodingTable[128]; + static BOOL hasInited = NO; + + if (!hasInited) { + CreateDecodingTable(gStandardEncodingTable, sizeof(gStandardEncodingTable), + decodingTable); + hasInited = YES; + } + return DecodeBase64StringCommon(base64Str, decodingTable); +} + +NSData *GTLRDecodeWebSafeBase64(NSString *base64Str) { + static char decodingTable[128]; + static BOOL hasInited = NO; + + if (!hasInited) { + CreateDecodingTable(gWebSafeEncodingTable, sizeof(gWebSafeEncodingTable), + decodingTable); + hasInited = YES; + } + return DecodeBase64StringCommon(base64Str, decodingTable); +} diff --git a/Pods/GoogleAPIClientForREST/Source/Utilities/GTLRFramework.h b/Pods/GoogleAPIClientForREST/Source/Utilities/GTLRFramework.h @@ -0,0 +1,34 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#import "GTLRDefines.h" + +NS_ASSUME_NONNULL_BEGIN + +// Returns the version of the framework. Major and minor should +// match the bundle version in the Info.plist file. +// +// Pass NULL to ignore any of the parameters. + +void GTLRFrameworkVersion(NSUInteger * _Nullable major, + NSUInteger * _Nullable minor, + NSUInteger * _Nullable release); + +// Returns the version in @"a.b" or @"a.b.c" format +NSString *GTLRFrameworkVersionString(void); + +NS_ASSUME_NONNULL_END diff --git a/Pods/GoogleAPIClientForREST/Source/Utilities/GTLRFramework.m b/Pods/GoogleAPIClientForREST/Source/Utilities/GTLRFramework.m @@ -0,0 +1,44 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !__has_feature(objc_arc) +#error "This file needs to be compiled with ARC enabled." +#endif + +#include "GTLRFramework.h" + +void GTLRFrameworkVersion(NSUInteger* major, NSUInteger* minor, NSUInteger* release) { + // version 3.0.0 + if (major) *major = 3; + if (minor) *minor = 0; + if (release) *release = 0; +} + +NSString *GTLRFrameworkVersionString(void) { + NSUInteger major, minor, release; + NSString *libVersionString; + + GTLRFrameworkVersion(&major, &minor, &release); + + // most library releases will have a release value of zero + if (release != 0) { + libVersionString = [NSString stringWithFormat:@"%d.%d.%d", + (int)major, (int)minor, (int)release]; + } else { + libVersionString = [NSString stringWithFormat:@"%d.%d", + (int)major, (int)minor]; + } + return libVersionString; +} diff --git a/Pods/GoogleAPIClientForREST/Source/Utilities/GTLRURITemplate.h b/Pods/GoogleAPIClientForREST/Source/Utilities/GTLRURITemplate.h @@ -0,0 +1,48 @@ +/* Copyright (c) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#ifndef SKIP_GTLR_DEFINES + #import "GTLRDefines.h" +#endif + +NS_ASSUME_NONNULL_BEGIN + +// +// URI Template +// +// http://tools.ietf.org/html/draft-gregorio-uritemplate-04 +// +// NOTE: This implemention is only a subset of the spec. It should be able +// to parse any tempate that matches the spec, but if the template makes use +// of a feature that is not supported, it will fail with an error. +// + +@interface GTLRURITemplate : NSObject + +// Process the template. If the template uses an unsupported feature, it will +// throw an exception to help catch that limitation. Currently unsupported +// feature is partial result modifiers (prefix/suffix). +// +// valueProvider should be anything that implements -objectForKey:. At the +// simplest level, this can be an NSDictionary. However, a custom class that +// implements valueForKey my be better for some uses. ++ (NSString *)expandTemplate:(NSString *)URITemplate + values:(NSDictionary *)valueProvider; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Pods/GoogleAPIClientForREST/Source/Utilities/GTLRURITemplate.m b/Pods/GoogleAPIClientForREST/Source/Utilities/GTLRURITemplate.m @@ -0,0 +1,511 @@ +/* Copyright (c) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !__has_feature(objc_arc) +#error "This file needs to be compiled with ARC enabled." +#endif + +#import "GTLRURITemplate.h" + +// Key constants for handling variables. +static NSString *const kVariable = @"variable"; // NSString +static NSString *const kExplode = @"explode"; // NSString +static NSString *const kPartial = @"partial"; // NSString +static NSString *const kPartialValue = @"partialValue"; // NSNumber + +// Help for passing the Expansion info in one shot. +struct ExpansionInfo { + // Constant for the whole expansion. + unichar expressionOperator; + __unsafe_unretained NSString *joiner; + BOOL allowReservedInEscape; + + // Update for each variable. + __unsafe_unretained NSString *explode; +}; + +// Helper just to shorten the lines when needed. +static NSString *UnescapeString(NSString *str) { + return [str stringByRemovingPercentEncoding]; +} + +static NSString *EscapeString(NSString *str, BOOL allowReserved) { + // The spec is a little hard to map onto the charsets, so force + // reserved bits in/out. + NSMutableCharacterSet *cs = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy]; + NSString * const kReservedChars = @":/?#[]@!$&'()*+,;="; + if (allowReserved) { + [cs addCharactersInString:kReservedChars]; + } else { + [cs removeCharactersInString:kReservedChars]; + } + NSString *resultStr = [str stringByAddingPercentEncodingWithAllowedCharacters:cs]; + return resultStr; +} + +static NSString *StringFromNSNumber(NSNumber *rawValue) { + NSString *strValue; + // NSNumber doesn't expose a way to tell if it is holding a BOOL or something + // else. -[NSNumber objCType] for a BOOL is the same as @encoding(char), but + // in the 64bit runtine @encoding(BOOL) (or for "bool") won't match that as + // the 64bit runtime actually has a true boolean type. Instead we reply on + // checking if the numbers are the CFBoolean constants to force true/value + // values. + if ((rawValue == (NSNumber *)kCFBooleanTrue) || + (rawValue == (NSNumber *)kCFBooleanFalse)) { + strValue = (rawValue.boolValue ? @"true" : @"false"); + } else { + strValue = [rawValue stringValue]; + } + return strValue; +} + +@implementation GTLRURITemplate + +#pragma mark Internal Helpers + ++ (BOOL)parseExpression:(NSString *)expression + expressionOperator:(unichar*)outExpressionOperator + variables:(NSMutableArray **)outVariables + defaultValues:(NSMutableDictionary **)outDefaultValues { + + // Please see the spec for full details, but here are the basics: + // + // URI-Template = *( literals / expression ) + // expression = "{" [ operator ] variable-list "}" + // variable-list = varspec *( "," varspec ) + // varspec = varname [ modifier ] [ "=" default ] + // varname = varchar *( varchar / "." ) + // modifier = explode / partial + // explode = ( "*" / "+" ) + // partial = ( substring / remainder ) offset + // + // Examples: + // http://www.example.com/foo{?query,number} + // http://maps.com/mapper{?address*} + // http://directions.org/directions{?from+,to+} + // http://search.org/query{?terms+=none} + // + + // http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-2.2 + // Operator and op-reserve characters + static NSCharacterSet *operatorSet = nil; + // http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-2.4.1 + // Explode characters + static NSCharacterSet *explodeSet = nil; + // http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-2.4.2 + // Partial (prefix/subset) characters + static NSCharacterSet *partialSet = nil; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + operatorSet = [NSCharacterSet characterSetWithCharactersInString:@"+./;?|!@"]; + explodeSet = [NSCharacterSet characterSetWithCharactersInString:@"*+"]; + partialSet = [NSCharacterSet characterSetWithCharactersInString:@":^"]; + }); + + // http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-3.3 + // Empty expression inlines the expression. + if (expression.length == 0) return NO; + + // Pull off any operator. + *outExpressionOperator = 0; + unichar firstChar = [expression characterAtIndex:0]; + if ([operatorSet characterIsMember:firstChar]) { + *outExpressionOperator = firstChar; + expression = [expression substringFromIndex:1]; + } + + if (expression.length == 0) return NO; + + // Need to find atleast one varspec for the expresssion to be considered + // valid. + BOOL gotAVarspec = NO; + + // Split the variable list. + NSArray *varspecs = [expression componentsSeparatedByString:@","]; + + // Extract the defaults, explodes and modifiers from the varspecs. + *outVariables = [NSMutableArray arrayWithCapacity:varspecs.count]; + for (__strong NSString *varspec in varspecs) { + NSString *defaultValue = nil; + + if (varspec.length == 0) continue; + + NSMutableDictionary *varInfo = + [NSMutableDictionary dictionaryWithCapacity:4]; + + // Check for a default (foo=bar). + NSRange range = [varspec rangeOfString:@"="]; + if (range.location != NSNotFound) { + defaultValue = + UnescapeString([varspec substringFromIndex:range.location + 1]); + varspec = [varspec substringToIndex:range.location]; + + if (varspec.length == 0) continue; + } + + // Check for explode (foo*). + NSUInteger lenLessOne = varspec.length - 1; + if ([explodeSet characterIsMember:[varspec characterAtIndex:lenLessOne]]) { + [varInfo setObject:[varspec substringFromIndex:lenLessOne] forKey:kExplode]; + varspec = [varspec substringToIndex:lenLessOne]; + if (varspec.length == 0) continue; + } else { + // Check for partial (prefix/suffix) (foo:12). + range = [varspec rangeOfCharacterFromSet:partialSet]; + if (range.location != NSNotFound) { + NSString *partialMode = [varspec substringWithRange:range]; + NSString *valueStr = [varspec substringFromIndex:range.location + 1]; + // If there wasn't a value for the partial, ignore it. + if (valueStr.length > 0) { + [varInfo setObject:partialMode forKey:kPartial]; + // TODO: Should validate valueStr is just a number... + [varInfo setObject:[NSNumber numberWithInteger:[valueStr integerValue]] + forKey:kPartialValue]; + } + varspec = [varspec substringToIndex:range.location]; + if (varspec.length == 0) continue; + } + } + + // Spec allows percent escaping in names, so undo that. + varspec = UnescapeString(varspec); + + // Save off the cleaned up variable name. + [varInfo setObject:varspec forKey:kVariable]; + [*outVariables addObject:varInfo]; + gotAVarspec = YES; + + // Now that the variable has been cleaned up, store its default. + if (defaultValue) { + if (*outDefaultValues == nil) { + *outDefaultValues = [NSMutableDictionary dictionary]; + } + [*outDefaultValues setObject:defaultValue forKey:varspec]; + } + } + // All done. + return gotAVarspec; +} + ++ (NSString *)expandVariables:(NSArray *)variables + expressionOperator:(unichar)expressionOperator + values:(NSDictionary *)valueProvider + defaultValues:(NSMutableDictionary *)defaultValues { + NSString *prefix = nil; + struct ExpansionInfo expansionInfo = { + .expressionOperator = expressionOperator, + .joiner = nil, + .allowReservedInEscape = NO, + .explode = nil, + }; + switch (expressionOperator) { + case 0: + expansionInfo.joiner = @","; + prefix = @""; + break; + case '+': + expansionInfo.joiner = @","; + prefix = @""; + // The reserved character are safe from escaping. + expansionInfo.allowReservedInEscape = YES; + break; + case '.': + expansionInfo.joiner = @"."; + prefix = @"."; + break; + case '/': + expansionInfo.joiner = @"/"; + prefix = @"/"; + break; + case ';': + expansionInfo.joiner = @";"; + prefix = @";"; + break; + case '?': + expansionInfo.joiner = @"&"; + prefix = @"?"; + break; + default: + [NSException raise:@"GTLRURITemplateUnsupported" + format:@"Unknown expression operator '%C'", expressionOperator]; + break; + } + + NSMutableArray *results = [NSMutableArray arrayWithCapacity:variables.count]; + + for (NSDictionary *varInfo in variables) { + NSString *variable = [varInfo objectForKey:kVariable]; + + expansionInfo.explode = [varInfo objectForKey:kExplode]; + // Look up the variable value. + id rawValue = [valueProvider objectForKey:variable]; + + // If the value is an empty array or dictionary, the default is still used. + if (([rawValue isKindOfClass:[NSArray class]] + || [rawValue isKindOfClass:[NSDictionary class]]) + && ((NSArray *)rawValue).count == 0) { + rawValue = nil; + } + + // Got nothing? Check defaults. + if (rawValue == nil) { + rawValue = [defaultValues objectForKey:variable]; + } + + // If we didn't get any value, on to the next thing. + if (!rawValue) { + continue; + } + + // Time do to the work... + NSString *result = nil; + if ([rawValue isKindOfClass:[NSString class]]) { + result = [self expandString:rawValue + variableName:variable + expansionInfo:&expansionInfo]; + } else if ([rawValue isKindOfClass:[NSNumber class]]) { + // Turn the number into a string and send it on its way. + NSString *strValue = StringFromNSNumber(rawValue); + result = [self expandString:strValue + variableName:variable + expansionInfo:&expansionInfo]; + } else if ([rawValue isKindOfClass:[NSArray class]]) { + result = [self expandArray:rawValue + variableName:variable + expansionInfo:&expansionInfo]; + } else if ([rawValue isKindOfClass:[NSDictionary class]]) { + result = [self expandDictionary:rawValue + variableName:variable + expansionInfo:&expansionInfo]; + } else { + [NSException raise:@"GTLRURITemplateUnsupported" + format:@"Variable returned unsupported type (%@)", + NSStringFromClass([rawValue class])]; + } + + // Did it generate anything? + if (result == nil) + continue; + + // Apply partial. + // Defaults should get partial applied? + // ( http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-2.5 ) + NSString *partial = [varInfo objectForKey:kPartial]; + if (partial.length > 0) { + [NSException raise:@"GTLRURITemplateUnsupported" + format:@"Unsupported partial on expansion %@", partial]; + } + + // Add the result + [results addObject:result]; + } + + // Join and add any needed prefix. + NSString *joinedResults = + [results componentsJoinedByString:expansionInfo.joiner]; + if ((prefix.length > 0) && (joinedResults.length > 0)) { + return [prefix stringByAppendingString:joinedResults]; + } + return joinedResults; +} + ++ (NSString *)expandString:(NSString *)valueStr + variableName:(NSString *)variableName + expansionInfo:(struct ExpansionInfo *)expansionInfo { + NSString *escapedValue = + EscapeString(valueStr, expansionInfo->allowReservedInEscape); + switch (expansionInfo->expressionOperator) { + case ';': + case '?': + if (valueStr.length > 0) { + return [NSString stringWithFormat:@"%@=%@", variableName, escapedValue]; + } + return variableName; + default: + return escapedValue; + } +} + ++ (NSString *)expandArray:(NSArray *)valueArray + variableName:(NSString *)variableName + expansionInfo:(struct ExpansionInfo *)expansionInfo { + NSMutableArray *results = [NSMutableArray arrayWithCapacity:valueArray.count]; + // When joining variable with value, use "var.val" except for 'path' and + // 'form' style expression, use 'var=val' then. + char variableValueJoiner = '.'; + unichar expressionOperator = expansionInfo->expressionOperator; + if ((expressionOperator == ';') || (expressionOperator == '?')) { + variableValueJoiner = '='; + } + // Loop over the values. + for (id rawValue in valueArray) { + NSString *value; + if ([rawValue isKindOfClass:[NSNumber class]]) { + value = StringFromNSNumber((id)rawValue); + } else if ([rawValue isKindOfClass:[NSString class]]) { + value = rawValue; + } else { + [NSException raise:@"GTLRURITemplateUnsupported" + format:@"Variable '%@' returned NSArray with unsupported type (%@), array: %@", + variableName, NSStringFromClass([rawValue class]), valueArray]; + } + // Escape it. + value = EscapeString(value, expansionInfo->allowReservedInEscape); + // Should variable names be used? + if ([expansionInfo->explode isEqual:@"+"]) { + value = [NSString stringWithFormat:@"%@%c%@", + variableName, variableValueJoiner, value]; + } + [results addObject:value]; + } + if (results.count > 0) { + // Use the default joiner unless there was no explode request, then a list + // always gets comma seperated. + NSString *joiner = expansionInfo->joiner; + if (expansionInfo->explode == nil) { + joiner = @","; + } + // Join the values. + NSString *joined = [results componentsJoinedByString:joiner]; + // 'form' style without an explode gets the variable name set to the + // joined list of values. + if ((expressionOperator == '?') && (expansionInfo->explode == nil)) { + return [NSString stringWithFormat:@"%@=%@", variableName, joined]; + } + return joined; + } + return nil; +} + ++ (NSString *)expandDictionary:(NSDictionary *)valueDict + variableName:(NSString *)variableName + expansionInfo:(struct ExpansionInfo *)expansionInfo { + NSMutableArray *results = [NSMutableArray arrayWithCapacity:valueDict.count]; + // When joining variable with value: + // - Default to the joiner... + // - No explode, always comma... + // - For 'path' and 'form' style expression, use 'var=val'. + NSString *keyValueJoiner = expansionInfo->joiner; + unichar expressionOperator = expansionInfo->expressionOperator; + if (expansionInfo->explode == nil) { + keyValueJoiner = @","; + } else if ((expressionOperator == ';') || (expressionOperator == '?')) { + keyValueJoiner = @"="; + } + // Loop over the sorted keys. + NSArray *sortedKeys = [valueDict.allKeys sortedArrayUsingSelector:@selector(compare:)]; + for (__strong NSString *key in sortedKeys) { + NSString *value = [valueDict objectForKey:key]; + // Escape them. + key = EscapeString(key, expansionInfo->allowReservedInEscape); + value = EscapeString(value, expansionInfo->allowReservedInEscape); + // Should variable names be used? + if ([expansionInfo->explode isEqual:@"+"]) { + key = [NSString stringWithFormat:@"%@.%@", variableName, key]; + } + if ((expressionOperator == '?' || expressionOperator == ';') + && (value.length == 0)) { + [results addObject:key]; + } else { + NSString *pair = [NSString stringWithFormat:@"%@%@%@", + key, keyValueJoiner, value]; + [results addObject:pair]; + } + } + if (results.count) { + // Use the default joiner unless there was no explode request, then a list + // always gets comma seperated. + NSString *joiner = expansionInfo->joiner; + if (expansionInfo->explode == nil) { + joiner = @","; + } + // Join the values. + NSString *joined = [results componentsJoinedByString:joiner]; + // 'form' style without an explode gets the variable name set to the + // joined list of values. + if ((expressionOperator == '?') && (expansionInfo->explode == nil)) { + return [NSString stringWithFormat:@"%@=%@", variableName, joined]; + } + return joined; + } + return nil; +} + +#pragma mark Public API + ++ (NSString *)expandTemplate:(NSString *)uriTemplate + values:(NSDictionary *)valueProvider { + NSMutableString *result = + [NSMutableString stringWithCapacity:uriTemplate.length]; + + NSScanner *scanner = [NSScanner scannerWithString:uriTemplate]; + [scanner setCharactersToBeSkipped:nil]; + + // Defaults have to live through the full evaluation, so if any are encoured + // they are reused throughout the expansion calls. + NSMutableDictionary *defaultValues = nil; + + // Pull out the expressions for processing. + while (![scanner isAtEnd]) { + NSString *skipped = nil; + // Find the next '{'. + if ([scanner scanUpToString:@"{" intoString:&skipped]) { + // Add anything before it to the result. + [result appendString:skipped]; + } + // Advance over the '{'. + [scanner scanString:@"{" intoString:nil]; + // Collect the expression. + NSString *expression = nil; + if ([scanner scanUpToString:@"}" intoString:&expression]) { + // Collect the trailing '}' on the expression. + BOOL hasTrailingBrace = [scanner scanString:@"}" intoString:nil]; + + // Parse the expression. + NSMutableArray *variables = nil; + unichar expressionOperator = 0; + if ([self parseExpression:expression + expressionOperator:&expressionOperator + variables:&variables + defaultValues:&defaultValues]) { + // Do the expansion. + NSString *substitution = [self expandVariables:variables + expressionOperator:expressionOperator + values:valueProvider + defaultValues:defaultValues]; + if (substitution) { + [result appendString:substitution]; + } + } else { + // Failed to parse, add the raw expression to the output. + if (hasTrailingBrace) { + [result appendFormat:@"{%@}", expression]; + } else { + [result appendFormat:@"{%@", expression]; + } + } + } else if (![scanner isAtEnd]) { + // Empty expression ('{}'). Copy over the opening brace and the trailing + // one will be copied by the next cycle of the loop. + [result appendString:@"{"]; + } + } + + return result; +} + +@end diff --git a/Pods/GoogleAPIClientForREST/Source/Utilities/GTLRUtilities.h b/Pods/GoogleAPIClientForREST/Source/Utilities/GTLRUtilities.h @@ -0,0 +1,52 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#ifndef SKIP_GTLR_DEFINES + #import "GTLRDefines.h" +#endif + +NS_ASSUME_NONNULL_BEGIN + +// Helper functions for implementing isEqual: +BOOL GTLR_AreEqualOrBothNil(id _Nullable obj1, id _Nullable obj2); +BOOL GTLR_AreBoolsEqual(BOOL b1, BOOL b2); + +// Helper to ensure a number is a number. +// +// The Google API servers will send numbers >53 bits as strings to avoid +// bugs in some JavaScript implementations. Work around this by catching +// the string and turning it back into a number. +NSNumber *GTLR_EnsureNSNumber(NSNumber *num); + +@interface GTLRUtilities : NSObject + +// Key-value coding searches in an array +// +// Utilities to get from an array objects having a known value (or nil) +// at a keyPath + ++ (NSArray *)objectsFromArray:(NSArray *)sourceArray + withValue:(id)desiredValue + forKeyPath:(NSString *)keyPath; + ++ (nullable id)firstObjectFromArray:(NSArray *)sourceArray + withValue:(id)desiredValue + forKeyPath:(NSString *)keyPath; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Pods/GoogleAPIClientForREST/Source/Utilities/GTLRUtilities.m b/Pods/GoogleAPIClientForREST/Source/Utilities/GTLRUtilities.m @@ -0,0 +1,117 @@ +/* Copyright (c) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if !__has_feature(objc_arc) +#error "This file needs to be compiled with ARC enabled." +#endif + +#import "GTLRUtilities.h" + +#include <objc/runtime.h> + +@implementation GTLRUtilities + +#pragma mark Key-Value Coding Searches in an Array + ++ (NSArray *)objectsFromArray:(NSArray *)sourceArray + withValue:(id)desiredValue + forKeyPath:(NSString *)keyPath { + // Step through all entries, get the value from + // the key path, and see if it's equal to the + // desired value + NSMutableArray *results = [NSMutableArray array]; + + for(id obj in sourceArray) { + id val = [obj valueForKeyPath:keyPath]; + if (GTLR_AreEqualOrBothNil(val, desiredValue)) { + + // found a match; add it to the results array + [results addObject:obj]; + } + } + return results; +} + ++ (id)firstObjectFromArray:(NSArray *)sourceArray + withValue:(id)desiredValue + forKeyPath:(NSString *)keyPath { + for (id obj in sourceArray) { + id val = [obj valueForKeyPath:keyPath]; + if (GTLR_AreEqualOrBothNil(val, desiredValue)) { + // found a match; return it + return obj; + } + } + return nil; +} + +#pragma mark Version helpers + +@end + +// isEqual: has the fatal flaw that it doesn't deal well with the receiver +// being nil. We'll use this utility instead. +BOOL GTLR_AreEqualOrBothNil(id obj1, id obj2) { + if (obj1 == obj2) { + return YES; + } + if (obj1 && obj2) { + BOOL areEqual = [(NSObject *)obj1 isEqual:obj2]; + return areEqual; + } + return NO; +} + +BOOL GTLR_AreBoolsEqual(BOOL b1, BOOL b2) { + // avoid comparison problems with boolean types by negating + // both booleans + return (!b1 == !b2); +} + +NSNumber *GTLR_EnsureNSNumber(NSNumber *num) { + // If the server returned a string object where we expect a number, try + // to make a number object. + if ([num isKindOfClass:[NSString class]]) { + NSNumber *newNum; + NSString *str = (NSString *)num; + if ([str rangeOfString:@"."].location != NSNotFound) { + // This is a floating-point number. + // Force the parser to use '.' as the decimal separator. + static NSLocale *usLocale = nil; + @synchronized([GTLRUtilities class]) { + if (usLocale == nil) { + usLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; + } + newNum = [NSDecimalNumber decimalNumberWithString:(NSString*)num + locale:(id)usLocale]; + } + } else { + // NSDecimalNumber +decimalNumberWithString:locale: + // does not correctly create an NSNumber for large values like + // 71100000000007780. + if ([str hasPrefix:@"-"]) { + newNum = @([str longLongValue]); + } else { + const char *utf8 = str.UTF8String; + unsigned long long ull = strtoull(utf8, NULL, 10); + newNum = @(ull); + } + } + if (newNum != nil) { + num = newNum; + } + } + return num; +} diff --git a/Pods/Manifest.lock b/Pods/Manifest.lock @@ -0,0 +1,27 @@ +PODS: + - GoogleAPIClientForREST (1.3.8): + - GoogleAPIClientForREST/Core (= 1.3.8) + - GTMSessionFetcher (>= 1.1.7) + - GoogleAPIClientForREST/Core (1.3.8): + - GTMSessionFetcher (>= 1.1.7) + - GTMSessionFetcher (1.2.1): + - GTMSessionFetcher/Full (= 1.2.1) + - GTMSessionFetcher/Core (1.2.1) + - GTMSessionFetcher/Full (1.2.1): + - GTMSessionFetcher/Core (= 1.2.1) + +DEPENDENCIES: + - GoogleAPIClientForREST (~> 1.3.8) + +SPEC REPOS: + https://github.com/cocoapods/specs.git: + - GoogleAPIClientForREST + - GTMSessionFetcher + +SPEC CHECKSUMS: + GoogleAPIClientForREST: 5447a194eae517986cafe6421a5330b80b820591 + GTMSessionFetcher: 32aeca0aa144acea523e1c8e053089dec2cb98ca + +PODFILE CHECKSUM: dbc5c2766ede4673e953e610de8390e5215f6c55 + +COCOAPODS: 1.6.1 diff --git a/Pods/Pods.xcodeproj/project.pbxproj b/Pods/Pods.xcodeproj/project.pbxproj @@ -0,0 +1,1140 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + 0259933BCBC56AA6C06A6C9965DAF0B1 /* GTMSessionFetcherService.m in Sources */ = {isa = PBXBuildFile; fileRef = 66AFD895BB32492C28401C014FEA5B2A /* GTMSessionFetcherService.m */; }; + 05F189E1E5B58CD99B4BD4EF466BC28F /* GTMSessionFetcherLogging.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A37FA7630E26C1BB376757F9B56D929 /* GTMSessionFetcherLogging.m */; }; + 0A84CCA22F1C8173E6B37F2069950093 /* Pods-TeachersAssistantTests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 176E21E234D1EF72C0869C40836ECBCC /* Pods-TeachersAssistantTests-dummy.m */; }; + 0BD74743A49183F02ED5A060896D395C /* GTLRQuery.m in Sources */ = {isa = PBXBuildFile; fileRef = F60610D99772BFEF6876443BB367B406 /* GTLRQuery.m */; }; + 143CA95BCA38E98A1C953F12ED7A68C5 /* GTLRBatchResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 65F97E32937BB5235135137421037918 /* GTLRBatchResult.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1E7061DAA1598078FD6F7467039FD69A /* GTLRBase64.h in Headers */ = {isa = PBXBuildFile; fileRef = C4CD076CCAAC4DDE1AD8F8D908810C14 /* GTLRBase64.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 231E72C5222861B3A2749B5999FCCD3B /* GTLRBatchQuery.h in Headers */ = {isa = PBXBuildFile; fileRef = 7D4EB57F9DFC922E6EC6E7FDE6B66497 /* GTLRBatchQuery.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25018FB43685B12C22C192ACA8B195DF /* GTLRUploadParameters.m in Sources */ = {isa = PBXBuildFile; fileRef = 1BAC2903B8C1077CF9EECF374BCBBE01 /* GTLRUploadParameters.m */; }; + 27ADCD707AC02EE76FB6A8D9E2DA2BCB /* GTLRDuration.m in Sources */ = {isa = PBXBuildFile; fileRef = DA49BC58BF18A35373D3EA2DBF421D00 /* GTLRDuration.m */; }; + 2B77711505B6A35910051173A84BECF0 /* GTLRBatchResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 156A1A9280D93A7463591064FCCAD02F /* GTLRBatchResult.m */; }; + 305C5C21B3895A0E26391E09987C2385 /* GTLRUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 961467D2BF26CCA63537C0D3487B1DB8 /* GTLRUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 382F74A3FBE759251A5B6AB8868A4765 /* GTLRRuntimeCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = 278E2C6B9FF0A381750BFB75627FFDB3 /* GTLRRuntimeCommon.m */; }; + 45B0D2B682D7C862E294F1467980E2E3 /* GTLRDateTime.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D5E10D54CC70F2A3DD8909F38107D22 /* GTLRDateTime.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 47E2FBDB54550F73850F0B39FABAC84C /* GTMSessionFetcher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7C69C428FB7737AC8AB7F9954BA14AF /* GTMSessionFetcher.framework */; }; + 4B2F27E4AAC32FDEF59BAE37A9027501 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCA4CC8015A178888F4A57DDED67433A /* Foundation.framework */; }; + 4B4ED067E7A6A0E4102A77EF6E12B6C5 /* GTLRDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = E4C698A16FDC77A62625C9DA6165582D /* GTLRDefines.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4CDF43FD9B915EF76C6A771BA4D2C0A4 /* GTLRDateTime.m in Sources */ = {isa = PBXBuildFile; fileRef = 37E7B98E15AE2BF1DD55266099D7B651 /* GTLRDateTime.m */; }; + 4CF2FAAEE1D988441F281430D392B6BF /* GTMGatherInputStream.h in Headers */ = {isa = PBXBuildFile; fileRef = EF176B2AA0FA643A494621B8C192EDB5 /* GTMGatherInputStream.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5F50C32122B3ADA9EA0D5CE4ABEFDD78 /* GoogleAPIClientForREST-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 35B8090078E48E239D39288A69412AD5 /* GoogleAPIClientForREST-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 62B708568BAC8B6D5C9084BEC87B90E4 /* GTLRObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 2E013B0D53EF922E03F88FAFCBD9FF17 /* GTLRObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 62DB18E73656128001751C0A2F73648B /* GTLRService.h in Headers */ = {isa = PBXBuildFile; fileRef = 085E24A1AC949B7CDB4C719B3C53EA14 /* GTLRService.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 66C7544B5A09F539D7D4DB6F937F61B0 /* GTMGatherInputStream.m in Sources */ = {isa = PBXBuildFile; fileRef = 6D3AE8D1C29E34EC8CE96ACB0CDED5E5 /* GTMGatherInputStream.m */; }; + 6C571A236A1EBAA0111698DE1FC4D0A4 /* Pods-TeachersAssistantTests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 029A66B7819D4FD2C4DD1328C5488419 /* Pods-TeachersAssistantTests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7260CEF6719019E19D1A0D894188B05F /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCA4CC8015A178888F4A57DDED67433A /* Foundation.framework */; }; + 790C8B1BED5D639EADE6BD6E45C9E8F5 /* GTLRURITemplate.h in Headers */ = {isa = PBXBuildFile; fileRef = F77C7F83EE5CFC28448551424FFA648B /* GTLRURITemplate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7F46C4E77AF7E4C2F9751A5154F7BFEB /* GoogleAPIClientForREST-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B8F6CCC3AB370F4B07F5B1EFFF4DD0E /* GoogleAPIClientForREST-dummy.m */; }; + 825E4CB2EA00517293F381A582600B6A /* GTLRUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BA885921F7C975C7E54FC245579DCDF /* GTLRUtilities.m */; }; + 877465AAAEA86697CEC56A0CC9F0AA43 /* GTMSessionFetcherLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = DA0FCEE372F04B2A01DB11789130BC5B /* GTMSessionFetcherLogging.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 87F7C625A44A243746CFA5568B05E023 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCA4CC8015A178888F4A57DDED67433A /* Foundation.framework */; }; + 89D083B4302DD4BFEFD5E4EC85350D77 /* GTMMIMEDocument.h in Headers */ = {isa = PBXBuildFile; fileRef = FD8BE7C44C98327BFA930F0F6F23D72A /* GTMMIMEDocument.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 91BF7F097ACC8FD1F9BF6248F400719A /* GTMSessionFetcher.h in Headers */ = {isa = PBXBuildFile; fileRef = D96F98D3386360D1DDE0C7ACF9E76014 /* GTMSessionFetcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 98FC93E3E80044F01349D3BBDC818CC0 /* GTLRBatchQuery.m in Sources */ = {isa = PBXBuildFile; fileRef = B390CCF062FC2081F94C3B2C3E894ACC /* GTLRBatchQuery.m */; }; + AD4147888DF506ACFCCE3FA5819A425C /* GTLRObject.m in Sources */ = {isa = PBXBuildFile; fileRef = CB7EA1458FDD080BFADB7A928D369059 /* GTLRObject.m */; }; + AD645B60AF3D1B463ECE1BCC70209B7D /* GTLRErrorObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 15B773B78B52F266A0CC624DDFFAE7EF /* GTLRErrorObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B3073DFBB5CB806BCFD875B3A5D949BB /* Pods-TeachersAssistant-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 74104373A68924B948E2FD66656DDF04 /* Pods-TeachersAssistant-dummy.m */; }; + B58498DCE67D2464A50F18A12F0C6FC8 /* GTLRFramework.h in Headers */ = {isa = PBXBuildFile; fileRef = E3726C85C7B6D56594A3E93E16CB8CD7 /* GTLRFramework.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BBFBB062310CB7A6DBD5A9C82310C5D4 /* GTLRErrorObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 62E9F4BAC2AA0264F8658C43B4E58636 /* GTLRErrorObject.m */; }; + BE578FD68DD6E0A6369DE0A0A5404B6F /* GTLRBase64.m in Sources */ = {isa = PBXBuildFile; fileRef = 9122A91940337F4A5E3FE7E589B77E2B /* GTLRBase64.m */; }; + C08BC708C768AB5D4A7759073374C09A /* GTLRUploadParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = E9E89C72B14FF0804BDF968943B9430E /* GTLRUploadParameters.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C0F61C910AA6E3457E18FBD400862107 /* GTMSessionUploadFetcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D39543E4D61DF7D7495633B6CCB2AAB /* GTMSessionUploadFetcher.m */; }; + C5CE0F21B7E068FD8FBAADC46F7EA854 /* Pods-TeachersAssistant-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DFABC77294AE1B3C79891E913990217 /* Pods-TeachersAssistant-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CB4139747C70971C4984E7511A20146E /* GTMReadMonitorInputStream.m in Sources */ = {isa = PBXBuildFile; fileRef = 0F8D1B4EFF5D66AE0AC686E8D76CA5A5 /* GTMReadMonitorInputStream.m */; }; + CB4E2DCF06B3563AE98E03932A5D80A0 /* GTLRDuration.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E8AB8288409C217E896484EDEB7D484 /* GTLRDuration.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D1F90521F48C8F6A488BC10751B720E1 /* GTLRQuery.h in Headers */ = {isa = PBXBuildFile; fileRef = B9CE91C5B6E4E8D7B8A7E5FEC3AEB4BC /* GTLRQuery.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DA7B909C6F910C653E12B053843E424E /* GTLRURITemplate.m in Sources */ = {isa = PBXBuildFile; fileRef = 9D0C1C1A79A5F7F06B97A360EEF3274C /* GTLRURITemplate.m */; }; + DCE0FED8DC34E000ED3EFA06C77CD76C /* GTMSessionUploadFetcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 01D3802510F36289EF4A957360AB8B4E /* GTMSessionUploadFetcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E0CB35D900D10D1C7E37F987ABFF8938 /* GTMReadMonitorInputStream.h in Headers */ = {isa = PBXBuildFile; fileRef = 9F361C9292422B96C48EEA211001CD82 /* GTMReadMonitorInputStream.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E43064D55A1482C00F38585873D6BB48 /* GTMMIMEDocument.m in Sources */ = {isa = PBXBuildFile; fileRef = 4996B5C022E49C108C735CCD2E8FAE2B /* GTMMIMEDocument.m */; }; + E982A92F18C647036E12507A4A4389C1 /* GTMSessionFetcher-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = BF6932F6AD90C0858CB4BD08B122853D /* GTMSessionFetcher-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EA6D1F0D9C9CB43E1C240B7052B65CC5 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ABBEE353D5FCEB9ED26E64872A4AB2B6 /* Security.framework */; }; + EA93FC0BAF02B8E5D9CA7137B8F35AD9 /* GTLRService.m in Sources */ = {isa = PBXBuildFile; fileRef = 63940FFB83E85057EEB621E27DE09111 /* GTLRService.m */; }; + EBB7AAAB95C04DC7F4B473EB68F1FD84 /* GTMSessionFetcher-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = EC32701EAC620041889026F257CF930A /* GTMSessionFetcher-dummy.m */; }; + F0F798FAF8F64C21D8A06294F9CCF82B /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCA4CC8015A178888F4A57DDED67433A /* Foundation.framework */; }; + F19CCC1BE7E7EFF05FE57EBE25F21414 /* GTMSessionFetcher.m in Sources */ = {isa = PBXBuildFile; fileRef = A174B22A98E274B816F8496BCC54DAB4 /* GTMSessionFetcher.m */; }; + FCB136E76490B4268311A8230BA408EA /* GTLRRuntimeCommon.h in Headers */ = {isa = PBXBuildFile; fileRef = 9B56C5D0A1A2E8BCAF6EC0BFE4BD9FB6 /* GTLRRuntimeCommon.h */; settings = {ATTRIBUTES = (Public, ); }; }; + FCCB8D11147B399C0F89A9F45A138BC7 /* GTMSessionFetcherService.h in Headers */ = {isa = PBXBuildFile; fileRef = 3361F9111575058DB2A8609E3DBF9B89 /* GTMSessionFetcherService.h */; settings = {ATTRIBUTES = (Public, ); }; }; + FF53FEBEA1CECB739B37F452ABE18FC2 /* GTLRFramework.m in Sources */ = {isa = PBXBuildFile; fileRef = 941A7548E49FA90C92AECE6F6F72CDD7 /* GTLRFramework.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 558BF2DE4D4799B5794C4AFA6DB7B7F7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 70193AD59F957BCD7AC6BECC549C2C8E; + remoteInfo = GoogleAPIClientForREST; + }; + 788133539002A098132EB82ED2D1EADC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EF3C6612CDE86D2A1A64BAA50E0787C3; + remoteInfo = GTMSessionFetcher; + }; + AB7DE924C39B46D36AF45A0CEF559572 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EF3C6612CDE86D2A1A64BAA50E0787C3; + remoteInfo = GTMSessionFetcher; + }; + CBA0C327941F96015A5D0C2A0F65E1D9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2B5D02F570FFD6BDFE13FF35208146B1; + remoteInfo = "Pods-TeachersAssistant"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 01D3802510F36289EF4A957360AB8B4E /* GTMSessionUploadFetcher.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTMSessionUploadFetcher.h; path = Source/GTMSessionUploadFetcher.h; sourceTree = "<group>"; }; + 029A66B7819D4FD2C4DD1328C5488419 /* Pods-TeachersAssistantTests-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-TeachersAssistantTests-umbrella.h"; sourceTree = "<group>"; }; + 031780C18D1D961B19C0925A08FD435D /* GTMSessionFetcher-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "GTMSessionFetcher-Info.plist"; sourceTree = "<group>"; }; + 085E24A1AC949B7CDB4C719B3C53EA14 /* GTLRService.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTLRService.h; path = Source/Objects/GTLRService.h; sourceTree = "<group>"; }; + 08BCC69787B382B4AACFA48AED07D11C /* Pods_TeachersAssistant.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Pods_TeachersAssistant.framework; path = "Pods-TeachersAssistant.framework"; sourceTree = BUILT_PRODUCTS_DIR; }; + 0D5E10D54CC70F2A3DD8909F38107D22 /* GTLRDateTime.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTLRDateTime.h; path = Source/Objects/GTLRDateTime.h; sourceTree = "<group>"; }; + 0F8D1B4EFF5D66AE0AC686E8D76CA5A5 /* GTMReadMonitorInputStream.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTMReadMonitorInputStream.m; path = Source/GTMReadMonitorInputStream.m; sourceTree = "<group>"; }; + 118B269F032567135C30C461CA7A23AD /* Pods-TeachersAssistant.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-TeachersAssistant.modulemap"; sourceTree = "<group>"; }; + 123565447F11349B461EAA44530EFD01 /* Pods-TeachersAssistantTests-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-TeachersAssistantTests-acknowledgements.plist"; sourceTree = "<group>"; }; + 156A1A9280D93A7463591064FCCAD02F /* GTLRBatchResult.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTLRBatchResult.m; path = Source/Objects/GTLRBatchResult.m; sourceTree = "<group>"; }; + 15B773B78B52F266A0CC624DDFFAE7EF /* GTLRErrorObject.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTLRErrorObject.h; path = Source/Objects/GTLRErrorObject.h; sourceTree = "<group>"; }; + 176E21E234D1EF72C0869C40836ECBCC /* Pods-TeachersAssistantTests-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-TeachersAssistantTests-dummy.m"; sourceTree = "<group>"; }; + 1BAC2903B8C1077CF9EECF374BCBBE01 /* GTLRUploadParameters.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTLRUploadParameters.m; path = Source/Objects/GTLRUploadParameters.m; sourceTree = "<group>"; }; + 278E2C6B9FF0A381750BFB75627FFDB3 /* GTLRRuntimeCommon.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTLRRuntimeCommon.m; path = Source/Objects/GTLRRuntimeCommon.m; sourceTree = "<group>"; }; + 28EC55CDE5ECEB600750BA9EFB6497AA /* GoogleAPIClientForREST.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = GoogleAPIClientForREST.framework; path = GoogleAPIClientForREST.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2D39543E4D61DF7D7495633B6CCB2AAB /* GTMSessionUploadFetcher.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTMSessionUploadFetcher.m; path = Source/GTMSessionUploadFetcher.m; sourceTree = "<group>"; }; + 2E013B0D53EF922E03F88FAFCBD9FF17 /* GTLRObject.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTLRObject.h; path = Source/Objects/GTLRObject.h; sourceTree = "<group>"; }; + 3361F9111575058DB2A8609E3DBF9B89 /* GTMSessionFetcherService.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTMSessionFetcherService.h; path = Source/GTMSessionFetcherService.h; sourceTree = "<group>"; }; + 35039373C75F0CAA94D50D97AF90BF12 /* Pods-TeachersAssistantTests-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-TeachersAssistantTests-acknowledgements.markdown"; sourceTree = "<group>"; }; + 35B8090078E48E239D39288A69412AD5 /* GoogleAPIClientForREST-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "GoogleAPIClientForREST-umbrella.h"; sourceTree = "<group>"; }; + 37E7B98E15AE2BF1DD55266099D7B651 /* GTLRDateTime.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTLRDateTime.m; path = Source/Objects/GTLRDateTime.m; sourceTree = "<group>"; }; + 3DFABC77294AE1B3C79891E913990217 /* Pods-TeachersAssistant-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-TeachersAssistant-umbrella.h"; sourceTree = "<group>"; }; + 44581CF76B50941A6B5755E48A3F0B73 /* Pods-TeachersAssistantTests.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-TeachersAssistantTests.modulemap"; sourceTree = "<group>"; }; + 4996B5C022E49C108C735CCD2E8FAE2B /* GTMMIMEDocument.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTMMIMEDocument.m; path = Source/GTMMIMEDocument.m; sourceTree = "<group>"; }; + 4A37FA7630E26C1BB376757F9B56D929 /* GTMSessionFetcherLogging.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTMSessionFetcherLogging.m; path = Source/GTMSessionFetcherLogging.m; sourceTree = "<group>"; }; + 4D65ABF8CCC3AF286C22D5B65FC16D8A /* GTMSessionFetcher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = GTMSessionFetcher.framework; path = GTMSessionFetcher.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 62E9F4BAC2AA0264F8658C43B4E58636 /* GTLRErrorObject.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTLRErrorObject.m; path = Source/Objects/GTLRErrorObject.m; sourceTree = "<group>"; }; + 63940FFB83E85057EEB621E27DE09111 /* GTLRService.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTLRService.m; path = Source/Objects/GTLRService.m; sourceTree = "<group>"; }; + 64AFF54F34EBE50066D296C25F3EC48F /* GTMSessionFetcher-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "GTMSessionFetcher-prefix.pch"; sourceTree = "<group>"; }; + 65F97E32937BB5235135137421037918 /* GTLRBatchResult.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTLRBatchResult.h; path = Source/Objects/GTLRBatchResult.h; sourceTree = "<group>"; }; + 66AFD895BB32492C28401C014FEA5B2A /* GTMSessionFetcherService.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTMSessionFetcherService.m; path = Source/GTMSessionFetcherService.m; sourceTree = "<group>"; }; + 6CBEAF0154F92F0CD942C90DCF6C7A1B /* Pods-TeachersAssistantTests-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-TeachersAssistantTests-Info.plist"; sourceTree = "<group>"; }; + 6CF6CDA458E493AD3A13C09451C84F01 /* Pods-TeachersAssistant-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-TeachersAssistant-acknowledgements.markdown"; sourceTree = "<group>"; }; + 6D3AE8D1C29E34EC8CE96ACB0CDED5E5 /* GTMGatherInputStream.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTMGatherInputStream.m; path = Source/GTMGatherInputStream.m; sourceTree = "<group>"; }; + 6E8AB8288409C217E896484EDEB7D484 /* GTLRDuration.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTLRDuration.h; path = Source/Objects/GTLRDuration.h; sourceTree = "<group>"; }; + 74104373A68924B948E2FD66656DDF04 /* Pods-TeachersAssistant-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-TeachersAssistant-dummy.m"; sourceTree = "<group>"; }; + 7D4EB57F9DFC922E6EC6E7FDE6B66497 /* GTLRBatchQuery.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTLRBatchQuery.h; path = Source/Objects/GTLRBatchQuery.h; sourceTree = "<group>"; }; + 80D0ABF6F63F3EF080A906F8C625412E /* Pods-TeachersAssistant.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-TeachersAssistant.debug.xcconfig"; sourceTree = "<group>"; }; + 90AE378D1FFFBA091EF3B82D4C606245 /* GoogleAPIClientForREST-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "GoogleAPIClientForREST-prefix.pch"; sourceTree = "<group>"; }; + 9122A91940337F4A5E3FE7E589B77E2B /* GTLRBase64.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTLRBase64.m; path = Source/Utilities/GTLRBase64.m; sourceTree = "<group>"; }; + 941A7548E49FA90C92AECE6F6F72CDD7 /* GTLRFramework.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTLRFramework.m; path = Source/Utilities/GTLRFramework.m; sourceTree = "<group>"; }; + 961467D2BF26CCA63537C0D3487B1DB8 /* GTLRUtilities.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTLRUtilities.h; path = Source/Utilities/GTLRUtilities.h; sourceTree = "<group>"; }; + 96C607A0030873A86250384D32E69039 /* GoogleAPIClientForREST.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = GoogleAPIClientForREST.modulemap; sourceTree = "<group>"; }; + 9B56C5D0A1A2E8BCAF6EC0BFE4BD9FB6 /* GTLRRuntimeCommon.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTLRRuntimeCommon.h; path = Source/Objects/GTLRRuntimeCommon.h; sourceTree = "<group>"; }; + 9B8F6CCC3AB370F4B07F5B1EFFF4DD0E /* GoogleAPIClientForREST-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "GoogleAPIClientForREST-dummy.m"; sourceTree = "<group>"; }; + 9BA885921F7C975C7E54FC245579DCDF /* GTLRUtilities.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTLRUtilities.m; path = Source/Utilities/GTLRUtilities.m; sourceTree = "<group>"; }; + 9D0C1C1A79A5F7F06B97A360EEF3274C /* GTLRURITemplate.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTLRURITemplate.m; path = Source/Utilities/GTLRURITemplate.m; sourceTree = "<group>"; }; + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + 9F361C9292422B96C48EEA211001CD82 /* GTMReadMonitorInputStream.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTMReadMonitorInputStream.h; path = Source/GTMReadMonitorInputStream.h; sourceTree = "<group>"; }; + A174B22A98E274B816F8496BCC54DAB4 /* GTMSessionFetcher.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTMSessionFetcher.m; path = Source/GTMSessionFetcher.m; sourceTree = "<group>"; }; + A40C263DBAD4C39D2E09F730467D5085 /* Pods-TeachersAssistantTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-TeachersAssistantTests.debug.xcconfig"; sourceTree = "<group>"; }; + A61A5FBB4C4917E85820D47EA05A7A31 /* Pods-TeachersAssistant.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-TeachersAssistant.release.xcconfig"; sourceTree = "<group>"; }; + A7C69C428FB7737AC8AB7F9954BA14AF /* GTMSessionFetcher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GTMSessionFetcher.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A91F81008C5187F2E124CB8EC5EA8024 /* GTMSessionFetcher.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = GTMSessionFetcher.xcconfig; sourceTree = "<group>"; }; + ABBEE353D5FCEB9ED26E64872A4AB2B6 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.0.sdk/System/Library/Frameworks/Security.framework; sourceTree = DEVELOPER_DIR; }; + B141A2B8E0C6A4286A1DA134E884BE97 /* Pods_TeachersAssistantTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Pods_TeachersAssistantTests.framework; path = "Pods-TeachersAssistantTests.framework"; sourceTree = BUILT_PRODUCTS_DIR; }; + B390CCF062FC2081F94C3B2C3E894ACC /* GTLRBatchQuery.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTLRBatchQuery.m; path = Source/Objects/GTLRBatchQuery.m; sourceTree = "<group>"; }; + B50337BA0606A576144D70B0E05ED8C1 /* Pods-TeachersAssistant-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-TeachersAssistant-Info.plist"; sourceTree = "<group>"; }; + B9CE91C5B6E4E8D7B8A7E5FEC3AEB4BC /* GTLRQuery.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTLRQuery.h; path = Source/Objects/GTLRQuery.h; sourceTree = "<group>"; }; + BF6932F6AD90C0858CB4BD08B122853D /* GTMSessionFetcher-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "GTMSessionFetcher-umbrella.h"; sourceTree = "<group>"; }; + C4CD076CCAAC4DDE1AD8F8D908810C14 /* GTLRBase64.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTLRBase64.h; path = Source/Utilities/GTLRBase64.h; sourceTree = "<group>"; }; + CB7EA1458FDD080BFADB7A928D369059 /* GTLRObject.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTLRObject.m; path = Source/Objects/GTLRObject.m; sourceTree = "<group>"; }; + D036D2B7B2CE7D2BB207A2A4849EAA58 /* GoogleAPIClientForREST-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "GoogleAPIClientForREST-Info.plist"; sourceTree = "<group>"; }; + D2C38B5767FC35488427CDB72E1252BE /* Pods-TeachersAssistant-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-TeachersAssistant-acknowledgements.plist"; sourceTree = "<group>"; }; + D96F98D3386360D1DDE0C7ACF9E76014 /* GTMSessionFetcher.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTMSessionFetcher.h; path = Source/GTMSessionFetcher.h; sourceTree = "<group>"; }; + DA0FCEE372F04B2A01DB11789130BC5B /* GTMSessionFetcherLogging.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTMSessionFetcherLogging.h; path = Source/GTMSessionFetcherLogging.h; sourceTree = "<group>"; }; + DA49BC58BF18A35373D3EA2DBF421D00 /* GTLRDuration.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTLRDuration.m; path = Source/Objects/GTLRDuration.m; sourceTree = "<group>"; }; + E3726C85C7B6D56594A3E93E16CB8CD7 /* GTLRFramework.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTLRFramework.h; path = Source/Utilities/GTLRFramework.h; sourceTree = "<group>"; }; + E4C698A16FDC77A62625C9DA6165582D /* GTLRDefines.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTLRDefines.h; path = Source/GTLRDefines.h; sourceTree = "<group>"; }; + E9E89C72B14FF0804BDF968943B9430E /* GTLRUploadParameters.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTLRUploadParameters.h; path = Source/Objects/GTLRUploadParameters.h; sourceTree = "<group>"; }; + EC32701EAC620041889026F257CF930A /* GTMSessionFetcher-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "GTMSessionFetcher-dummy.m"; sourceTree = "<group>"; }; + EF176B2AA0FA643A494621B8C192EDB5 /* GTMGatherInputStream.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTMGatherInputStream.h; path = Source/GTMGatherInputStream.h; sourceTree = "<group>"; }; + F529CA2B381C222BE9ECC27FA5F94111 /* Pods-TeachersAssistant-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-TeachersAssistant-frameworks.sh"; sourceTree = "<group>"; }; + F60610D99772BFEF6876443BB367B406 /* GTLRQuery.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GTLRQuery.m; path = Source/Objects/GTLRQuery.m; sourceTree = "<group>"; }; + F77C7F83EE5CFC28448551424FFA648B /* GTLRURITemplate.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTLRURITemplate.h; path = Source/Utilities/GTLRURITemplate.h; sourceTree = "<group>"; }; + F7C39F533C62291956B73FFBAC3BEA6A /* GoogleAPIClientForREST.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = GoogleAPIClientForREST.xcconfig; sourceTree = "<group>"; }; + FAB365696B9D3A38472FAC11B630C9CA /* GTMSessionFetcher.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = GTMSessionFetcher.modulemap; sourceTree = "<group>"; }; + FCA4CC8015A178888F4A57DDED67433A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + FD8BE7C44C98327BFA930F0F6F23D72A /* GTMMIMEDocument.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GTMMIMEDocument.h; path = Source/GTMMIMEDocument.h; sourceTree = "<group>"; }; + FE024B7689A72CA8CC54C9B407F9507C /* Pods-TeachersAssistantTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-TeachersAssistantTests.release.xcconfig"; sourceTree = "<group>"; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 054C7BD617B0509F6CFA3658F9ADA4AD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B2F27E4AAC32FDEF59BAE37A9027501 /* Foundation.framework in Frameworks */, + 47E2FBDB54550F73850F0B39FABAC84C /* GTMSessionFetcher.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1CC1732E7605318A00DDBDB18FDDD2A8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 87F7C625A44A243746CFA5568B05E023 /* Foundation.framework in Frameworks */, + EA6D1F0D9C9CB43E1C240B7052B65CC5 /* Security.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 42AFFA9AC72D8A2FF83C883E890B5F2A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F0F798FAF8F64C21D8A06294F9CCF82B /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84EFDE6B861DAF6A18F6CEE6ACE05591 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7260CEF6719019E19D1A0D894188B05F /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0FBF9B71E9B8C5E178D8C2E26CE101D7 /* Pods-TeachersAssistantTests */ = { + isa = PBXGroup; + children = ( + 44581CF76B50941A6B5755E48A3F0B73 /* Pods-TeachersAssistantTests.modulemap */, + 35039373C75F0CAA94D50D97AF90BF12 /* Pods-TeachersAssistantTests-acknowledgements.markdown */, + 123565447F11349B461EAA44530EFD01 /* Pods-TeachersAssistantTests-acknowledgements.plist */, + 176E21E234D1EF72C0869C40836ECBCC /* Pods-TeachersAssistantTests-dummy.m */, + 6CBEAF0154F92F0CD942C90DCF6C7A1B /* Pods-TeachersAssistantTests-Info.plist */, + 029A66B7819D4FD2C4DD1328C5488419 /* Pods-TeachersAssistantTests-umbrella.h */, + A40C263DBAD4C39D2E09F730467D5085 /* Pods-TeachersAssistantTests.debug.xcconfig */, + FE024B7689A72CA8CC54C9B407F9507C /* Pods-TeachersAssistantTests.release.xcconfig */, + ); + name = "Pods-TeachersAssistantTests"; + path = "Target Support Files/Pods-TeachersAssistantTests"; + sourceTree = "<group>"; + }; + 46A909CE8B6AE14585E6AE200FB49F59 /* Support Files */ = { + isa = PBXGroup; + children = ( + 96C607A0030873A86250384D32E69039 /* GoogleAPIClientForREST.modulemap */, + F7C39F533C62291956B73FFBAC3BEA6A /* GoogleAPIClientForREST.xcconfig */, + 9B8F6CCC3AB370F4B07F5B1EFFF4DD0E /* GoogleAPIClientForREST-dummy.m */, + D036D2B7B2CE7D2BB207A2A4849EAA58 /* GoogleAPIClientForREST-Info.plist */, + 90AE378D1FFFBA091EF3B82D4C606245 /* GoogleAPIClientForREST-prefix.pch */, + 35B8090078E48E239D39288A69412AD5 /* GoogleAPIClientForREST-umbrella.h */, + ); + name = "Support Files"; + path = "../Target Support Files/GoogleAPIClientForREST"; + sourceTree = "<group>"; + }; + 4ADD1838B1ABF81938826238AA61140A /* Products */ = { + isa = PBXGroup; + children = ( + 28EC55CDE5ECEB600750BA9EFB6497AA /* GoogleAPIClientForREST.framework */, + 4D65ABF8CCC3AF286C22D5B65FC16D8A /* GTMSessionFetcher.framework */, + 08BCC69787B382B4AACFA48AED07D11C /* Pods_TeachersAssistant.framework */, + B141A2B8E0C6A4286A1DA134E884BE97 /* Pods_TeachersAssistantTests.framework */, + ); + name = Products; + sourceTree = "<group>"; + }; + 4DEA211B00F10A3BE6AAB7ADFFA1162E /* Frameworks */ = { + isa = PBXGroup; + children = ( + A7C69C428FB7737AC8AB7F9954BA14AF /* GTMSessionFetcher.framework */, + FC1A34E42428A1E2D0FCA8C6E6B9F499 /* iOS */, + ); + name = Frameworks; + sourceTree = "<group>"; + }; + 53D26FAC150F45E3579F8607BA8BAD03 /* GTMSessionFetcher */ = { + isa = PBXGroup; + children = ( + B901D7A314C7DD3767801F689D45D7C8 /* Core */, + A39622AC4E4F6CDEE3A4E39EE5C74076 /* Full */, + AAE5785F77D8783FEB319A17F7E1E960 /* Support Files */, + ); + name = GTMSessionFetcher; + path = GTMSessionFetcher; + sourceTree = "<group>"; + }; + 562E31A1C1FB3E3474A483B9DF11DE2D /* Pods */ = { + isa = PBXGroup; + children = ( + 7302224407A36BAF248FB955CABB9FEA /* GoogleAPIClientForREST */, + 53D26FAC150F45E3579F8607BA8BAD03 /* GTMSessionFetcher */, + ); + name = Pods; + sourceTree = "<group>"; + }; + 7302224407A36BAF248FB955CABB9FEA /* GoogleAPIClientForREST */ = { + isa = PBXGroup; + children = ( + 9A2F3797269678F3B58E933D3BAEC8FC /* Core */, + 46A909CE8B6AE14585E6AE200FB49F59 /* Support Files */, + ); + name = GoogleAPIClientForREST; + path = GoogleAPIClientForREST; + sourceTree = "<group>"; + }; + 9A2F3797269678F3B58E933D3BAEC8FC /* Core */ = { + isa = PBXGroup; + children = ( + C4CD076CCAAC4DDE1AD8F8D908810C14 /* GTLRBase64.h */, + 9122A91940337F4A5E3FE7E589B77E2B /* GTLRBase64.m */, + 7D4EB57F9DFC922E6EC6E7FDE6B66497 /* GTLRBatchQuery.h */, + B390CCF062FC2081F94C3B2C3E894ACC /* GTLRBatchQuery.m */, + 65F97E32937BB5235135137421037918 /* GTLRBatchResult.h */, + 156A1A9280D93A7463591064FCCAD02F /* GTLRBatchResult.m */, + 0D5E10D54CC70F2A3DD8909F38107D22 /* GTLRDateTime.h */, + 37E7B98E15AE2BF1DD55266099D7B651 /* GTLRDateTime.m */, + E4C698A16FDC77A62625C9DA6165582D /* GTLRDefines.h */, + 6E8AB8288409C217E896484EDEB7D484 /* GTLRDuration.h */, + DA49BC58BF18A35373D3EA2DBF421D00 /* GTLRDuration.m */, + 15B773B78B52F266A0CC624DDFFAE7EF /* GTLRErrorObject.h */, + 62E9F4BAC2AA0264F8658C43B4E58636 /* GTLRErrorObject.m */, + E3726C85C7B6D56594A3E93E16CB8CD7 /* GTLRFramework.h */, + 941A7548E49FA90C92AECE6F6F72CDD7 /* GTLRFramework.m */, + 2E013B0D53EF922E03F88FAFCBD9FF17 /* GTLRObject.h */, + CB7EA1458FDD080BFADB7A928D369059 /* GTLRObject.m */, + B9CE91C5B6E4E8D7B8A7E5FEC3AEB4BC /* GTLRQuery.h */, + F60610D99772BFEF6876443BB367B406 /* GTLRQuery.m */, + 9B56C5D0A1A2E8BCAF6EC0BFE4BD9FB6 /* GTLRRuntimeCommon.h */, + 278E2C6B9FF0A381750BFB75627FFDB3 /* GTLRRuntimeCommon.m */, + 085E24A1AC949B7CDB4C719B3C53EA14 /* GTLRService.h */, + 63940FFB83E85057EEB621E27DE09111 /* GTLRService.m */, + E9E89C72B14FF0804BDF968943B9430E /* GTLRUploadParameters.h */, + 1BAC2903B8C1077CF9EECF374BCBBE01 /* GTLRUploadParameters.m */, + F77C7F83EE5CFC28448551424FFA648B /* GTLRURITemplate.h */, + 9D0C1C1A79A5F7F06B97A360EEF3274C /* GTLRURITemplate.m */, + 961467D2BF26CCA63537C0D3487B1DB8 /* GTLRUtilities.h */, + 9BA885921F7C975C7E54FC245579DCDF /* GTLRUtilities.m */, + ); + name = Core; + sourceTree = "<group>"; + }; + A39622AC4E4F6CDEE3A4E39EE5C74076 /* Full */ = { + isa = PBXGroup; + children = ( + EF176B2AA0FA643A494621B8C192EDB5 /* GTMGatherInputStream.h */, + 6D3AE8D1C29E34EC8CE96ACB0CDED5E5 /* GTMGatherInputStream.m */, + FD8BE7C44C98327BFA930F0F6F23D72A /* GTMMIMEDocument.h */, + 4996B5C022E49C108C735CCD2E8FAE2B /* GTMMIMEDocument.m */, + 9F361C9292422B96C48EEA211001CD82 /* GTMReadMonitorInputStream.h */, + 0F8D1B4EFF5D66AE0AC686E8D76CA5A5 /* GTMReadMonitorInputStream.m */, + ); + name = Full; + sourceTree = "<group>"; + }; + AAE5785F77D8783FEB319A17F7E1E960 /* Support Files */ = { + isa = PBXGroup; + children = ( + FAB365696B9D3A38472FAC11B630C9CA /* GTMSessionFetcher.modulemap */, + A91F81008C5187F2E124CB8EC5EA8024 /* GTMSessionFetcher.xcconfig */, + EC32701EAC620041889026F257CF930A /* GTMSessionFetcher-dummy.m */, + 031780C18D1D961B19C0925A08FD435D /* GTMSessionFetcher-Info.plist */, + 64AFF54F34EBE50066D296C25F3EC48F /* GTMSessionFetcher-prefix.pch */, + BF6932F6AD90C0858CB4BD08B122853D /* GTMSessionFetcher-umbrella.h */, + ); + name = "Support Files"; + path = "../Target Support Files/GTMSessionFetcher"; + sourceTree = "<group>"; + }; + AEB6E5ED4592D8BEF6808CBB5ABD44EA /* Pods-TeachersAssistant */ = { + isa = PBXGroup; + children = ( + 118B269F032567135C30C461CA7A23AD /* Pods-TeachersAssistant.modulemap */, + 6CF6CDA458E493AD3A13C09451C84F01 /* Pods-TeachersAssistant-acknowledgements.markdown */, + D2C38B5767FC35488427CDB72E1252BE /* Pods-TeachersAssistant-acknowledgements.plist */, + 74104373A68924B948E2FD66656DDF04 /* Pods-TeachersAssistant-dummy.m */, + F529CA2B381C222BE9ECC27FA5F94111 /* Pods-TeachersAssistant-frameworks.sh */, + B50337BA0606A576144D70B0E05ED8C1 /* Pods-TeachersAssistant-Info.plist */, + 3DFABC77294AE1B3C79891E913990217 /* Pods-TeachersAssistant-umbrella.h */, + 80D0ABF6F63F3EF080A906F8C625412E /* Pods-TeachersAssistant.debug.xcconfig */, + A61A5FBB4C4917E85820D47EA05A7A31 /* Pods-TeachersAssistant.release.xcconfig */, + ); + name = "Pods-TeachersAssistant"; + path = "Target Support Files/Pods-TeachersAssistant"; + sourceTree = "<group>"; + }; + B901D7A314C7DD3767801F689D45D7C8 /* Core */ = { + isa = PBXGroup; + children = ( + D96F98D3386360D1DDE0C7ACF9E76014 /* GTMSessionFetcher.h */, + A174B22A98E274B816F8496BCC54DAB4 /* GTMSessionFetcher.m */, + DA0FCEE372F04B2A01DB11789130BC5B /* GTMSessionFetcherLogging.h */, + 4A37FA7630E26C1BB376757F9B56D929 /* GTMSessionFetcherLogging.m */, + 3361F9111575058DB2A8609E3DBF9B89 /* GTMSessionFetcherService.h */, + 66AFD895BB32492C28401C014FEA5B2A /* GTMSessionFetcherService.m */, + 01D3802510F36289EF4A957360AB8B4E /* GTMSessionUploadFetcher.h */, + 2D39543E4D61DF7D7495633B6CCB2AAB /* GTMSessionUploadFetcher.m */, + ); + name = Core; + sourceTree = "<group>"; + }; + CF1408CF629C7361332E53B88F7BD30C = { + isa = PBXGroup; + children = ( + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */, + 4DEA211B00F10A3BE6AAB7ADFFA1162E /* Frameworks */, + 562E31A1C1FB3E3474A483B9DF11DE2D /* Pods */, + 4ADD1838B1ABF81938826238AA61140A /* Products */, + E201F6F1A7795FB7DB590924C510BDB4 /* Targets Support Files */, + ); + sourceTree = "<group>"; + }; + E201F6F1A7795FB7DB590924C510BDB4 /* Targets Support Files */ = { + isa = PBXGroup; + children = ( + AEB6E5ED4592D8BEF6808CBB5ABD44EA /* Pods-TeachersAssistant */, + 0FBF9B71E9B8C5E178D8C2E26CE101D7 /* Pods-TeachersAssistantTests */, + ); + name = "Targets Support Files"; + sourceTree = "<group>"; + }; + FC1A34E42428A1E2D0FCA8C6E6B9F499 /* iOS */ = { + isa = PBXGroup; + children = ( + FCA4CC8015A178888F4A57DDED67433A /* Foundation.framework */, + ABBEE353D5FCEB9ED26E64872A4AB2B6 /* Security.framework */, + ); + name = iOS; + sourceTree = "<group>"; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 3BB78E3A97AF72FDFF27F408456FB45F /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 4CF2FAAEE1D988441F281430D392B6BF /* GTMGatherInputStream.h in Headers */, + 89D083B4302DD4BFEFD5E4EC85350D77 /* GTMMIMEDocument.h in Headers */, + E0CB35D900D10D1C7E37F987ABFF8938 /* GTMReadMonitorInputStream.h in Headers */, + E982A92F18C647036E12507A4A4389C1 /* GTMSessionFetcher-umbrella.h in Headers */, + 91BF7F097ACC8FD1F9BF6248F400719A /* GTMSessionFetcher.h in Headers */, + 877465AAAEA86697CEC56A0CC9F0AA43 /* GTMSessionFetcherLogging.h in Headers */, + FCCB8D11147B399C0F89A9F45A138BC7 /* GTMSessionFetcherService.h in Headers */, + DCE0FED8DC34E000ED3EFA06C77CD76C /* GTMSessionUploadFetcher.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6BE7497F191CA5AF4A354E7BEEEC0105 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + C5CE0F21B7E068FD8FBAADC46F7EA854 /* Pods-TeachersAssistant-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C8D68C37F05F2B18CA59022ABEEAF76D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 6C571A236A1EBAA0111698DE1FC4D0A4 /* Pods-TeachersAssistantTests-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F57B334B60CEEAA7B2C1B79D66162B36 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 5F50C32122B3ADA9EA0D5CE4ABEFDD78 /* GoogleAPIClientForREST-umbrella.h in Headers */, + 1E7061DAA1598078FD6F7467039FD69A /* GTLRBase64.h in Headers */, + 231E72C5222861B3A2749B5999FCCD3B /* GTLRBatchQuery.h in Headers */, + 143CA95BCA38E98A1C953F12ED7A68C5 /* GTLRBatchResult.h in Headers */, + 45B0D2B682D7C862E294F1467980E2E3 /* GTLRDateTime.h in Headers */, + 4B4ED067E7A6A0E4102A77EF6E12B6C5 /* GTLRDefines.h in Headers */, + CB4E2DCF06B3563AE98E03932A5D80A0 /* GTLRDuration.h in Headers */, + AD645B60AF3D1B463ECE1BCC70209B7D /* GTLRErrorObject.h in Headers */, + B58498DCE67D2464A50F18A12F0C6FC8 /* GTLRFramework.h in Headers */, + 62B708568BAC8B6D5C9084BEC87B90E4 /* GTLRObject.h in Headers */, + D1F90521F48C8F6A488BC10751B720E1 /* GTLRQuery.h in Headers */, + FCB136E76490B4268311A8230BA408EA /* GTLRRuntimeCommon.h in Headers */, + 62DB18E73656128001751C0A2F73648B /* GTLRService.h in Headers */, + C08BC708C768AB5D4A7759073374C09A /* GTLRUploadParameters.h in Headers */, + 790C8B1BED5D639EADE6BD6E45C9E8F5 /* GTLRURITemplate.h in Headers */, + 305C5C21B3895A0E26391E09987C2385 /* GTLRUtilities.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 2B5D02F570FFD6BDFE13FF35208146B1 /* Pods-TeachersAssistant */ = { + isa = PBXNativeTarget; + buildConfigurationList = 738722911C991930027B35774521B7F9 /* Build configuration list for PBXNativeTarget "Pods-TeachersAssistant" */; + buildPhases = ( + 6BE7497F191CA5AF4A354E7BEEEC0105 /* Headers */, + 47F618A83F8527BFEAF513CB44C26867 /* Sources */, + 84EFDE6B861DAF6A18F6CEE6ACE05591 /* Frameworks */, + 242EC4AB351D90DCC21C39E1F9B1FD68 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 062F472295E0FB6D7AEA72EAA2A54156 /* PBXTargetDependency */, + FAA4071411D935EB8CCDAD0B3F25F868 /* PBXTargetDependency */, + ); + name = "Pods-TeachersAssistant"; + productName = "Pods-TeachersAssistant"; + productReference = 08BCC69787B382B4AACFA48AED07D11C /* Pods_TeachersAssistant.framework */; + productType = "com.apple.product-type.framework"; + }; + 70193AD59F957BCD7AC6BECC549C2C8E /* GoogleAPIClientForREST */ = { + isa = PBXNativeTarget; + buildConfigurationList = 280AF7B639A1AE6C203ADF301996EC83 /* Build configuration list for PBXNativeTarget "GoogleAPIClientForREST" */; + buildPhases = ( + F57B334B60CEEAA7B2C1B79D66162B36 /* Headers */, + F4A7D16270832B0EC1CD7126AC4291C8 /* Sources */, + 054C7BD617B0509F6CFA3658F9ADA4AD /* Frameworks */, + 91E9E7E50119719FC109E982FE76042C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7805F743CD08543910A00715F8F6921A /* PBXTargetDependency */, + ); + name = GoogleAPIClientForREST; + productName = GoogleAPIClientForREST; + productReference = 28EC55CDE5ECEB600750BA9EFB6497AA /* GoogleAPIClientForREST.framework */; + productType = "com.apple.product-type.framework"; + }; + 9115383C4EEFD422CC2FA6BC0AEEF0EE /* Pods-TeachersAssistantTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 34A3E21F8EBDF4849164D45564BF83D3 /* Build configuration list for PBXNativeTarget "Pods-TeachersAssistantTests" */; + buildPhases = ( + C8D68C37F05F2B18CA59022ABEEAF76D /* Headers */, + F85DC9EE210197B435EAE6DA39B0DCB6 /* Sources */, + 42AFFA9AC72D8A2FF83C883E890B5F2A /* Frameworks */, + 0CFAA73F65795CED4B9F45F1FE7E9EAA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + EB4921EFA7B1ACE9CB14B01E8D6AA7B9 /* PBXTargetDependency */, + ); + name = "Pods-TeachersAssistantTests"; + productName = "Pods-TeachersAssistantTests"; + productReference = B141A2B8E0C6A4286A1DA134E884BE97 /* Pods_TeachersAssistantTests.framework */; + productType = "com.apple.product-type.framework"; + }; + EF3C6612CDE86D2A1A64BAA50E0787C3 /* GTMSessionFetcher */ = { + isa = PBXNativeTarget; + buildConfigurationList = F69C5E3EF0EDB7B90F7ACAC42BB21610 /* Build configuration list for PBXNativeTarget "GTMSessionFetcher" */; + buildPhases = ( + 3BB78E3A97AF72FDFF27F408456FB45F /* Headers */, + C3138F29F9B820D51CE987A72EAFF7C7 /* Sources */, + 1CC1732E7605318A00DDBDB18FDDD2A8 /* Frameworks */, + 50BECB9E27710FF42974E7AE9A6E7F43 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = GTMSessionFetcher; + productName = GTMSessionFetcher; + productReference = 4D65ABF8CCC3AF286C22D5B65FC16D8A /* GTMSessionFetcher.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BFDFE7DC352907FC980B868725387E98 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0930; + LastUpgradeCheck = 0930; + }; + buildConfigurationList = 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = CF1408CF629C7361332E53B88F7BD30C; + productRefGroup = 4ADD1838B1ABF81938826238AA61140A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 70193AD59F957BCD7AC6BECC549C2C8E /* GoogleAPIClientForREST */, + EF3C6612CDE86D2A1A64BAA50E0787C3 /* GTMSessionFetcher */, + 2B5D02F570FFD6BDFE13FF35208146B1 /* Pods-TeachersAssistant */, + 9115383C4EEFD422CC2FA6BC0AEEF0EE /* Pods-TeachersAssistantTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 0CFAA73F65795CED4B9F45F1FE7E9EAA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 242EC4AB351D90DCC21C39E1F9B1FD68 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 50BECB9E27710FF42974E7AE9A6E7F43 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 91E9E7E50119719FC109E982FE76042C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 47F618A83F8527BFEAF513CB44C26867 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B3073DFBB5CB806BCFD875B3A5D949BB /* Pods-TeachersAssistant-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C3138F29F9B820D51CE987A72EAFF7C7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 66C7544B5A09F539D7D4DB6F937F61B0 /* GTMGatherInputStream.m in Sources */, + E43064D55A1482C00F38585873D6BB48 /* GTMMIMEDocument.m in Sources */, + CB4139747C70971C4984E7511A20146E /* GTMReadMonitorInputStream.m in Sources */, + EBB7AAAB95C04DC7F4B473EB68F1FD84 /* GTMSessionFetcher-dummy.m in Sources */, + F19CCC1BE7E7EFF05FE57EBE25F21414 /* GTMSessionFetcher.m in Sources */, + 05F189E1E5B58CD99B4BD4EF466BC28F /* GTMSessionFetcherLogging.m in Sources */, + 0259933BCBC56AA6C06A6C9965DAF0B1 /* GTMSessionFetcherService.m in Sources */, + C0F61C910AA6E3457E18FBD400862107 /* GTMSessionUploadFetcher.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F4A7D16270832B0EC1CD7126AC4291C8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7F46C4E77AF7E4C2F9751A5154F7BFEB /* GoogleAPIClientForREST-dummy.m in Sources */, + BE578FD68DD6E0A6369DE0A0A5404B6F /* GTLRBase64.m in Sources */, + 98FC93E3E80044F01349D3BBDC818CC0 /* GTLRBatchQuery.m in Sources */, + 2B77711505B6A35910051173A84BECF0 /* GTLRBatchResult.m in Sources */, + 4CDF43FD9B915EF76C6A771BA4D2C0A4 /* GTLRDateTime.m in Sources */, + 27ADCD707AC02EE76FB6A8D9E2DA2BCB /* GTLRDuration.m in Sources */, + BBFBB062310CB7A6DBD5A9C82310C5D4 /* GTLRErrorObject.m in Sources */, + FF53FEBEA1CECB739B37F452ABE18FC2 /* GTLRFramework.m in Sources */, + AD4147888DF506ACFCCE3FA5819A425C /* GTLRObject.m in Sources */, + 0BD74743A49183F02ED5A060896D395C /* GTLRQuery.m in Sources */, + 382F74A3FBE759251A5B6AB8868A4765 /* GTLRRuntimeCommon.m in Sources */, + EA93FC0BAF02B8E5D9CA7137B8F35AD9 /* GTLRService.m in Sources */, + 25018FB43685B12C22C192ACA8B195DF /* GTLRUploadParameters.m in Sources */, + DA7B909C6F910C653E12B053843E424E /* GTLRURITemplate.m in Sources */, + 825E4CB2EA00517293F381A582600B6A /* GTLRUtilities.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F85DC9EE210197B435EAE6DA39B0DCB6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0A84CCA22F1C8173E6B37F2069950093 /* Pods-TeachersAssistantTests-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 062F472295E0FB6D7AEA72EAA2A54156 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = GTMSessionFetcher; + target = EF3C6612CDE86D2A1A64BAA50E0787C3 /* GTMSessionFetcher */; + targetProxy = AB7DE924C39B46D36AF45A0CEF559572 /* PBXContainerItemProxy */; + }; + 7805F743CD08543910A00715F8F6921A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = GTMSessionFetcher; + target = EF3C6612CDE86D2A1A64BAA50E0787C3 /* GTMSessionFetcher */; + targetProxy = 788133539002A098132EB82ED2D1EADC /* PBXContainerItemProxy */; + }; + EB4921EFA7B1ACE9CB14B01E8D6AA7B9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "Pods-TeachersAssistant"; + target = 2B5D02F570FFD6BDFE13FF35208146B1 /* Pods-TeachersAssistant */; + targetProxy = CBA0C327941F96015A5D0C2A0F65E1D9 /* PBXContainerItemProxy */; + }; + FAA4071411D935EB8CCDAD0B3F25F868 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = GoogleAPIClientForREST; + target = 70193AD59F957BCD7AC6BECC549C2C8E /* GoogleAPIClientForREST */; + targetProxy = 558BF2DE4D4799B5794C4AFA6DB7B7F7 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 026F7DC614C89E01C44B24058B56A1D5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F7C39F533C62291956B73FFBAC3BEA6A /* GoogleAPIClientForREST.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MODULEMAP_FILE = "Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST.modulemap"; + PRODUCT_MODULE_NAME = GoogleAPIClientForREST; + PRODUCT_NAME = GoogleAPIClientForREST; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 07488D4657FB0A78086563621D425F8A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_DEBUG=1", + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Debug; + }; + 46992557C544161799C77D65D9095352 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F7C39F533C62291956B73FFBAC3BEA6A /* GoogleAPIClientForREST.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MODULEMAP_FILE = "Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST.modulemap"; + PRODUCT_MODULE_NAME = GoogleAPIClientForREST; + PRODUCT_NAME = GoogleAPIClientForREST; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 6BA1E5F08380297D3623BD359BB4E6CC /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A91F81008C5187F2E124CB8EC5EA8024 /* GTMSessionFetcher.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/GTMSessionFetcher/GTMSessionFetcher-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/GTMSessionFetcher/GTMSessionFetcher-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MODULEMAP_FILE = "Target Support Files/GTMSessionFetcher/GTMSessionFetcher.modulemap"; + PRODUCT_MODULE_NAME = GTMSessionFetcher; + PRODUCT_NAME = GTMSessionFetcher; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 98BEBE48302FFCD74988E05AE3ECA56B /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A61A5FBB4C4917E85820D47EA05A7A31 /* Pods-TeachersAssistant.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + A1962E6FF39BBAC201A2E5DDF99557DF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_RELEASE=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 4.2; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Release; + }; + A91C1089EC051EA16D3436CB75BBDD4E /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A40C263DBAD4C39D2E09F730467D5085 /* Pods-TeachersAssistantTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + A93163473AD32534E87956099A47F219 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A91F81008C5187F2E124CB8EC5EA8024 /* GTMSessionFetcher.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/GTMSessionFetcher/GTMSessionFetcher-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/GTMSessionFetcher/GTMSessionFetcher-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MODULEMAP_FILE = "Target Support Files/GTMSessionFetcher/GTMSessionFetcher.modulemap"; + PRODUCT_MODULE_NAME = GTMSessionFetcher; + PRODUCT_NAME = GTMSessionFetcher; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + A9F245220578F41B9636AC6B1C6372B2 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 80D0ABF6F63F3EF080A906F8C625412E /* Pods-TeachersAssistant.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + F045264B613674F3119ECB97E01F2E61 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FE024B7689A72CA8CC54C9B407F9507C /* Pods-TeachersAssistantTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 280AF7B639A1AE6C203ADF301996EC83 /* Build configuration list for PBXNativeTarget "GoogleAPIClientForREST" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 026F7DC614C89E01C44B24058B56A1D5 /* Debug */, + 46992557C544161799C77D65D9095352 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 34A3E21F8EBDF4849164D45564BF83D3 /* Build configuration list for PBXNativeTarget "Pods-TeachersAssistantTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A91C1089EC051EA16D3436CB75BBDD4E /* Debug */, + F045264B613674F3119ECB97E01F2E61 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 07488D4657FB0A78086563621D425F8A /* Debug */, + A1962E6FF39BBAC201A2E5DDF99557DF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 738722911C991930027B35774521B7F9 /* Build configuration list for PBXNativeTarget "Pods-TeachersAssistant" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A9F245220578F41B9636AC6B1C6372B2 /* Debug */, + 98BEBE48302FFCD74988E05AE3ECA56B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F69C5E3EF0EDB7B90F7ACAC42BB21610 /* Build configuration list for PBXNativeTarget "GTMSessionFetcher" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6BA1E5F08380297D3623BD359BB4E6CC /* Debug */, + A93163473AD32534E87956099A47F219 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BFDFE7DC352907FC980B868725387E98 /* Project object */; +} diff --git a/Pods/Pods.xcodeproj/xcuserdata/naomi.xcuserdatad/xcschemes/GTMSessionFetcher.xcscheme b/Pods/Pods.xcodeproj/xcuserdata/naomi.xcuserdatad/xcschemes/GTMSessionFetcher.xcscheme @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "0930" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForAnalyzing = "YES" + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "EF3C6612CDE86D2A1A64BAA50E0787C3" + BuildableName = "GTMSessionFetcher.framework" + BlueprintName = "GTMSessionFetcher" + ReferencedContainer = "container:Pods.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES" + buildConfiguration = "Debug"> + <AdditionalOptions> + </AdditionalOptions> + </TestAction> + <LaunchAction + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + buildConfiguration = "Debug" + allowLocationSimulation = "YES"> + <AdditionalOptions> + </AdditionalOptions> + </LaunchAction> + <ProfileAction + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES" + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES"> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/Pods/Pods.xcodeproj/xcuserdata/naomi.xcuserdatad/xcschemes/GoogleAPIClientForREST.xcscheme b/Pods/Pods.xcodeproj/xcuserdata/naomi.xcuserdatad/xcschemes/GoogleAPIClientForREST.xcscheme @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "0930" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForAnalyzing = "YES" + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "70193AD59F957BCD7AC6BECC549C2C8E" + BuildableName = "GoogleAPIClientForREST.framework" + BlueprintName = "GoogleAPIClientForREST" + ReferencedContainer = "container:Pods.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES" + buildConfiguration = "Debug"> + <AdditionalOptions> + </AdditionalOptions> + </TestAction> + <LaunchAction + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + buildConfiguration = "Debug" + allowLocationSimulation = "YES"> + <AdditionalOptions> + </AdditionalOptions> + </LaunchAction> + <ProfileAction + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES" + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES"> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/Pods/Pods.xcodeproj/xcuserdata/naomi.xcuserdatad/xcschemes/Pods-TeachersAssistant.xcscheme b/Pods/Pods.xcodeproj/xcuserdata/naomi.xcuserdatad/xcschemes/Pods-TeachersAssistant.xcscheme @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "0930" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "2B5D02F570FFD6BDFE13FF35208146B1" + BuildableName = "Pods_TeachersAssistant.framework" + BlueprintName = "Pods-TeachersAssistant" + ReferencedContainer = "container:Pods.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" + shouldUseLaunchSchemeArgsEnv = "YES"> + <Testables> + </Testables> + <AdditionalOptions> + </AdditionalOptions> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "2B5D02F570FFD6BDFE13FF35208146B1" + BuildableName = "Pods_TeachersAssistant.framework" + BlueprintName = "Pods-TeachersAssistant" + ReferencedContainer = "container:Pods.xcodeproj"> + </BuildableReference> + </MacroExpansion> + <AdditionalOptions> + </AdditionalOptions> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/Pods/Pods.xcodeproj/xcuserdata/naomi.xcuserdatad/xcschemes/Pods-TeachersAssistantTests.xcscheme b/Pods/Pods.xcodeproj/xcuserdata/naomi.xcuserdatad/xcschemes/Pods-TeachersAssistantTests.xcscheme @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "0930" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "9115383C4EEFD422CC2FA6BC0AEEF0EE" + BuildableName = "Pods_TeachersAssistantTests.framework" + BlueprintName = "Pods-TeachersAssistantTests" + ReferencedContainer = "container:Pods.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" + shouldUseLaunchSchemeArgsEnv = "YES"> + <Testables> + </Testables> + <AdditionalOptions> + </AdditionalOptions> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "9115383C4EEFD422CC2FA6BC0AEEF0EE" + BuildableName = "Pods_TeachersAssistantTests.framework" + BlueprintName = "Pods-TeachersAssistantTests" + ReferencedContainer = "container:Pods.xcodeproj"> + </BuildableReference> + </MacroExpansion> + <AdditionalOptions> + </AdditionalOptions> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/Pods/Pods.xcodeproj/xcuserdata/naomi.xcuserdatad/xcschemes/xcschememanagement.plist b/Pods/Pods.xcodeproj/xcuserdata/naomi.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>SchemeUserState</key> + <dict> + <key>GTMSessionFetcher.xcscheme</key> + <dict> + <key>isShown</key> + <false/> + <key>orderHint</key> + <integer>1</integer> + </dict> + <key>GoogleAPIClientForREST.xcscheme</key> + <dict> + <key>isShown</key> + <false/> + <key>orderHint</key> + <integer>0</integer> + </dict> + <key>Pods-TeachersAssistant.xcscheme</key> + <dict> + <key>isShown</key> + <false/> + <key>orderHint</key> + <integer>2</integer> + </dict> + <key>Pods-TeachersAssistantTests.xcscheme</key> + <dict> + <key>isShown</key> + <false/> + <key>orderHint</key> + <integer>3</integer> + </dict> + </dict> + <key>SuppressBuildableAutocreation</key> + <dict/> +</dict> +</plist> diff --git a/Pods/Target Support Files/GTMSessionFetcher/GTMSessionFetcher-Info.plist b/Pods/Target Support Files/GTMSessionFetcher/GTMSessionFetcher-Info.plist @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${PRODUCT_BUNDLE_IDENTIFIER}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>FMWK</string> + <key>CFBundleShortVersionString</key> + <string>1.2.1</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>${CURRENT_PROJECT_VERSION}</string> + <key>NSPrincipalClass</key> + <string></string> +</dict> +</plist> diff --git a/Pods/Target Support Files/GTMSessionFetcher/GTMSessionFetcher-dummy.m b/Pods/Target Support Files/GTMSessionFetcher/GTMSessionFetcher-dummy.m @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> +@interface PodsDummy_GTMSessionFetcher : NSObject +@end +@implementation PodsDummy_GTMSessionFetcher +@end diff --git a/Pods/Target Support Files/GTMSessionFetcher/GTMSessionFetcher-prefix.pch b/Pods/Target Support Files/GTMSessionFetcher/GTMSessionFetcher-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import <UIKit/UIKit.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/Pods/Target Support Files/GTMSessionFetcher/GTMSessionFetcher-umbrella.h b/Pods/Target Support Files/GTMSessionFetcher/GTMSessionFetcher-umbrella.h @@ -0,0 +1,23 @@ +#ifdef __OBJC__ +#import <UIKit/UIKit.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + +#import "GTMSessionFetcher.h" +#import "GTMSessionFetcherLogging.h" +#import "GTMSessionFetcherService.h" +#import "GTMSessionUploadFetcher.h" +#import "GTMGatherInputStream.h" +#import "GTMMIMEDocument.h" +#import "GTMReadMonitorInputStream.h" + +FOUNDATION_EXPORT double GTMSessionFetcherVersionNumber; +FOUNDATION_EXPORT const unsigned char GTMSessionFetcherVersionString[]; + diff --git a/Pods/Target Support Files/GTMSessionFetcher/GTMSessionFetcher.modulemap b/Pods/Target Support Files/GTMSessionFetcher/GTMSessionFetcher.modulemap @@ -0,0 +1,6 @@ +framework module GTMSessionFetcher { + umbrella header "GTMSessionFetcher-umbrella.h" + + export * + module * { export * } +} diff --git a/Pods/Target Support Files/GTMSessionFetcher/GTMSessionFetcher.xcconfig b/Pods/Target Support Files/GTMSessionFetcher/GTMSessionFetcher.xcconfig @@ -0,0 +1,9 @@ +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +OTHER_LDFLAGS = $(inherited) -framework "Security" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/GTMSessionFetcher +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES diff --git a/Pods/Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST-Info.plist b/Pods/Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST-Info.plist @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${PRODUCT_BUNDLE_IDENTIFIER}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>FMWK</string> + <key>CFBundleShortVersionString</key> + <string>1.3.8</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>${CURRENT_PROJECT_VERSION}</string> + <key>NSPrincipalClass</key> + <string></string> +</dict> +</plist> diff --git a/Pods/Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST-dummy.m b/Pods/Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST-dummy.m @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> +@interface PodsDummy_GoogleAPIClientForREST : NSObject +@end +@implementation PodsDummy_GoogleAPIClientForREST +@end diff --git a/Pods/Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST-prefix.pch b/Pods/Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import <UIKit/UIKit.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/Pods/Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST-umbrella.h b/Pods/Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST-umbrella.h @@ -0,0 +1,31 @@ +#ifdef __OBJC__ +#import <UIKit/UIKit.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + +#import "GTLRDefines.h" +#import "GTLRBatchQuery.h" +#import "GTLRBatchResult.h" +#import "GTLRDateTime.h" +#import "GTLRDuration.h" +#import "GTLRErrorObject.h" +#import "GTLRObject.h" +#import "GTLRQuery.h" +#import "GTLRRuntimeCommon.h" +#import "GTLRService.h" +#import "GTLRUploadParameters.h" +#import "GTLRBase64.h" +#import "GTLRFramework.h" +#import "GTLRURITemplate.h" +#import "GTLRUtilities.h" + +FOUNDATION_EXPORT double GoogleAPIClientForRESTVersionNumber; +FOUNDATION_EXPORT const unsigned char GoogleAPIClientForRESTVersionString[]; + diff --git a/Pods/Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST.modulemap b/Pods/Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST.modulemap @@ -0,0 +1,6 @@ +framework module GoogleAPIClientForREST { + umbrella header "GoogleAPIClientForREST-umbrella.h" + + export * + module * { export * } +} diff --git a/Pods/Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST.xcconfig b/Pods/Target Support Files/GoogleAPIClientForREST/GoogleAPIClientForREST.xcconfig @@ -0,0 +1,9 @@ +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/GoogleAPIClientForREST +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/GoogleAPIClientForREST +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES diff --git a/Pods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-Info.plist b/Pods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-Info.plist @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${PRODUCT_BUNDLE_IDENTIFIER}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>FMWK</string> + <key>CFBundleShortVersionString</key> + <string>1.0.0</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>${CURRENT_PROJECT_VERSION}</string> + <key>NSPrincipalClass</key> + <string></string> +</dict> +</plist> diff --git a/Pods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-acknowledgements.markdown b/Pods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-acknowledgements.markdown @@ -0,0 +1,415 @@ +# Acknowledgements +This application makes use of the following third party libraries: + +## GTMSessionFetcher + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +## GoogleAPIClientForREST + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Generated by CocoaPods - https://cocoapods.org diff --git a/Pods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-acknowledgements.plist b/Pods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-acknowledgements.plist @@ -0,0 +1,453 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>PreferenceSpecifiers</key> + <array> + <dict> + <key>FooterText</key> + <string>This application makes use of the following third party libraries:</string> + <key>Title</key> + <string>Acknowledgements</string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + <dict> + <key>FooterText</key> + <string> + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +</string> + <key>License</key> + <string>Apache</string> + <key>Title</key> + <string>GTMSessionFetcher</string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + <dict> + <key>FooterText</key> + <string> + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +</string> + <key>License</key> + <string>Apache</string> + <key>Title</key> + <string>GoogleAPIClientForREST</string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + <dict> + <key>FooterText</key> + <string>Generated by CocoaPods - https://cocoapods.org</string> + <key>Title</key> + <string></string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + </array> + <key>StringsTable</key> + <string>Acknowledgements</string> + <key>Title</key> + <string>Acknowledgements</string> +</dict> +</plist> diff --git a/Pods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-dummy.m b/Pods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-dummy.m @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> +@interface PodsDummy_Pods_TeachersAssistant : NSObject +@end +@implementation PodsDummy_Pods_TeachersAssistant +@end diff --git a/Pods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-frameworks.sh b/Pods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-frameworks.sh @@ -0,0 +1,165 @@ +#!/bin/sh +set -e +set -u +set -o pipefail + +function on_error { + echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" +} +trap 'on_error $LINENO' ERR + +if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then + # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy + # frameworks to, so exit 0 (signalling the script phase was successful). + exit 0 +fi + +echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" +mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" +SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" + +# Used as a return value for each invocation of `strip_invalid_archs` function. +STRIP_BINARY_RETVAL=0 + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +# Copies and strips a vendored framework +install_framework() +{ + if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then + local source="${BUILT_PRODUCTS_DIR}/$1" + elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then + local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" + elif [ -r "$1" ]; then + local source="$1" + fi + + local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink "${source}")" + fi + + # Use filter instead of exclude so missing patterns don't throw errors. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" + + local basename + basename="$(basename -s .framework "$1")" + binary="${destination}/${basename}.framework/${basename}" + + if ! [ -r "$binary" ]; then + binary="${destination}/${basename}" + elif [ -L "${binary}" ]; then + echo "Destination binary is symlinked..." + dirname="$(dirname "${binary}")" + binary="${dirname}/$(readlink "${binary}")" + fi + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then + strip_invalid_archs "$binary" + fi + + # Resign the code if required by the build settings to avoid unstable apps + code_sign_if_enabled "${destination}/$(basename "$1")" + + # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. + if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then + local swift_runtime_libs + swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) + for lib in $swift_runtime_libs; do + echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" + rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" + code_sign_if_enabled "${destination}/${lib}" + done + fi +} + +# Copies and strips a vendored dSYM +install_dsym() { + local source="$1" + if [ -r "$source" ]; then + # Copy the dSYM into a the targets temp dir. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" + + local basename + basename="$(basename -s .framework.dSYM "$source")" + binary="${DERIVED_FILES_DIR}/${basename}.framework.dSYM/Contents/Resources/DWARF/${basename}" + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"Mach-O dSYM companion"* ]]; then + strip_invalid_archs "$binary" + fi + + if [[ $STRIP_BINARY_RETVAL == 1 ]]; then + # Move the stripped file into its final destination. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.framework.dSYM" "${DWARF_DSYM_FOLDER_PATH}" + else + # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. + touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.framework.dSYM" + fi + fi +} + +# Signs a framework with the provided identity +code_sign_if_enabled() { + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then + # Use the current code_sign_identity + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" + + if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + code_sign_cmd="$code_sign_cmd &" + fi + echo "$code_sign_cmd" + eval "$code_sign_cmd" + fi +} + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + # Get architectures for current target binary + binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" + # Intersect them with the architectures we are building for + intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" + # If there are no archs supported by this binary then warn the user + if [[ -z "$intersected_archs" ]]; then + echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." + STRIP_BINARY_RETVAL=0 + return + fi + stripped="" + for arch in $binary_archs; do + if ! [[ "${ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$binary" "$binary" + stripped="$stripped $arch" + fi + done + if [[ "$stripped" ]]; then + echo "Stripped $binary of architectures:$stripped" + fi + STRIP_BINARY_RETVAL=1 +} + + +if [[ "$CONFIGURATION" == "Debug" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework" + install_framework "${BUILT_PRODUCTS_DIR}/GoogleAPIClientForREST/GoogleAPIClientForREST.framework" +fi +if [[ "$CONFIGURATION" == "Release" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework" + install_framework "${BUILT_PRODUCTS_DIR}/GoogleAPIClientForREST/GoogleAPIClientForREST.framework" +fi +if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + wait +fi diff --git a/Pods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-umbrella.h b/Pods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import <UIKit/UIKit.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_TeachersAssistantVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_TeachersAssistantVersionString[]; + diff --git a/Pods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant.debug.xcconfig b/Pods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant.debug.xcconfig @@ -0,0 +1,9 @@ +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher" "${PODS_CONFIGURATION_BUILD_DIR}/GoogleAPIClientForREST" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/GoogleAPIClientForREST/GoogleAPIClientForREST.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' +OTHER_LDFLAGS = $(inherited) -framework "GTMSessionFetcher" -framework "GoogleAPIClientForREST" -framework "Security" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/Pods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant.modulemap b/Pods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant.modulemap @@ -0,0 +1,6 @@ +framework module Pods_TeachersAssistant { + umbrella header "Pods-TeachersAssistant-umbrella.h" + + export * + module * { export * } +} diff --git a/Pods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant.release.xcconfig b/Pods/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant.release.xcconfig @@ -0,0 +1,9 @@ +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher" "${PODS_CONFIGURATION_BUILD_DIR}/GoogleAPIClientForREST" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/GoogleAPIClientForREST/GoogleAPIClientForREST.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' +OTHER_LDFLAGS = $(inherited) -framework "GTMSessionFetcher" -framework "GoogleAPIClientForREST" -framework "Security" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/Pods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests-Info.plist b/Pods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests-Info.plist @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${PRODUCT_BUNDLE_IDENTIFIER}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>FMWK</string> + <key>CFBundleShortVersionString</key> + <string>1.0.0</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>${CURRENT_PROJECT_VERSION}</string> + <key>NSPrincipalClass</key> + <string></string> +</dict> +</plist> diff --git a/Pods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests-acknowledgements.markdown b/Pods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests-acknowledgements.markdown @@ -0,0 +1,3 @@ +# Acknowledgements +This application makes use of the following third party libraries: +Generated by CocoaPods - https://cocoapods.org diff --git a/Pods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests-acknowledgements.plist b/Pods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests-acknowledgements.plist @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>PreferenceSpecifiers</key> + <array> + <dict> + <key>FooterText</key> + <string>This application makes use of the following third party libraries:</string> + <key>Title</key> + <string>Acknowledgements</string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + <dict> + <key>FooterText</key> + <string>Generated by CocoaPods - https://cocoapods.org</string> + <key>Title</key> + <string></string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + </array> + <key>StringsTable</key> + <string>Acknowledgements</string> + <key>Title</key> + <string>Acknowledgements</string> +</dict> +</plist> diff --git a/Pods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests-dummy.m b/Pods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests-dummy.m @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> +@interface PodsDummy_Pods_TeachersAssistantTests : NSObject +@end +@implementation PodsDummy_Pods_TeachersAssistantTests +@end diff --git a/Pods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests-umbrella.h b/Pods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import <UIKit/UIKit.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_TeachersAssistantTestsVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_TeachersAssistantTestsVersionString[]; + diff --git a/Pods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests.debug.xcconfig b/Pods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests.debug.xcconfig @@ -0,0 +1,9 @@ +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher" "${PODS_CONFIGURATION_BUILD_DIR}/GoogleAPIClientForREST" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/GoogleAPIClientForREST/GoogleAPIClientForREST.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' +OTHER_LDFLAGS = $(inherited) -framework "GTMSessionFetcher" -framework "GoogleAPIClientForREST" -framework "Security" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/Pods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests.modulemap b/Pods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests.modulemap @@ -0,0 +1,6 @@ +framework module Pods_TeachersAssistantTests { + umbrella header "Pods-TeachersAssistantTests-umbrella.h" + + export * + module * { export * } +} diff --git a/Pods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests.release.xcconfig b/Pods/Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests.release.xcconfig @@ -0,0 +1,9 @@ +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher" "${PODS_CONFIGURATION_BUILD_DIR}/GoogleAPIClientForREST" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/GoogleAPIClientForREST/GoogleAPIClientForREST.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' +OTHER_LDFLAGS = $(inherited) -framework "GTMSessionFetcher" -framework "GoogleAPIClientForREST" -framework "Security" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/Teachers' Assistant/.DS_Store b/Teachers' Assistant/.DS_Store Binary files differ. diff --git a/Teachers' Assistant/AppDelegate.swift b/Teachers' Assistant/AppDelegate.swift @@ -0,0 +1,46 @@ +// +// AppDelegate.swift +// Teachers' Assistant +// +// Created by Benjamin Welner on 2/9/19. +// Copyright © 2019 FIGBERT Inc. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/Teachers' Assistant/Assets.xcassets/AppIcon.appiconset/Contents.json b/Teachers' Assistant/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} +\ No newline at end of file diff --git a/Teachers' Assistant/Assets.xcassets/Contents.json b/Teachers' Assistant/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} +\ No newline at end of file diff --git a/Teachers' Assistant/Base.lproj/LaunchScreen.storyboard b/Teachers' Assistant/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM"> + <device id="retina5_9" orientation="portrait"> + <adaptation id="fullscreen"/> + </device> + <dependencies> + <deployment identifier="iOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/> + <capability name="Constraints to layout margins" minToolsVersion="6.0"/> + <capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/> + <capability name="Safe area layout guides" minToolsVersion="9.0"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <scenes> + <!--View Controller--> + <scene sceneID="EHf-IW-A2E"> + <objects> + <viewController id="01J-lp-oVM" sceneMemberID="viewController"> + <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3"> + <rect key="frame" x="0.0" y="0.0" width="375" height="812"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Brandeis_Web_HiRes_Orange.png" translatesAutoresizingMaskIntoConstraints="NO" id="bpw-F1-HPs"> + <rect key="frame" x="47" y="59" width="281" height="704"/> + </imageView> + </subviews> + <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <constraints> + <constraint firstItem="bpw-F1-HPs" firstAttribute="top" relation="greaterThanOrEqual" secondItem="Ze5-6b-2t3" secondAttribute="topMargin" constant="15" id="0Hr-2J-C4N"/> + <constraint firstItem="bpw-F1-HPs" firstAttribute="width" secondItem="Ze5-6b-2t3" secondAttribute="width" multiplier="0.75" id="8gi-bM-wW0"/> + <constraint firstItem="bpw-F1-HPs" firstAttribute="centerX" secondItem="6Tk-OE-BBY" secondAttribute="centerX" id="Ibe-4k-nbp"/> + <constraint firstAttribute="bottomMargin" relation="greaterThanOrEqual" secondItem="bpw-F1-HPs" secondAttribute="bottom" constant="15" id="OiV-XL-ts6"/> + <constraint firstItem="bpw-F1-HPs" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="6Tk-OE-BBY" secondAttribute="leading" constant="15" id="Q3f-9l-2uE"/> + <constraint firstItem="bpw-F1-HPs" firstAttribute="centerY" secondItem="6Tk-OE-BBY" secondAttribute="centerY" id="fFc-nP-EgW"/> + <constraint firstItem="6Tk-OE-BBY" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="bpw-F1-HPs" secondAttribute="trailing" constant="15" id="vcd-nE-uBa"/> + </constraints> + <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/> + </view> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="52" y="374.66266866566718"/> + </scene> + </scenes> + <resources> + <image name="Brandeis_Web_HiRes_Orange.png" width="8319" height="8318"/> + </resources> +</document> diff --git a/Teachers' Assistant/Base.lproj/Main.storyboard b/Teachers' Assistant/Base.lproj/Main.storyboard @@ -0,0 +1,260 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r"> + <device id="retina5_9" orientation="portrait"> + <adaptation id="fullscreen"/> + </device> + <dependencies> + <deployment identifier="iOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/> + <capability name="Aspect ratio constraints" minToolsVersion="5.1"/> + <capability name="Constraints to layout margins" minToolsVersion="6.0"/> + <capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/> + <capability name="Safe area layout guides" minToolsVersion="9.0"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <scenes> + <!--Sign-In--> + <scene sceneID="tne-QT-ifu"> + <objects> + <viewController title="Sign-In" id="BYZ-38-t0r" customClass="ViewController" customModule="TeachersAssistant" customModuleProvider="target" sceneMemberID="viewController"> + <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC"> + <rect key="frame" x="0.0" y="0.0" width="375" height="812"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Brandeis_Web_HiRes_Orange.png" translatesAutoresizingMaskIntoConstraints="NO" id="ANN-xB-sdu"> + <rect key="frame" x="47" y="35" width="281" height="281.66666666666669"/> + <accessibility key="accessibilityConfiguration"> + <accessibilityTraits key="traits" image="YES" notEnabled="YES"/> + </accessibility> + <constraints> + <constraint firstAttribute="width" secondItem="ANN-xB-sdu" secondAttribute="height" multiplier="1:1" id="zas-2j-QdZ"/> + </constraints> + </imageView> + <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" alignment="top" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="dF2-xd-hym"> + <rect key="frame" x="10" y="391.66666666666669" width="355" height="190.33333333333331"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="12" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="V6s-Bj-0Sf"> + <rect key="frame" x="0.0" y="0.0" width="106.66666666666667" height="28.666666666666668"/> + <fontDescription key="fontDescription" type="system" pointSize="24"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + <textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Work Email" textAlignment="natural" minimumFontSize="12" translatesAutoresizingMaskIntoConstraints="NO" id="3jP-Vs-pvB"> + <rect key="frame" x="0.0" y="36.666666666666629" width="355" height="30"/> + <nil key="textColor"/> + <fontDescription key="fontDescription" type="system" pointSize="14"/> + <textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardAppearance="light" returnKeyType="done" smartDashesType="no" smartInsertDeleteType="no" smartQuotesType="no" textContentType="username"/> + </textField> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Password" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="12" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="yS0-Qf-usa"> + <rect key="frame" x="0.0" y="74.666666666666629" width="100.33333333333333" height="28.666666666666671"/> + <fontDescription key="fontDescription" type="system" pointSize="24"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + <textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Password" textAlignment="natural" minimumFontSize="12" translatesAutoresizingMaskIntoConstraints="NO" id="jNJ-CW-a0w"> + <rect key="frame" x="0.0" y="111.33333333333331" width="355" height="30"/> + <nil key="textColor"/> + <fontDescription key="fontDescription" type="system" pointSize="14"/> + <textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardAppearance="light" returnKeyType="done" secureTextEntry="YES" smartDashesType="no" smartInsertDeleteType="no" smartQuotesType="no" textContentType="password"/> + </textField> + <button opaque="NO" contentMode="scaleAspectFit" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="w8A-1s-Ffj"> + <rect key="frame" x="0.0" y="149.33333333333331" width="76" height="41"/> + <fontDescription key="fontDescription" type="system" pointSize="24"/> + <state key="normal" title="Sign-In"/> + <connections> + <action selector="signInToHome:" destination="BYZ-38-t0r" eventType="touchUpInside" id="Evv-pC-KzT"/> + </connections> + </button> + </subviews> + <constraints> + <constraint firstAttribute="trailing" secondItem="jNJ-CW-a0w" secondAttribute="trailing" id="2aX-4n-BTD"/> + <constraint firstAttribute="trailing" secondItem="3jP-Vs-pvB" secondAttribute="trailing" id="E36-zP-2gz"/> + </constraints> + </stackView> + </subviews> + <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <constraints> + <constraint firstItem="ANN-xB-sdu" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="6cG-6Q-Qli"/> + <constraint firstItem="6Tk-OE-BBY" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="dF2-xd-hym" secondAttribute="bottom" constant="10" id="AKD-A9-2af"/> + <constraint firstItem="ANN-xB-sdu" firstAttribute="height" relation="lessThanOrEqual" secondItem="8bC-Xf-vdC" secondAttribute="height" id="DPz-UH-xSq"/> + <constraint firstItem="ANN-xB-sdu" firstAttribute="width" secondItem="8bC-Xf-vdC" secondAttribute="width" multiplier="0.75" priority="999" id="NVM-ja-kPN"/> + <constraint firstItem="dF2-xd-hym" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" constant="10" id="Tzz-h8-lCc"/> + <constraint firstItem="6Tk-OE-BBY" firstAttribute="trailing" secondItem="dF2-xd-hym" secondAttribute="trailing" constant="10" id="hwA-OU-3FN"/> + <constraint firstItem="dF2-xd-hym" firstAttribute="top" secondItem="ANN-xB-sdu" secondAttribute="bottom" constant="75" id="o0d-f7-jpT"/> + <constraint firstItem="ANN-xB-sdu" firstAttribute="top" secondItem="8bC-Xf-vdC" secondAttribute="top" constant="35" id="y4E-7b-SYI"/> + </constraints> + <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/> + </view> + <connections> + <outlet property="passwordField" destination="jNJ-CW-a0w" id="lea-5f-2bM"/> + <outlet property="usernameField" destination="3jP-Vs-pvB" id="Cb9-nS-7S8"/> + <segue destination="N0Z-74-nkD" kind="presentation" identifier="logInToHomeScreen" id="M90-aa-ejb"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="134.5108695652174" y="121.73913043478262"/> + </scene> + <!--Home--> + <scene sceneID="WhT-W0-fj1"> + <objects> + <viewController title="Home" id="N0Z-74-nkD" customClass="ViewControllerHome" customModule="TeachersAssistant" customModuleProvider="target" sceneMemberID="viewController"> + <view key="view" contentMode="scaleToFill" id="NgL-GF-Dl2"> + <rect key="frame" x="0.0" y="0.0" width="375" height="812"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <button opaque="NO" contentMode="scaleAspectFit" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ehf-di-njk"> + <rect key="frame" x="293" y="696" width="67" height="67"/> + <accessibility key="accessibilityConfiguration"> + <accessibilityTraits key="traits" button="YES" image="YES"/> + </accessibility> + <constraints> + <constraint firstAttribute="width" secondItem="Ehf-di-njk" secondAttribute="height" multiplier="1:1" id="nR5-Rk-vg3"/> + </constraints> + <state key="normal" title="Settings Button" image="Settings Button.png"/> + <connections> + <action selector="homeToSettings:" destination="N0Z-74-nkD" eventType="touchUpInside" id="ixz-Xl-nZd"/> + </connections> + </button> + <imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Brandeis_Web_HiRes_Orange.png" translatesAutoresizingMaskIntoConstraints="NO" id="ca8-q6-A9o"> + <rect key="frame" x="93.666666666666671" y="35.666666666666657" width="187.66666666666663" height="188"/> + <accessibility key="accessibilityConfiguration"> + <accessibilityTraits key="traits" image="YES" notEnabled="YES"/> + </accessibility> + <constraints> + <constraint firstAttribute="width" secondItem="ca8-q6-A9o" secondAttribute="height" multiplier="1:1" id="uqk-57-apG"/> + </constraints> + </imageView> + <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="25" translatesAutoresizingMaskIntoConstraints="NO" id="Tt4-Lg-LC8"> + <rect key="frame" x="46.333333333333343" y="257.33333333333337" width="282.33333333333326" height="307.33333333333337"/> + <subviews> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="wlq-xP-XSO"> + <rect key="frame" x="0.0" y="0.0" width="282.33333333333331" height="141.33333333333334"/> + <constraints> + <constraint firstAttribute="width" secondItem="wlq-xP-XSO" secondAttribute="height" multiplier="2:1" id="xOr-PL-O20"/> + </constraints> + <state key="normal" title="Sign In/Out" image="Sign-In (Orange).png"/> + <connections> + <action selector="signInOutPress:" destination="N0Z-74-nkD" eventType="touchUpInside" id="yc3-kr-h0p"/> + </connections> + </button> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="L5M-3r-781"> + <rect key="frame" x="0.0" y="166.33333333333337" width="282.33333333333331" height="141"/> + <constraints> + <constraint firstAttribute="width" secondItem="L5M-3r-781" secondAttribute="height" multiplier="2:1" id="2ug-Ld-f0a"/> + </constraints> + <state key="normal" title="LiveList" image="LiveList (Orange).png"/> + </button> + </subviews> + </stackView> + </subviews> + <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <constraints> + <constraint firstItem="wlq-xP-XSO" firstAttribute="height" secondItem="NgL-GF-Dl2" secondAttribute="height" priority="999" id="AOe-La-pcA"/> + <constraint firstItem="Tt4-Lg-LC8" firstAttribute="top" relation="greaterThanOrEqual" secondItem="ca8-q6-A9o" secondAttribute="bottom" constant="16" id="GtU-oj-Ta1"/> + <constraint firstItem="L5M-3r-781" firstAttribute="height" secondItem="NgL-GF-Dl2" secondAttribute="height" priority="999" id="Ihr-wb-9gb"/> + <constraint firstItem="ca8-q6-A9o" firstAttribute="centerX" secondItem="NgL-GF-Dl2" secondAttribute="centerX" id="Jzj-KP-xDz"/> + <constraint firstItem="Tt4-Lg-LC8" firstAttribute="centerX" secondItem="I1U-GZ-Vty" secondAttribute="centerX" id="Mjw-GL-Xtx"/> + <constraint firstItem="ca8-q6-A9o" firstAttribute="width" secondItem="NgL-GF-Dl2" secondAttribute="width" multiplier="0.5" priority="999" id="Nsu-b7-p2L"/> + <constraint firstItem="ca8-q6-A9o" firstAttribute="top" secondItem="NgL-GF-Dl2" secondAttribute="top" constant="35" id="PC7-fe-zPL"/> + <constraint firstItem="ca8-q6-A9o" firstAttribute="height" relation="lessThanOrEqual" secondItem="NgL-GF-Dl2" secondAttribute="height" id="Q5I-6t-v1i"/> + <constraint firstItem="I1U-GZ-Vty" firstAttribute="trailing" secondItem="Ehf-di-njk" secondAttribute="trailing" constant="15" id="TFo-aa-pIE"/> + <constraint firstItem="Ehf-di-njk" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="NgL-GF-Dl2" secondAttribute="leading" constant="20" symbolic="YES" id="Tgx-Sj-nkb"/> + <constraint firstItem="Ehf-di-njk" firstAttribute="width" relation="lessThanOrEqual" secondItem="NgL-GF-Dl2" secondAttribute="width" multiplier="0.179" id="Wys-gB-FcB"/> + <constraint firstItem="L5M-3r-781" firstAttribute="width" secondItem="NgL-GF-Dl2" secondAttribute="width" multiplier="0.75" constant="1" id="l8a-RR-TCX"/> + <constraint firstItem="Ehf-di-njk" firstAttribute="height" secondItem="NgL-GF-Dl2" secondAttribute="height" multiplier="0.1" priority="999" id="nxy-fr-MD8"/> + <constraint firstItem="Tt4-Lg-LC8" firstAttribute="centerY" secondItem="I1U-GZ-Vty" secondAttribute="centerY" id="pro-fc-wbR"/> + <constraint firstItem="I1U-GZ-Vty" firstAttribute="bottom" secondItem="Ehf-di-njk" secondAttribute="bottom" constant="15" id="vYS-C7-wXQ"/> + <constraint firstItem="wlq-xP-XSO" firstAttribute="width" secondItem="NgL-GF-Dl2" secondAttribute="width" multiplier="0.75" constant="1" id="wDk-Gk-nsQ"/> + <constraint firstItem="Ehf-di-njk" firstAttribute="top" relation="greaterThanOrEqual" secondItem="Tt4-Lg-LC8" secondAttribute="bottom" constant="15" id="yKm-Fk-HWQ"/> + </constraints> + <viewLayoutGuide key="safeArea" id="I1U-GZ-Vty"/> + </view> + <connections> + <outlet property="homeLogo" destination="ca8-q6-A9o" id="8dH-uD-2k6"/> + <outlet property="liveList" destination="L5M-3r-781" id="lxk-qj-EGB"/> + <outlet property="signInOut" destination="wlq-xP-XSO" id="eth-lF-NuY"/> + <segue destination="bIP-T9-dEq" kind="presentation" identifier="homeToSettings" id="63G-xu-RXq"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="Mlu-bV-OGB" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="925.60000000000002" y="119.70443349753695"/> + </scene> + <!--Settings--> + <scene sceneID="job-Fe-Ins"> + <objects> + <viewController title="Settings" id="bIP-T9-dEq" customClass="ViewControllerSettings" customModule="TeachersAssistant" customModuleProvider="target" sceneMemberID="viewController"> + <view key="view" contentMode="scaleToFill" id="d8q-iB-uUt"> + <rect key="frame" x="0.0" y="0.0" width="375" height="812"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Settings" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="ymd-tO-Soa"> + <rect key="frame" x="15" y="79" width="69" height="20"/> + <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + <pickerView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="b95-TE-PRl"> + <rect key="frame" x="27.666666666666657" y="231" width="320" height="102"/> + <connections> + <outlet property="dataSource" destination="bIP-T9-dEq" id="fnE-Ft-BSj"/> + <outlet property="delegate" destination="bIP-T9-dEq" id="6oV-bC-Dbx"/> + </connections> + </pickerView> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Color Theme" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZgH-Q1-zDR"> + <rect key="frame" x="138" y="200" width="99" height="21"/> + <constraints> + <constraint firstAttribute="height" relation="greaterThanOrEqual" constant="21" id="lNP-am-p1W"/> + </constraints> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="EZS-Fu-0ql"> + <rect key="frame" x="15" y="733" width="37" height="30"/> + <fontDescription key="fontDescription" type="boldSystem" pointSize="15"/> + <state key="normal" title="Back"/> + <connections> + <action selector="settingsToHome:" destination="bIP-T9-dEq" eventType="touchUpInside" id="9z1-M8-CrL"/> + </connections> + </button> + </subviews> + <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <constraints> + <constraint firstItem="ZgH-Q1-zDR" firstAttribute="top" secondItem="q9F-AS-ibx" secondAttribute="top" constant="156" id="6EJ-NK-Pfb"/> + <constraint firstItem="q9F-AS-ibx" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="b95-TE-PRl" secondAttribute="bottom" constant="445" id="7q4-fd-bUI"/> + <constraint firstItem="ymd-tO-Soa" firstAttribute="top" secondItem="q9F-AS-ibx" secondAttribute="top" constant="35" id="BBh-uO-knj"/> + <constraint firstItem="b95-TE-PRl" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="q9F-AS-ibx" secondAttribute="leading" constant="15" id="Lfe-on-ufo"/> + <constraint firstItem="ymd-tO-Soa" firstAttribute="leading" secondItem="q9F-AS-ibx" secondAttribute="leading" constant="15" id="S7n-xp-MMN"/> + <constraint firstItem="EZS-Fu-0ql" firstAttribute="leading" secondItem="d8q-iB-uUt" secondAttribute="leading" constant="15" id="U2J-d0-y3B"/> + <constraint firstItem="b95-TE-PRl" firstAttribute="centerX" secondItem="q9F-AS-ibx" secondAttribute="centerX" id="aXb-1w-Qwe"/> + <constraint firstItem="ZgH-Q1-zDR" firstAttribute="centerX" secondItem="q9F-AS-ibx" secondAttribute="centerX" id="eGV-ra-kyh"/> + <constraint firstItem="b95-TE-PRl" firstAttribute="top" secondItem="ZgH-Q1-zDR" secondAttribute="bottom" constant="10" id="fAm-Gl-Ibc"/> + <constraint firstItem="q9F-AS-ibx" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="b95-TE-PRl" secondAttribute="trailing" constant="15" id="nZU-2c-Do1"/> + <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="ymd-tO-Soa" secondAttribute="trailing" constant="20" symbolic="YES" id="qN2-uC-NNG"/> + <constraint firstAttribute="bottomMargin" secondItem="EZS-Fu-0ql" secondAttribute="bottom" constant="15" id="xDt-UL-SHZ"/> + <constraint firstItem="q9F-AS-ibx" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="EZS-Fu-0ql" secondAttribute="trailing" constant="250" id="y0R-bJ-UYb"/> + </constraints> + <viewLayoutGuide key="safeArea" id="q9F-AS-ibx"/> + </view> + <connections> + <outlet property="colorTheme" destination="b95-TE-PRl" id="Uu3-Vd-Byw"/> + <segue destination="N0Z-74-nkD" kind="presentation" identifier="settingsToHome" id="QHk-wX-055"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="LDn-En-VWf" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="133" y="871"/> + </scene> + </scenes> + <resources> + <image name="Brandeis_Web_HiRes_Orange.png" width="8319" height="8318"/> + <image name="LiveList (Orange).png" width="4800" height="2401"/> + <image name="Settings Button.png" width="220" height="216"/> + <image name="Sign-In (Orange).png" width="4800" height="2401"/> + </resources> + <inferredMetricsTieBreakers> + <segue reference="QHk-wX-055"/> + </inferredMetricsTieBreakers> +</document> diff --git a/Teachers' Assistant/Images/Brandeis_Web_HiRes_Orange.png b/Teachers' Assistant/Images/Brandeis_Web_HiRes_Orange.png Binary files differ. diff --git a/Teachers' Assistant/Images/DarkBlue-Logo.PNG b/Teachers' Assistant/Images/DarkBlue-Logo.PNG Binary files differ. diff --git a/Teachers' Assistant/Images/Gray-Logo.png b/Teachers' Assistant/Images/Gray-Logo.png Binary files differ. diff --git a/Teachers' Assistant/Images/Green-Logo.PNG b/Teachers' Assistant/Images/Green-Logo.PNG Binary files differ. diff --git a/Teachers' Assistant/Images/LightBlue-Logo.PNG b/Teachers' Assistant/Images/LightBlue-Logo.PNG Binary files differ. diff --git a/Teachers' Assistant/Images/LiveList (Dark Blue).png b/Teachers' Assistant/Images/LiveList (Dark Blue).png Binary files differ. diff --git a/Teachers' Assistant/Images/LiveList (Gray).png b/Teachers' Assistant/Images/LiveList (Gray).png Binary files differ. diff --git a/Teachers' Assistant/Images/LiveList (Green).png b/Teachers' Assistant/Images/LiveList (Green).png Binary files differ. diff --git a/Teachers' Assistant/Images/LiveList (Light Blue).png b/Teachers' Assistant/Images/LiveList (Light Blue).png Binary files differ. diff --git a/Teachers' Assistant/Images/LiveList (Orange).png b/Teachers' Assistant/Images/LiveList (Orange).png Binary files differ. diff --git a/Teachers' Assistant/Images/LiveList (Pink).png b/Teachers' Assistant/Images/LiveList (Pink).png Binary files differ. diff --git a/Teachers' Assistant/Images/Pink-Logo.PNG b/Teachers' Assistant/Images/Pink-Logo.PNG Binary files differ. diff --git a/Teachers' Assistant/Images/Settings Button.png b/Teachers' Assistant/Images/Settings Button.png Binary files differ. diff --git a/Teachers' Assistant/Images/Sign-In (Dark Blue).png b/Teachers' Assistant/Images/Sign-In (Dark Blue).png Binary files differ. diff --git a/Teachers' Assistant/Images/Sign-In (Gray).png b/Teachers' Assistant/Images/Sign-In (Gray).png Binary files differ. diff --git a/Teachers' Assistant/Images/Sign-In (Green).png b/Teachers' Assistant/Images/Sign-In (Green).png Binary files differ. diff --git a/Teachers' Assistant/Images/Sign-In (Light Blue).png b/Teachers' Assistant/Images/Sign-In (Light Blue).png Binary files differ. diff --git a/Teachers' Assistant/Images/Sign-In (Orange).png b/Teachers' Assistant/Images/Sign-In (Orange).png Binary files differ. diff --git a/Teachers' Assistant/Images/Sign-In (Pink).png b/Teachers' Assistant/Images/Sign-In (Pink).png Binary files differ. diff --git a/Teachers' Assistant/Images/Sign-Out (Dark Blue).png b/Teachers' Assistant/Images/Sign-Out (Dark Blue).png Binary files differ. diff --git a/Teachers' Assistant/Images/Sign-Out (Gray).png b/Teachers' Assistant/Images/Sign-Out (Gray).png Binary files differ. diff --git a/Teachers' Assistant/Images/Sign-Out (Green).png b/Teachers' Assistant/Images/Sign-Out (Green).png Binary files differ. diff --git a/Teachers' Assistant/Images/Sign-Out (Light Blue).png b/Teachers' Assistant/Images/Sign-Out (Light Blue).png Binary files differ. diff --git a/Teachers' Assistant/Images/Sign-Out (Orange).png b/Teachers' Assistant/Images/Sign-Out (Orange).png Binary files differ. diff --git a/Teachers' Assistant/Images/Sign-Out (Pink).png b/Teachers' Assistant/Images/Sign-Out (Pink).png Binary files differ. diff --git a/Teachers' Assistant/Info.plist b/Teachers' Assistant/Info.plist @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>$(DEVELOPMENT_LANGUAGE)</string> + <key>CFBundleDisplayName</key> + <string>Teachers' Assistant</string> + <key>CFBundleExecutable</key> + <string>$(EXECUTABLE_NAME)</string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>$(PRODUCT_NAME)</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>1.0</string> + <key>CFBundleVersion</key> + <string>1</string> + <key>LSRequiresIPhoneOS</key> + <true/> + <key>UILaunchStoryboardName</key> + <string>LaunchScreen</string> + <key>UIMainStoryboardFile</key> + <string>Main</string> + <key>UIRequiredDeviceCapabilities</key> + <array> + <string>armv7</string> + </array> + <key>UISupportedInterfaceOrientations</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + <string>UIInterfaceOrientationLandscapeLeft</string> + <string>UIInterfaceOrientationLandscapeRight</string> + </array> + <key>UISupportedInterfaceOrientations~ipad</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + <string>UIInterfaceOrientationPortraitUpsideDown</string> + <string>UIInterfaceOrientationLandscapeLeft</string> + <string>UIInterfaceOrientationLandscapeRight</string> + </array> +</dict> +</plist> diff --git a/Teachers' Assistant/ViewController.swift b/Teachers' Assistant/ViewController.swift @@ -0,0 +1,51 @@ +// +// ViewController.swift +// Teachers' Assistant +// +// Created by Benjamin Welner on 2/9/19. +// Copyright © 2019 FIGBERT Inc. All rights reserved. +// + +import UIKit +import GoogleAPIClientForREST + +class ViewController: UIViewController { + +/* PRELIMINARY GLOBAL COLOR THEME START */ + +//Creating a global variable to manage color theme +struct globalVariables { + //All of the possible color themes + static let colorThemes = ["Orange", "Dark Blue", "Light Blue", "Gray", "Green", "Pink"] + //Color theme of all buttons + static var activeColorTheme = "Orange" + //Dark mode + //static var darkMode: Bool = false +} + +/* PRELIMINARY GLOBAL COLOR THEME END*/ + +@IBOutlet weak var usernameField: UITextField! +@IBOutlet weak var passwordField: UITextField! + +@IBAction func signInToHome(_ sender: Any) { + let username: String = usernameField.text! + let password: String = passwordField.text! + + if (username == "bwelner" && password == "c4ctus"){ + performSegue(withIdentifier: "logInToHomeScreen", sender: self) + } +} + +override func viewDidLoad() { + super.viewDidLoad() + //Do any additional setup after loading the view, typically from a nib. +} + +override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. +} + + +} diff --git a/Teachers' Assistant/ViewControllerHome.swift b/Teachers' Assistant/ViewControllerHome.swift @@ -0,0 +1,182 @@ +// +// ViewControllerHome.swift +// Teachers' Assistant +// +// Created by Benjamin Welner on 2/9/19. +// Copyright © 2019 FIGBERT Inc. All rights reserved. +// + +import UIKit +import GoogleAPIClientForREST + +class ViewControllerHome: UIViewController { + + /* PRELIMINARY COLOR THEME START */ + + //Naming all colored buttons so I can change colors + @IBOutlet weak var liveList: UIButton! + @IBOutlet weak var homeLogo: UIImageView! + + /* PRELIMINARY COLOR THEME END */ + + /* SIGN IN-OUT BUTTON START */ + + //Naming signInOut button so I can modify it on press, and giving in/out state functionality + @IBOutlet weak var signInOut: UIButton! + var signedOut: Bool = true + //Making a function to decide button color when signInOut button pressed + func inOutButtonSet (color: String, state: Bool) -> String{ + if (color=="Dark Blue" && state){ + return "DBSI" + } else if (color=="Dark Blue" && !state){ + return "DBSO" + } else if (color=="Green" && state){ + return "GESI" + } else if (color=="Green" && !state){ + return "GESO" + } else if (color=="Gray" && state){ + return "GRSI" + } else if (color=="Gray" && !state){ + return "GRSO" + } else if (color=="Light Blue" && state){ + return "LBSI" + } else if (color=="Light Blue" && !state){ + return "LBSO" + } else if (color=="Orange" && state){ + return "OSI" + } else if (color=="Orange" && !state){ + return "OSO" + } else if (color=="Pink" && state){ + return "PSI" + } else if (color=="Pink" && !state){ + return "PSO" + } else { + return "OSI" + } + } + //Defining a variable to use on button press to determine color/state + var properButton: String = "" + //signInOut button functionality + @IBAction func signInOutPress(_ sender: Any) { + //Invert button graphic + signedOut = !signedOut + properButton = inOutButtonSet(color: ViewController.globalVariables.activeColorTheme, state: signedOut) + if properButton=="DBSI" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-In (Dark Blue).png"), for: .normal) + } else if properButton=="DBSO" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-Out (Dark Blue).png"), for: .normal) + } else if properButton=="GESI" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-In (Green).png"), for: .normal) + } else if properButton=="GESO" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-Out (Green).png"), for: .normal) + } else if properButton=="GRSI" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-In (Gray).png"), for: .normal) + } else if properButton=="GRSO" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-Out (Gray).png"), for: .normal) + } else if properButton=="LBSI" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-In (Light Blue).png"), for: .normal) + } else if properButton=="LBSO" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-Out (Light Blue).png"), for: .normal) + } else if properButton=="OSI" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-In (Orange).png"), for: .normal) + } else if properButton=="OSO" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-Out (Orange).png"), for: .normal) + } else if properButton=="PSI" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-In (Pink).png"), for: .normal) + } else if properButton=="PSO" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-Out (Pink).png"), for: .normal) + } else { + signInOut.setImage(#imageLiteral(resourceName: "Sign-In (Orange).png"), for: .normal) + } + //Google Drive functionality + } + + /* SIGN IN-OUT BUTTON END */ + + //Settings button segue to settings page + @IBAction func homeToSettings(_ sender: Any) { + performSegue(withIdentifier: "homeToSettings", sender: self) + } + + override func viewDidLoad() { + super.viewDidLoad() + + /* REGULAR BUTTON COLOR THEME START */ + + if ViewController.globalVariables.activeColorTheme=="Dark Blue"{ + liveList.setImage(#imageLiteral(resourceName: "LiveList (Dark Blue).png"), for: .normal) + homeLogo.image = UIImage(named: "DarkBlue-Logo.PNG") + } else if ViewController.globalVariables.activeColorTheme=="Green"{ + liveList.setImage(#imageLiteral(resourceName: "LiveList (Green).png"), for: .normal) + homeLogo.image = UIImage(named: "Green-Logo.PNG") + } else if ViewController.globalVariables.activeColorTheme=="Gray"{ + liveList.setImage(#imageLiteral(resourceName: "LiveList (Gray).png"), for: .normal) + homeLogo.image = UIImage(named: "Gray-Logo.png") + } else if ViewController.globalVariables.activeColorTheme=="Light Blue"{ + liveList.setImage(#imageLiteral(resourceName: "LiveList (Light Blue).png"), for: .normal) + homeLogo.image = UIImage(named: "LightBlue-Logo.PNG") + } else if ViewController.globalVariables.activeColorTheme=="Orange"{ + liveList.setImage(#imageLiteral(resourceName: "LiveList (Orange).png"), for: .normal) + homeLogo.image = UIImage(named: "Brandeis_Web_HiRes_Orange.png") + } else if ViewController.globalVariables.activeColorTheme=="Pink"{ + liveList.setImage(#imageLiteral(resourceName: "LiveList (Pink).png"), for: .normal) + homeLogo.image = UIImage(named: "Pink-Logo.PNG") + } else { + liveList.setImage(#imageLiteral(resourceName: "LiveList (Orange).png"), for: .normal) + homeLogo.image = UIImage(named: "Brandeis_Web_HiRes_Orange.png") + } + + /* REGULAR BUTTON COLOR THEME END */ + + /* SIGN IN BUTTON LOAD THEME START */ + + properButton = inOutButtonSet(color: ViewController.globalVariables.activeColorTheme, state: signedOut) + if properButton=="DBSI" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-In (Dark Blue).png"), for: .normal) + } else if properButton=="DBSO" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-Out (Dark Blue).png"), for: .normal) + } else if properButton=="GESI" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-In (Green).png"), for: .normal) + } else if properButton=="GESO" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-Out (Green).png"), for: .normal) + } else if properButton=="GRSI" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-In (Gray).png"), for: .normal) + } else if properButton=="GRSO" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-Out (Gray).png"), for: .normal) + } else if properButton=="LBSI" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-In (Light Blue).png"), for: .normal) + } else if properButton=="LBSO" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-Out (Light Blue).png"), for: .normal) + } else if properButton=="OSI" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-In (Orange).png"), for: .normal) + } else if properButton=="OSO" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-Out (Orange).png"), for: .normal) + } else if properButton=="PSI" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-In (Pink).png"), for: .normal) + } else if properButton=="PSO" { + signInOut.setImage(#imageLiteral(resourceName: "Sign-Out (Pink).png"), for: .normal) + } else { + signInOut.setImage(#imageLiteral(resourceName: "Sign-In (Orange).png"), for: .normal) + } + + /* SIGN IN BUTTON LOAD THEME END */ + + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destinationViewController. + // Pass the selected object to the new view controller. + } + */ + +} diff --git a/Teachers' Assistant/ViewControllerSettings.swift b/Teachers' Assistant/ViewControllerSettings.swift @@ -0,0 +1,66 @@ +// +// ViewControllerSettings.swift +// Teachers' Assistant +// +// Created by Benjamin Welner on 2/23/19. +// Copyright © 2019 FIGBERT Inc. All rights reserved. +// + +import UIKit +import GoogleAPIClientForREST + +class ViewControllerSettings: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate { + + + /* COLOR THEME PICKER VIEW START */ + //Connecting the picker view + @IBOutlet weak var colorTheme: UIPickerView! + //Declaring how many items in the color picker + func numberOfComponents(in pickerView: UIPickerView) -> Int { + return 1 + } + //Assigning the colors to the empty slots + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + return ViewController.globalVariables.colorThemes[row] + } + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + return ViewController.globalVariables.colorThemes.count + } + //What the selections do + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + ViewController.globalVariables.activeColorTheme = ViewController.globalVariables.colorThemes[row] + } + + /* COLOR THEME PICKER VIEW END */ + + /* DARK MODE START */ + /* DARK MODE END */ + + //Connecting settingsToHome segue to back button + @IBAction func settingsToHome(_ sender: Any) { + performSegue(withIdentifier: "settingsToHome", sender: self) + } + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destinationViewController. + // Pass the selected object to the new view controller. + } + */ + +} diff --git a/Teachers' AssistantTests/Info.plist b/Teachers' AssistantTests/Info.plist @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>$(DEVELOPMENT_LANGUAGE)</string> + <key>CFBundleExecutable</key> + <string>$(EXECUTABLE_NAME)</string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>$(PRODUCT_NAME)</string> + <key>CFBundlePackageType</key> + <string>BNDL</string> + <key>CFBundleShortVersionString</key> + <string>1.0</string> + <key>CFBundleVersion</key> + <string>1</string> +</dict> +</plist> diff --git a/Teachers' AssistantTests/Teachers__AssistantTests.swift b/Teachers' AssistantTests/Teachers__AssistantTests.swift @@ -0,0 +1,36 @@ +// +// Teachers__AssistantTests.swift +// Teachers' AssistantTests +// +// Created by Benjamin Welner on 2/9/19. +// Copyright © 2019 FIGBERT Inc. All rights reserved. +// + +import XCTest +@testable import Teachers__Assistant + +class Teachers__AssistantTests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/TeachersAssistant.xcodeproj/project.pbxproj b/TeachersAssistant.xcodeproj/project.pbxproj @@ -0,0 +1,663 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + 8D05B90C2A1C660E42AA56AE /* Pods_TeachersAssistant.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8D10E8B200C95EFE160F2EC8 /* Pods_TeachersAssistant.framework */; }; + 906A6F4745D1FA15B86160B9 /* Pods_TeachersAssistantTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 11EF5C302B8B1C8D48F3D0F2 /* Pods_TeachersAssistantTests.framework */; }; + B30366162221E45A00BB9DC6 /* Brandeis_Web_HiRes_Orange.png in Resources */ = {isa = PBXBuildFile; fileRef = B30366152221E45900BB9DC6 /* Brandeis_Web_HiRes_Orange.png */; }; + B30366182221E60C00BB9DC6 /* ViewControllerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B30366172221E60C00BB9DC6 /* ViewControllerSettings.swift */; }; + B34CDB9F221650410031C28B /* Sign-Out (Dark Blue).png in Resources */ = {isa = PBXBuildFile; fileRef = B34CDB8C221650360031C28B /* Sign-Out (Dark Blue).png */; }; + B34CDBA0221650410031C28B /* Sign-In (Light Blue).png in Resources */ = {isa = PBXBuildFile; fileRef = B34CDB8D221650360031C28B /* Sign-In (Light Blue).png */; }; + B34CDBA1221650410031C28B /* LiveList (Gray).png in Resources */ = {isa = PBXBuildFile; fileRef = B34CDB8E221650370031C28B /* LiveList (Gray).png */; }; + B34CDBA2221650410031C28B /* LiveList (Orange).png in Resources */ = {isa = PBXBuildFile; fileRef = B34CDB8F221650370031C28B /* LiveList (Orange).png */; }; + B34CDBA3221650410031C28B /* Sign-Out (Green).png in Resources */ = {isa = PBXBuildFile; fileRef = B34CDB90221650370031C28B /* Sign-Out (Green).png */; }; + B34CDBA4221650410031C28B /* LiveList (Dark Blue).png in Resources */ = {isa = PBXBuildFile; fileRef = B34CDB91221650380031C28B /* LiveList (Dark Blue).png */; }; + B34CDBA5221650410031C28B /* Sign-Out (Pink).png in Resources */ = {isa = PBXBuildFile; fileRef = B34CDB92221650390031C28B /* Sign-Out (Pink).png */; }; + B34CDBA6221650410031C28B /* Settings Button.png in Resources */ = {isa = PBXBuildFile; fileRef = B34CDB93221650390031C28B /* Settings Button.png */; }; + B34CDBA7221650410031C28B /* Sign-In (Orange).png in Resources */ = {isa = PBXBuildFile; fileRef = B34CDB942216503A0031C28B /* Sign-In (Orange).png */; }; + B34CDBA8221650410031C28B /* Sign-In (Pink).png in Resources */ = {isa = PBXBuildFile; fileRef = B34CDB952216503A0031C28B /* Sign-In (Pink).png */; }; + B34CDBA9221650410031C28B /* Sign-In (Green).png in Resources */ = {isa = PBXBuildFile; fileRef = B34CDB962216503B0031C28B /* Sign-In (Green).png */; }; + B34CDBAA221650410031C28B /* Sign-Out (Light Blue).png in Resources */ = {isa = PBXBuildFile; fileRef = B34CDB972216503C0031C28B /* Sign-Out (Light Blue).png */; }; + B34CDBAB221650410031C28B /* Sign-Out (Orange).png in Resources */ = {isa = PBXBuildFile; fileRef = B34CDB982216503C0031C28B /* Sign-Out (Orange).png */; }; + B34CDBAC221650410031C28B /* Sign-In (Gray).png in Resources */ = {isa = PBXBuildFile; fileRef = B34CDB992216503D0031C28B /* Sign-In (Gray).png */; }; + B34CDBAD221650410031C28B /* LiveList (Pink).png in Resources */ = {isa = PBXBuildFile; fileRef = B34CDB9A2216503E0031C28B /* LiveList (Pink).png */; }; + B34CDBAE221650410031C28B /* LiveList (Light Blue).png in Resources */ = {isa = PBXBuildFile; fileRef = B34CDB9B2216503E0031C28B /* LiveList (Light Blue).png */; }; + B34CDBAF221650410031C28B /* Sign-In (Dark Blue).png in Resources */ = {isa = PBXBuildFile; fileRef = B34CDB9C2216503F0031C28B /* Sign-In (Dark Blue).png */; }; + B34CDBB0221650410031C28B /* Sign-Out (Gray).png in Resources */ = {isa = PBXBuildFile; fileRef = B34CDB9D2216503F0031C28B /* Sign-Out (Gray).png */; }; + B34CDBB1221650410031C28B /* LiveList (Green).png in Resources */ = {isa = PBXBuildFile; fileRef = B34CDB9E221650400031C28B /* LiveList (Green).png */; }; + B38529CE223599870063E0F7 /* Gray-Logo.png in Resources */ = {isa = PBXBuildFile; fileRef = B38529C9223599850063E0F7 /* Gray-Logo.png */; }; + B38529CF223599870063E0F7 /* DarkBlue-Logo.PNG in Resources */ = {isa = PBXBuildFile; fileRef = B38529CA223599860063E0F7 /* DarkBlue-Logo.PNG */; }; + B38529D0223599870063E0F7 /* LightBlue-Logo.PNG in Resources */ = {isa = PBXBuildFile; fileRef = B38529CB223599860063E0F7 /* LightBlue-Logo.PNG */; }; + B38529D1223599870063E0F7 /* Green-Logo.PNG in Resources */ = {isa = PBXBuildFile; fileRef = B38529CC223599860063E0F7 /* Green-Logo.PNG */; }; + B38529D2223599870063E0F7 /* Pink-Logo.PNG in Resources */ = {isa = PBXBuildFile; fileRef = B38529CD223599870063E0F7 /* Pink-Logo.PNG */; }; + B3D8E1D5220F7847007CAD3A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D8E1D4220F7847007CAD3A /* AppDelegate.swift */; }; + B3D8E1D7220F7847007CAD3A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D8E1D6220F7847007CAD3A /* ViewController.swift */; }; + B3D8E1DA220F7847007CAD3A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B3D8E1D8220F7847007CAD3A /* Main.storyboard */; }; + B3D8E1DC220F7847007CAD3A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B3D8E1DB220F7847007CAD3A /* Assets.xcassets */; }; + B3D8E1DF220F7847007CAD3A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B3D8E1DD220F7847007CAD3A /* LaunchScreen.storyboard */; }; + B3D8E1EA220F7848007CAD3A /* Teachers__AssistantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D8E1E9220F7848007CAD3A /* Teachers__AssistantTests.swift */; }; + B3D8E1FB220F8172007CAD3A /* ViewControllerHome.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D8E1FA220F8172007CAD3A /* ViewControllerHome.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + B3D8E1E6220F7848007CAD3A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B3D8E1C9220F7847007CAD3A /* Project object */; + proxyType = 1; + remoteGlobalIDString = B3D8E1D0220F7847007CAD3A; + remoteInfo = "Teachers' Assistant"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 11EF5C302B8B1C8D48F3D0F2 /* Pods_TeachersAssistantTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TeachersAssistantTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1723407E824D6432322BF145 /* Pods-TeachersAssistant.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TeachersAssistant.release.xcconfig"; path = "Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant.release.xcconfig"; sourceTree = "<group>"; }; + 3FCDA47A9F6775F2E7818B5F /* Pods-TeachersAssistantTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TeachersAssistantTests.release.xcconfig"; path = "Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests.release.xcconfig"; sourceTree = "<group>"; }; + 7088E7DE163DD403B94F95F6 /* Pods-TeachersAssistantTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TeachersAssistantTests.debug.xcconfig"; path = "Target Support Files/Pods-TeachersAssistantTests/Pods-TeachersAssistantTests.debug.xcconfig"; sourceTree = "<group>"; }; + 800B9D84351418F21FB4E4AC /* Pods-TeachersAssistant.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TeachersAssistant.debug.xcconfig"; path = "Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant.debug.xcconfig"; sourceTree = "<group>"; }; + 8D10E8B200C95EFE160F2EC8 /* Pods_TeachersAssistant.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TeachersAssistant.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B30366152221E45900BB9DC6 /* Brandeis_Web_HiRes_Orange.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Brandeis_Web_HiRes_Orange.png; sourceTree = "<group>"; }; + B30366172221E60C00BB9DC6 /* ViewControllerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerSettings.swift; sourceTree = "<group>"; }; + B34CDB8C221650360031C28B /* Sign-Out (Dark Blue).png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Sign-Out (Dark Blue).png"; sourceTree = "<group>"; }; + B34CDB8D221650360031C28B /* Sign-In (Light Blue).png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Sign-In (Light Blue).png"; sourceTree = "<group>"; }; + B34CDB8E221650370031C28B /* LiveList (Gray).png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "LiveList (Gray).png"; sourceTree = "<group>"; }; + B34CDB8F221650370031C28B /* LiveList (Orange).png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "LiveList (Orange).png"; sourceTree = "<group>"; }; + B34CDB90221650370031C28B /* Sign-Out (Green).png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Sign-Out (Green).png"; sourceTree = "<group>"; }; + B34CDB91221650380031C28B /* LiveList (Dark Blue).png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "LiveList (Dark Blue).png"; sourceTree = "<group>"; }; + B34CDB92221650390031C28B /* Sign-Out (Pink).png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Sign-Out (Pink).png"; sourceTree = "<group>"; }; + B34CDB93221650390031C28B /* Settings Button.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Settings Button.png"; sourceTree = "<group>"; }; + B34CDB942216503A0031C28B /* Sign-In (Orange).png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Sign-In (Orange).png"; sourceTree = "<group>"; }; + B34CDB952216503A0031C28B /* Sign-In (Pink).png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Sign-In (Pink).png"; sourceTree = "<group>"; }; + B34CDB962216503B0031C28B /* Sign-In (Green).png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Sign-In (Green).png"; sourceTree = "<group>"; }; + B34CDB972216503C0031C28B /* Sign-Out (Light Blue).png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Sign-Out (Light Blue).png"; sourceTree = "<group>"; }; + B34CDB982216503C0031C28B /* Sign-Out (Orange).png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Sign-Out (Orange).png"; sourceTree = "<group>"; }; + B34CDB992216503D0031C28B /* Sign-In (Gray).png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Sign-In (Gray).png"; sourceTree = "<group>"; }; + B34CDB9A2216503E0031C28B /* LiveList (Pink).png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "LiveList (Pink).png"; sourceTree = "<group>"; }; + B34CDB9B2216503E0031C28B /* LiveList (Light Blue).png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "LiveList (Light Blue).png"; sourceTree = "<group>"; }; + B34CDB9C2216503F0031C28B /* Sign-In (Dark Blue).png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Sign-In (Dark Blue).png"; sourceTree = "<group>"; }; + B34CDB9D2216503F0031C28B /* Sign-Out (Gray).png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Sign-Out (Gray).png"; sourceTree = "<group>"; }; + B34CDB9E221650400031C28B /* LiveList (Green).png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "LiveList (Green).png"; sourceTree = "<group>"; }; + B38529C9223599850063E0F7 /* Gray-Logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Gray-Logo.png"; sourceTree = "<group>"; }; + B38529CA223599860063E0F7 /* DarkBlue-Logo.PNG */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "DarkBlue-Logo.PNG"; sourceTree = "<group>"; }; + B38529CB223599860063E0F7 /* LightBlue-Logo.PNG */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "LightBlue-Logo.PNG"; sourceTree = "<group>"; }; + B38529CC223599860063E0F7 /* Green-Logo.PNG */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Green-Logo.PNG"; sourceTree = "<group>"; }; + B38529CD223599870063E0F7 /* Pink-Logo.PNG */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Pink-Logo.PNG"; sourceTree = "<group>"; }; + B3D8E1D1220F7847007CAD3A /* TeachersAssistant.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TeachersAssistant.app; sourceTree = BUILT_PRODUCTS_DIR; }; + B3D8E1D4220F7847007CAD3A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; + B3D8E1D6220F7847007CAD3A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; }; + B3D8E1D9220F7847007CAD3A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; }; + B3D8E1DB220F7847007CAD3A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; + B3D8E1DE220F7847007CAD3A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; + B3D8E1E0220F7847007CAD3A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + B3D8E1E5220F7848007CAD3A /* TeachersAssistantTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TeachersAssistantTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + B3D8E1E9220F7848007CAD3A /* Teachers__AssistantTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Teachers__AssistantTests.swift; sourceTree = "<group>"; }; + B3D8E1EB220F7848007CAD3A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + B3D8E1FA220F8172007CAD3A /* ViewControllerHome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerHome.swift; sourceTree = "<group>"; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + B3D8E1CE220F7847007CAD3A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D05B90C2A1C660E42AA56AE /* Pods_TeachersAssistant.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B3D8E1E2220F7848007CAD3A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 906A6F4745D1FA15B86160B9 /* Pods_TeachersAssistantTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 795C191EFA61C2D1AEAF78DB /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8D10E8B200C95EFE160F2EC8 /* Pods_TeachersAssistant.framework */, + 11EF5C302B8B1C8D48F3D0F2 /* Pods_TeachersAssistantTests.framework */, + ); + name = Frameworks; + sourceTree = "<group>"; + }; + B34CDBB2221650480031C28B /* Images */ = { + isa = PBXGroup; + children = ( + B34CDB91221650380031C28B /* LiveList (Dark Blue).png */, + B34CDB8E221650370031C28B /* LiveList (Gray).png */, + B34CDB9E221650400031C28B /* LiveList (Green).png */, + B34CDB9B2216503E0031C28B /* LiveList (Light Blue).png */, + B34CDB8F221650370031C28B /* LiveList (Orange).png */, + B34CDB9A2216503E0031C28B /* LiveList (Pink).png */, + B34CDB93221650390031C28B /* Settings Button.png */, + B34CDB9C2216503F0031C28B /* Sign-In (Dark Blue).png */, + B30366152221E45900BB9DC6 /* Brandeis_Web_HiRes_Orange.png */, + B34CDB992216503D0031C28B /* Sign-In (Gray).png */, + B34CDB962216503B0031C28B /* Sign-In (Green).png */, + B34CDB8D221650360031C28B /* Sign-In (Light Blue).png */, + B34CDB942216503A0031C28B /* Sign-In (Orange).png */, + B34CDB952216503A0031C28B /* Sign-In (Pink).png */, + B38529C9223599850063E0F7 /* Gray-Logo.png */, + B38529CB223599860063E0F7 /* LightBlue-Logo.PNG */, + B38529CA223599860063E0F7 /* DarkBlue-Logo.PNG */, + B38529CC223599860063E0F7 /* Green-Logo.PNG */, + B38529CD223599870063E0F7 /* Pink-Logo.PNG */, + B34CDB8C221650360031C28B /* Sign-Out (Dark Blue).png */, + B34CDB9D2216503F0031C28B /* Sign-Out (Gray).png */, + B34CDB90221650370031C28B /* Sign-Out (Green).png */, + B34CDB972216503C0031C28B /* Sign-Out (Light Blue).png */, + B34CDB982216503C0031C28B /* Sign-Out (Orange).png */, + B34CDB92221650390031C28B /* Sign-Out (Pink).png */, + ); + path = Images; + sourceTree = "<group>"; + }; + B3D8E1C8220F7847007CAD3A = { + isa = PBXGroup; + children = ( + B3D8E1D3220F7847007CAD3A /* Teachers' Assistant */, + B3D8E1E8220F7848007CAD3A /* Teachers' AssistantTests */, + B3D8E1D2220F7847007CAD3A /* Products */, + BC8BCABAA51FEDF2839E3005 /* Pods */, + 795C191EFA61C2D1AEAF78DB /* Frameworks */, + ); + sourceTree = "<group>"; + }; + B3D8E1D2220F7847007CAD3A /* Products */ = { + isa = PBXGroup; + children = ( + B3D8E1D1220F7847007CAD3A /* TeachersAssistant.app */, + B3D8E1E5220F7848007CAD3A /* TeachersAssistantTests.xctest */, + ); + name = Products; + sourceTree = "<group>"; + }; + B3D8E1D3220F7847007CAD3A /* Teachers' Assistant */ = { + isa = PBXGroup; + children = ( + B3D8E1D4220F7847007CAD3A /* AppDelegate.swift */, + B3D8E1D6220F7847007CAD3A /* ViewController.swift */, + B3D8E1FA220F8172007CAD3A /* ViewControllerHome.swift */, + B30366172221E60C00BB9DC6 /* ViewControllerSettings.swift */, + B3D8E1D8220F7847007CAD3A /* Main.storyboard */, + B3D8E1DB220F7847007CAD3A /* Assets.xcassets */, + B3D8E1DD220F7847007CAD3A /* LaunchScreen.storyboard */, + B3D8E1E0220F7847007CAD3A /* Info.plist */, + B34CDBB2221650480031C28B /* Images */, + ); + path = "Teachers' Assistant"; + sourceTree = "<group>"; + }; + B3D8E1E8220F7848007CAD3A /* Teachers' AssistantTests */ = { + isa = PBXGroup; + children = ( + B3D8E1E9220F7848007CAD3A /* Teachers__AssistantTests.swift */, + B3D8E1EB220F7848007CAD3A /* Info.plist */, + ); + path = "Teachers' AssistantTests"; + sourceTree = "<group>"; + }; + BC8BCABAA51FEDF2839E3005 /* Pods */ = { + isa = PBXGroup; + children = ( + 800B9D84351418F21FB4E4AC /* Pods-TeachersAssistant.debug.xcconfig */, + 1723407E824D6432322BF145 /* Pods-TeachersAssistant.release.xcconfig */, + 7088E7DE163DD403B94F95F6 /* Pods-TeachersAssistantTests.debug.xcconfig */, + 3FCDA47A9F6775F2E7818B5F /* Pods-TeachersAssistantTests.release.xcconfig */, + ); + path = Pods; + sourceTree = "<group>"; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + B3D8E1D0220F7847007CAD3A /* TeachersAssistant */ = { + isa = PBXNativeTarget; + buildConfigurationList = B3D8E1EE220F7848007CAD3A /* Build configuration list for PBXNativeTarget "TeachersAssistant" */; + buildPhases = ( + 7D432762A029A21E1E5010CF /* [CP] Check Pods Manifest.lock */, + B3D8E1CD220F7847007CAD3A /* Sources */, + B3D8E1CE220F7847007CAD3A /* Frameworks */, + B3D8E1CF220F7847007CAD3A /* Resources */, + 95712E6662D48FC472676213 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TeachersAssistant; + productName = "Teachers' Assistant"; + productReference = B3D8E1D1220F7847007CAD3A /* TeachersAssistant.app */; + productType = "com.apple.product-type.application"; + }; + B3D8E1E4220F7848007CAD3A /* TeachersAssistantTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = B3D8E1F1220F7848007CAD3A /* Build configuration list for PBXNativeTarget "TeachersAssistantTests" */; + buildPhases = ( + 65EE7C1F76479D92B06A2476 /* [CP] Check Pods Manifest.lock */, + B3D8E1E1220F7848007CAD3A /* Sources */, + B3D8E1E2220F7848007CAD3A /* Frameworks */, + B3D8E1E3220F7848007CAD3A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + B3D8E1E7220F7848007CAD3A /* PBXTargetDependency */, + ); + name = TeachersAssistantTests; + productName = "Teachers' AssistantTests"; + productReference = B3D8E1E5220F7848007CAD3A /* TeachersAssistantTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + B3D8E1C9220F7847007CAD3A /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 0920; + ORGANIZATIONNAME = "FIGBERT Inc"; + TargetAttributes = { + B3D8E1D0220F7847007CAD3A = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + }; + B3D8E1E4220F7848007CAD3A = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + TestTargetID = B3D8E1D0220F7847007CAD3A; + }; + }; + }; + buildConfigurationList = B3D8E1CC220F7847007CAD3A /* Build configuration list for PBXProject "TeachersAssistant" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = B3D8E1C8220F7847007CAD3A; + productRefGroup = B3D8E1D2220F7847007CAD3A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + B3D8E1D0220F7847007CAD3A /* TeachersAssistant */, + B3D8E1E4220F7848007CAD3A /* TeachersAssistantTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + B3D8E1CF220F7847007CAD3A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B34CDBB0221650410031C28B /* Sign-Out (Gray).png in Resources */, + B38529CE223599870063E0F7 /* Gray-Logo.png in Resources */, + B3D8E1DF220F7847007CAD3A /* LaunchScreen.storyboard in Resources */, + B34CDBA2221650410031C28B /* LiveList (Orange).png in Resources */, + B34CDBA7221650410031C28B /* Sign-In (Orange).png in Resources */, + B34CDBAA221650410031C28B /* Sign-Out (Light Blue).png in Resources */, + B34CDBA4221650410031C28B /* LiveList (Dark Blue).png in Resources */, + B38529D0223599870063E0F7 /* LightBlue-Logo.PNG in Resources */, + B34CDBAB221650410031C28B /* Sign-Out (Orange).png in Resources */, + B34CDBAC221650410031C28B /* Sign-In (Gray).png in Resources */, + B38529D1223599870063E0F7 /* Green-Logo.PNG in Resources */, + B34CDBB1221650410031C28B /* LiveList (Green).png in Resources */, + B30366162221E45A00BB9DC6 /* Brandeis_Web_HiRes_Orange.png in Resources */, + B34CDBA6221650410031C28B /* Settings Button.png in Resources */, + B34CDBA8221650410031C28B /* Sign-In (Pink).png in Resources */, + B34CDB9F221650410031C28B /* Sign-Out (Dark Blue).png in Resources */, + B3D8E1DC220F7847007CAD3A /* Assets.xcassets in Resources */, + B34CDBA5221650410031C28B /* Sign-Out (Pink).png in Resources */, + B34CDBA0221650410031C28B /* Sign-In (Light Blue).png in Resources */, + B38529D2223599870063E0F7 /* Pink-Logo.PNG in Resources */, + B34CDBAE221650410031C28B /* LiveList (Light Blue).png in Resources */, + B38529CF223599870063E0F7 /* DarkBlue-Logo.PNG in Resources */, + B34CDBA9221650410031C28B /* Sign-In (Green).png in Resources */, + B34CDBA1221650410031C28B /* LiveList (Gray).png in Resources */, + B34CDBA3221650410031C28B /* Sign-Out (Green).png in Resources */, + B3D8E1DA220F7847007CAD3A /* Main.storyboard in Resources */, + B34CDBAF221650410031C28B /* Sign-In (Dark Blue).png in Resources */, + B34CDBAD221650410031C28B /* LiveList (Pink).png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B3D8E1E3220F7848007CAD3A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 65EE7C1F76479D92B06A2476 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-TeachersAssistantTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 7D432762A029A21E1E5010CF /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-TeachersAssistant-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 95712E6662D48FC472676213 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", + "${BUILT_PRODUCTS_DIR}/GoogleAPIClientForREST/GoogleAPIClientForREST.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleAPIClientForREST.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-TeachersAssistant/Pods-TeachersAssistant-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + B3D8E1CD220F7847007CAD3A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B3D8E1D7220F7847007CAD3A /* ViewController.swift in Sources */, + B3D8E1FB220F8172007CAD3A /* ViewControllerHome.swift in Sources */, + B3D8E1D5220F7847007CAD3A /* AppDelegate.swift in Sources */, + B30366182221E60C00BB9DC6 /* ViewControllerSettings.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B3D8E1E1220F7848007CAD3A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B3D8E1EA220F7848007CAD3A /* Teachers__AssistantTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + B3D8E1E7220F7848007CAD3A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B3D8E1D0220F7847007CAD3A /* TeachersAssistant */; + targetProxy = B3D8E1E6220F7848007CAD3A /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + B3D8E1D8220F7847007CAD3A /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + B3D8E1D9220F7847007CAD3A /* Base */, + ); + name = Main.storyboard; + sourceTree = "<group>"; + }; + B3D8E1DD220F7847007CAD3A /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + B3D8E1DE220F7847007CAD3A /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = "<group>"; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + B3D8E1EC220F7848007CAD3A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.2; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + B3D8E1ED220F7848007CAD3A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.2; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + B3D8E1EF220F7848007CAD3A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 800B9D84351418F21FB4E4AC /* Pods-TeachersAssistant.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 9WHCFZ6J4N; + INFOPLIST_FILE = "Teachers' Assistant/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.figbertinc.Teachers--Assistant"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + B3D8E1F0220F7848007CAD3A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1723407E824D6432322BF145 /* Pods-TeachersAssistant.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 9WHCFZ6J4N; + INFOPLIST_FILE = "Teachers' Assistant/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.figbertinc.Teachers--Assistant"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + B3D8E1F2220F7848007CAD3A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7088E7DE163DD403B94F95F6 /* Pods-TeachersAssistantTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 9WHCFZ6J4N; + INFOPLIST_FILE = "Teachers' AssistantTests/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.figbertinc.Teachers--AssistantTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TeachersAssistant.app/TeachersAssistant"; + }; + name = Debug; + }; + B3D8E1F3220F7848007CAD3A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3FCDA47A9F6775F2E7818B5F /* Pods-TeachersAssistantTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 9WHCFZ6J4N; + INFOPLIST_FILE = "Teachers' AssistantTests/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.figbertinc.Teachers--AssistantTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TeachersAssistant.app/TeachersAssistant"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + B3D8E1CC220F7847007CAD3A /* Build configuration list for PBXProject "TeachersAssistant" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B3D8E1EC220F7848007CAD3A /* Debug */, + B3D8E1ED220F7848007CAD3A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B3D8E1EE220F7848007CAD3A /* Build configuration list for PBXNativeTarget "TeachersAssistant" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B3D8E1EF220F7848007CAD3A /* Debug */, + B3D8E1F0220F7848007CAD3A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B3D8E1F1220F7848007CAD3A /* Build configuration list for PBXNativeTarget "TeachersAssistantTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B3D8E1F2220F7848007CAD3A /* Debug */, + B3D8E1F3220F7848007CAD3A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = B3D8E1C9220F7847007CAD3A /* Project object */; +} diff --git a/TeachersAssistant.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/TeachersAssistant.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Workspace + version = "1.0"> + <FileRef + location = "self:Teachers&apos; Assistant.xcodeproj"> + </FileRef> +</Workspace> diff --git a/TeachersAssistant.xcodeproj/xcuserdata/naomi.xcuserdatad/xcschemes/xcschememanagement.plist b/TeachersAssistant.xcodeproj/xcuserdata/naomi.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>SchemeUserState</key> + <dict> + <key>Teachers' Assistant.xcscheme</key> + <dict> + <key>orderHint</key> + <integer>4</integer> + </dict> + <key>TeachersAssistant.xcscheme</key> + <dict> + <key>orderHint</key> + <integer>4</integer> + </dict> + </dict> +</dict> +</plist> diff --git a/TeachersAssistant.xcworkspace/contents.xcworkspacedata b/TeachersAssistant.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Workspace + version = "1.0"> + <FileRef + location = "group:TeachersAssistant.xcodeproj"> + </FileRef> + <FileRef + location = "group:Pods/Pods.xcodeproj"> + </FileRef> +</Workspace> diff --git a/TeachersAssistant.xcworkspace/xcuserdata/naomi.xcuserdatad/UserInterfaceState.xcuserstate b/TeachersAssistant.xcworkspace/xcuserdata/naomi.xcuserdatad/UserInterfaceState.xcuserstate Binary files differ.