Skip to content

Commit

Permalink
Added Privacy Manifests for Login Kit
Browse files Browse the repository at this point in the history
Summary: Added Privacy Manifests

Differential Revision: D54642608

fbshipit-source-id: 733b848a36a701e366f8c44e74fd3a8fe10fb3a5
  • Loading branch information
Alejandro Guzmán authored and facebook-github-bot committed Mar 11, 2024
1 parent af20a84 commit db78ef7
Show file tree
Hide file tree
Showing 23 changed files with 885 additions and 167 deletions.
12 changes: 12 additions & 0 deletions FBSDKCoreKit/FBSDKCoreKit/FBProfilePictureView.swift
Expand Up @@ -6,6 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/

import AppTrackingTransparency
import UIKit

/// A view to display a profile picture.
Expand Down Expand Up @@ -195,6 +196,17 @@ public final class FBProfilePictureView: UIView {
}

lastState = nil

// We don't want to reset the user picture in case the login shim solution is being used.
// The login shim flow doesn't provide a valid access token to fetch the image, it
// leverages limited login implementation
if #available(iOS 14, *) {
let trackingAuthorizationStatus = ATTrackingManager.trackingAuthorizationStatus
if trackingAuthorizationStatus != .authorized {
return
}
}

updateImageWithAccessToken()
}

Expand Down
Expand Up @@ -415,7 +415,12 @@ - (void)start
[self taskDidCompleteWithResponse:responseV1 data:responseDataV1 requestStartTime:self.requestStartTime handler:handler];
}
};
[self.session executeURLRequest:request completionHandler:completionHandler];

if ([[FBSDKShimGraphRequestInterceptor shared] shouldInterceptRequest:request]) {
[[FBSDKShimGraphRequestInterceptor shared] executeWithRequest:request completionHandler:completionHandler];
} else {
[self.session executeURLRequest:request completionHandler:completionHandler];
}

id<FBSDKGraphRequestConnectionDelegate> delegate = self.delegate;
if ([delegate respondsToSelector:@selector(requestConnectionWillBeginLoading:)]) {
Expand Down
196 changes: 196 additions & 0 deletions FBSDKCoreKit/FBSDKCoreKit/GraphAPI/ShimGraphRequestInterceptor.swift
@@ -0,0 +1,196 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/

import AuthenticationServices
import Foundation

/**
Internal class exposed to facilitate transition to Swift.
API Subject to change or removal without warning. Do not use.
@warning INTERNAL - DO NOT USE
*/
@objcMembers
@objc(FBSDKShimGraphRequestInterceptor)
public final class ShimGraphRequestInterceptor: NSObject {

private enum GraphAPIPath: String, CaseIterable {
// swiftlint:disable:next identifier_name
case me = "/me"
case friends = "/friends"
case picture = "/picture"
// Add more cases as needed

static var regexPattern: String {
// Map each case to its raw value, escape the slashes for the regex, and join them with "|"
let graphAPIPaths: [GraphAPIPath] = [.friends, .picture]
let paths = graphAPIPaths
.map { $0.rawValue.replacingOccurrences(of: "/", with: "\\/") }
.joined(separator: "|")

// This part of the regex matches the optional version prefix, e.g. "/v17.0"
// "\\/v\\d+\\.\\d+" matches "/v", followed by one or more digits, ".", and one or more digits
// The "?" makes this part optional
let versionPattern = "(\\/v\\d+\\.\\d+)?"

// This part of the regex matches the path
// "\(paths)" matches any of the paths generated from the enum cases
// The "?" makes this part optional
let pathPattern = "(\(paths))?"

// The "^" at the start and "$" at the end ensure that the entire string must match the pattern
// The full regex matches an optional version prefix, followed by a slash, followed by one of the paths
return "^\(versionPattern)\\/me\(pathPattern)$"
}
}

private enum GraphAPIResponseKeys: String {
case accessToken = "access_token"
case format
case includesHeaders = "include_headers"
case sdk
case contentType = "Content-Type"
case redirect
}

private enum GraphAPIResponseValues: String {
case json
case `false`
case ios
case imageType = "image/jpeg"
case pictureRedirectData = "0"
}

public static let shared = ShimGraphRequestInterceptor()
private var urlResponseURLComponents: URLComponents?
private var currentGraphAPIPath: GraphAPIPath = .me
private var currentURLRequest: URLRequest?

// MARK: - Request Interception

@objc(shouldInterceptRequest:)
public func shouldInterceptRequest(_ request: URLRequest) -> Bool {
if _DomainHandler.sharedInstance().isDomainHandlingEnabled(), !Settings.shared.isAdvertiserTrackingEnabled {
return isGraphAPIShimSupportedRequest(request)
} else {
return false
}
}

private func isGraphAPIShimSupportedRequest(_ request: URLRequest) -> Bool {
guard let urlPath = request.url?.path else {
return false
}

if urlPath.range(of: GraphAPIPath.regexPattern, options: .regularExpression) != nil {
for path in GraphAPIPath.allCases {
if urlPath.hasSuffix(path.rawValue) {
currentGraphAPIPath = path
currentURLRequest = request
return true
}
}
}

return false
}

private func shouldSendPictureData(for request: URLRequest) -> Bool {
guard let url = request.url,
let urlComponents = URLComponents(string: url.absoluteString),
let queries = urlComponents.queryItems
else {
return false
}

for query in queries {
if query.name == GraphAPIResponseKeys.redirect.rawValue,
let redirect = query.value,
redirect == GraphAPIResponseValues.pictureRedirectData.rawValue {
return true
}
}

return false
}

private func handlePictureData(_ urlResponse: URL, completionHandler: @escaping UrlSessionTaskBlock) {
var httpURLResponse = HTTPURLResponse(url: urlResponse, statusCode: 200, httpVersion: nil, headerFields: nil)

guard let currentURLRequest = currentURLRequest, shouldSendPictureData(for: currentURLRequest) else {
let headerFields = [GraphAPIResponseKeys.contentType.rawValue: GraphAPIResponseValues.imageType.rawValue]
httpURLResponse = HTTPURLResponse(
url: urlResponse,
statusCode: 200,
httpVersion: nil,
headerFields: headerFields
)
completionHandler(nil, httpURLResponse, nil)
return
}

let responseData = Profile.current?.pictureData
completionHandler(responseData, httpURLResponse, nil)
}

// MARK: - Requests Handling

@objc(executeWithRequest:completionHandler:)
public func execute(request: URLRequest, completionHandler: @escaping UrlSessionTaskBlock) {
if let urlString = request.url?.absoluteString {
urlResponseURLComponents = URLComponents(string: urlString)
}
makeShimRequest(completionHandler: completionHandler)
}

func makeShimRequest(completionHandler: @escaping UrlSessionTaskBlock) {
let nsError = NSError(domain: "Could not make graph API request", code: -1, userInfo: nil)

guard
let urlResponseURLComponents = urlResponseURLComponents?.url,
let scheme = urlResponseURLComponents.scheme,
let host = urlResponseURLComponents.host
else {
completionHandler(nil, nil, nsError)
return
}
let path = urlResponseURLComponents.path
let baseURLString = "\(scheme)://\(host)\(path)"

var urlComponents = URLComponents(string: baseURLString)
urlComponents?.queryItems = [
URLQueryItem(name: GraphAPIResponseKeys.accessToken.rawValue, value: AccessToken.current?.tokenString),
URLQueryItem(name: GraphAPIResponseKeys.format.rawValue, value: GraphAPIResponseValues.json.rawValue),
URLQueryItem(name: GraphAPIResponseKeys.includesHeaders.rawValue, value: GraphAPIResponseValues.false.rawValue),
URLQueryItem(name: GraphAPIResponseKeys.sdk.rawValue, value: GraphAPIResponseValues.ios.rawValue),
]

guard let responseURL = urlComponents?.url else {
completionHandler(nil, nil, nsError)
return
}

let httpURLResponse = HTTPURLResponse(url: responseURL, statusCode: 200, httpVersion: nil, headerFields: nil)

var responseData: Data?
switch currentGraphAPIPath {
case .me:
responseData = Profile.current?.profileToData
case .friends:
responseData = Profile.current?.userFriendsData
case .picture:
handlePictureData(responseURL, completionHandler: completionHandler)
return
}
guard let responseData = responseData else {
completionHandler(nil, nil, nsError)
return
}
completionHandler(responseData, httpURLResponse, nil)
}
}
106 changes: 106 additions & 0 deletions FBSDKCoreKit/FBSDKCoreKit/Profile+Loading.swift
Expand Up @@ -17,6 +17,88 @@ extension Profile {
return formatter
}()

var profileToData: Data? {

var profileDict: [String: Any?] = [
Field.identifier.rawValue: userID,
Field.firstName.rawValue: firstName,
Field.middleName.rawValue: middleName,
Field.lastName.rawValue: lastName,
Field.fullName.rawValue: name,
Field.link.rawValue: linkURL?.absoluteString,
Field.email.rawValue: email,
Field.friends.rawValue: friendIDs,
Field.hometown.rawValue: hometown,
Field.gender.rawValue: gender,
]

if let birthday = birthday {
profileDict[Field.birthday.rawValue] = Self.dateFormatter.string(from: birthday)
}

if let location = location {
var locationDict = [String: String]()
locationDict[ResponseKey.identifier.rawValue] = location.id
locationDict[ResponseKey.name.rawValue] = location.name
profileDict[ResponseKey.location.rawValue] = locationDict
}

if let hometown = hometown {
var hometownDict = [String: String]()
hometownDict[ResponseKey.identifier.rawValue] = hometown.id
hometownDict[ResponseKey.name.rawValue] = hometown.name
profileDict[ResponseKey.hometown.rawValue] = hometownDict
}

if let ageRange = ageRange {
var ageRangeDict = [String: Any]()
ageRangeDict[UserAgeRangeKey.min.rawValue] = ageRange.min?.intValue
ageRangeDict[UserAgeRangeKey.max.rawValue] = ageRange.max?.intValue
profileDict[ResponseKey.ageRange.rawValue] = ageRangeDict
}

if let imageURL = imageURL {
var pictureAttributes = [String: Any]()
pictureAttributes[PictureKey.height.rawValue] = 100
pictureAttributes[PictureKey.width.rawValue] = 100
pictureAttributes[PictureKey.isSilhouette.rawValue] = false
pictureAttributes[PictureKey.url.rawValue] = imageURL.absoluteString
profileDict[Field.picture.rawValue] = [ResponseKey.data.rawValue: pictureAttributes]
}

return try? JSONSerialization.data(withJSONObject: profileDict.compactMapValues { $0 }, options: [])
}

var userFriendsData: Data? {
var userFriendsDict = [String: Any]()
var userData = [[String: String]]()

if let friendIDs = friendIDs {
for friendID in friendIDs {
let friend = [ResponseKey.identifier.rawValue: friendID]
userData.append(friend)
}
}

userFriendsDict[ResponseKey.data.rawValue] = userData
return try? JSONSerialization.data(withJSONObject: userFriendsDict, options: [])
}

var pictureData: Data? {
var pictureDict = [String: Any]()
var pictureData = [String: Any]()

if let imageURL = imageURL {
pictureData[PictureKey.height.rawValue] = 100
pictureData[PictureKey.width.rawValue] = 100
pictureData[PictureKey.isSilhouette.rawValue] = false
pictureData[PictureKey.url.rawValue] = imageURL.absoluteString
}

pictureDict[ResponseKey.data.rawValue] = pictureData
return try? JSONSerialization.data(withJSONObject: pictureDict, options: [])
}

/**
Loads the current profile and passes it to the completion block.
Expand Down Expand Up @@ -197,6 +279,7 @@ extension Profile {
case name
case email
case data
case permissions
}

private enum URLValues {
Expand All @@ -219,6 +302,29 @@ extension Profile {
case hometown
case location
case gender
case permissions
case picture
}

private enum UserAgeRangeKey: String {
case min
case max
}

private enum PictureKey: String {
case height
case width
case isSilhouette = "is_silhouette"
case url
}

private enum PermissionKey: String {
case permission
case status
}

private enum PermissionStatusValue: String {
case granted
}

private static let expirationInterval = TimeInterval(
Expand Down

0 comments on commit db78ef7

Please sign in to comment.