-
Notifications
You must be signed in to change notification settings - Fork 24.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement privacy manifest aggregation #44214
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
module PrivacyManifestUtils | ||
def self.add_aggregated_privacy_manifest(installer) | ||
user_project = get_user_project_from(installer) | ||
targets = get_application_targets(user_project) | ||
file_path = get_privacyinfo_file_path(user_project) | ||
|
||
privacy_info = read_privacyinfo_file(file_path) || { | ||
"NSPrivacyCollectedDataTypes" => [], | ||
"NSPrivacyTracking" => false | ||
} | ||
|
||
# Get all required reason APIs defined in current pods | ||
required_reason_apis = get_used_required_reason_apis(installer) | ||
|
||
# Add the Required Reason APIs from React Native core | ||
get_core_accessed_apis.each do |accessed_api| | ||
api_type = accessed_api["NSPrivacyAccessedAPIType"] | ||
reasons = accessed_api["NSPrivacyAccessedAPITypeReasons"] | ||
required_reason_apis[api_type] ||= [] | ||
required_reason_apis[api_type] += reasons | ||
end | ||
|
||
# Merge the Required Reason APIs from pods with the ones from the existing PrivacyInfo file | ||
(privacy_info["NSPrivacyAccessedAPITypes"] || []).each do |accessed_api| | ||
api_type = accessed_api["NSPrivacyAccessedAPIType"] | ||
reasons = accessed_api["NSPrivacyAccessedAPITypeReasons"] | ||
# Add reasons from existing PrivacyInfo file to the ones from pods | ||
required_reason_apis[api_type] ||= [] | ||
required_reason_apis[api_type] += reasons | ||
end | ||
|
||
# Update the existing PrivacyInfo file with the new aggregated data | ||
privacy_info["NSPrivacyAccessedAPITypes"] = required_reason_apis.map { |api_type, reasons| | ||
{ | ||
"NSPrivacyAccessedAPIType" => api_type, | ||
"NSPrivacyAccessedAPITypeReasons" => reasons.uniq | ||
} | ||
} | ||
|
||
Xcodeproj::Plist.write_to_path(privacy_info, file_path) | ||
|
||
targets.each do |target| | ||
ensure_reference(file_path, user_project, target) | ||
end | ||
end | ||
|
||
def self.get_application_targets(user_project) | ||
return user_project.targets.filter { |t| t.symbol_type == :application } | ||
end | ||
|
||
def self.read_privacyinfo_file(file_path) | ||
# Maybe add missing default NSPrivacyTracking, NSPrivacyTrackingDomains, NSPrivacyCollectedDataTypes, but this works without those keys | ||
source_data = nil | ||
# Try to read an existing PrivacyInfo.xcprivacy file | ||
begin | ||
source_data = Xcodeproj::Plist.read_from_path(file_path) | ||
Pod::UI.puts "[Privacy Manifest Aggregation] Appending aggregated reasons to existing PrivacyInfo.xcprivacy file." | ||
rescue => e | ||
Pod::UI.puts "[Privacy Manifest Aggregation] No existing PrivacyInfo.xcprivacy file found, creating a new one." | ||
end | ||
return source_data | ||
end | ||
|
||
def self.ensure_reference(file_path, user_project, target) | ||
reference_exists = target.resources_build_phase.files_references.any? { |file_ref| file_ref.path.end_with? "PrivacyInfo.xcprivacy" } | ||
unless reference_exists | ||
# We try to find the main group, but if it doesn't exist, we default to adding the file to the project root – both work | ||
file_root = user_project.root_object.main_group.children.first { |group| group.name == target.name } || user_project | ||
file_ref = file_root.new_file(file_path) | ||
build_file = target.resources_build_phase.add_file_reference(file_ref, true) | ||
aleqsio marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end | ||
end | ||
|
||
def self.get_privacyinfo_file_path(user_project) | ||
# We try to find a file we know exists in the project to get the path to the main group directory | ||
info_plist_path = user_project.files.find { |file_ref| file_ref.name == "Info.plist" } | ||
if info_plist_path.nil? | ||
# return path that is sibling to .xcodeproj | ||
path = user_project.path | ||
return File.join(File.dirname(path), "PrivacyInfo.xcprivacy") | ||
end | ||
return File.join(File.dirname(info_plist_path.real_path),"PrivacyInfo.xcprivacy") | ||
end | ||
|
||
def self.get_used_required_reason_apis(installer) | ||
# A dictionary with keys of type string (NSPrivacyAccessedAPIType) and values of type string[] (NSPrivacyAccessedAPITypeReasons[]) | ||
used_apis = {} | ||
Pod::UI.puts "[Privacy Manifest Aggregation] Reading .xcprivacy files to aggregate all used Required Reason APIs." | ||
installer.pod_targets.each do |pod_target| | ||
# puts pod_target | ||
pod_target.file_accessors.each do |file_accessor| | ||
file_accessor.resource_bundles.each do |bundle_name, bundle_files| | ||
bundle_files.each do |file_path| | ||
# This needs to be named like that due to apple requirements | ||
if File.basename(file_path) == 'PrivacyInfo.xcprivacy' | ||
content = Xcodeproj::Plist.read_from_path(file_path) | ||
accessed_api_types = content["NSPrivacyAccessedAPITypes"] | ||
accessed_api_types.each do |accessed_api| | ||
api_type = accessed_api["NSPrivacyAccessedAPIType"] | ||
reasons = accessed_api["NSPrivacyAccessedAPITypeReasons"] | ||
used_apis[api_type] ||= [] | ||
used_apis[api_type] += reasons | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end | ||
return used_apis | ||
end | ||
|
||
def self.get_privacy_manifest_paths_from(user_project) | ||
aleqsio marked this conversation as resolved.
Show resolved
Hide resolved
|
||
privacy_manifests = user_project | ||
.files | ||
.select { |p| | ||
p.path&.end_with?('PrivacyInfo.xcprivacy') | ||
} | ||
return privacy_manifests | ||
end | ||
|
||
def self.get_core_accessed_apis() | ||
file_timestamp_accessed_api = { | ||
"NSPrivacyAccessedAPIType" => "NSPrivacyAccessedAPICategoryFileTimestamp", | ||
"NSPrivacyAccessedAPITypeReasons" => ["C617.1"], | ||
} | ||
user_defaults_accessed_api = { | ||
"NSPrivacyAccessedAPIType" => "NSPrivacyAccessedAPICategoryUserDefaults", | ||
"NSPrivacyAccessedAPITypeReasons" => ["CA92.1"], | ||
} | ||
boot_time_accessed_api = { | ||
"NSPrivacyAccessedAPIType" => "NSPrivacyAccessedAPICategorySystemBootTime", | ||
"NSPrivacyAccessedAPITypeReasons" => ["35F9.1"], | ||
} | ||
return [file_timestamp_accessed_api, user_defaults_accessed_api, boot_time_accessed_api] | ||
end | ||
|
||
|
||
def self.get_user_project_from(installer) | ||
user_project = installer.aggregate_targets | ||
.map{ |t| t.user_project } | ||
.first | ||
return user_project | ||
end | ||
|
||
def self.add_privacy_manifest_if_needed(installer) | ||
user_project = get_user_project_from(installer) | ||
privacy_manifest = self.get_privacy_manifest_paths_from(user_project).first | ||
if privacy_manifest.nil? | ||
privacy_manifest = { | ||
"NSPrivacyCollectedDataTypes" => [], | ||
"NSPrivacyTracking" => false, | ||
"NSPrivacyAccessedAPITypes" => get_core_accessed_apis | ||
} | ||
path = File.join(user_project.path.parent, "PrivacyInfo.xcprivacy") | ||
Xcodeproj::Plist.write_to_path(privacy_manifest, path) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. feels we can reuse ensure_reference here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess it depends on what we want this logic to do:
Since this code path runs only if a user opts-out of the aggregation step, I'd keep it as simple as possible, do 1), and treat it as a "hey, we noticed you don't have a manifest and decided to opt-out of aggregation, here's a sample file you could use that we placed in your project root but didn't touch your actual ios project". |
||
Pod::UI.puts "Your app does not have a privacy manifest! A template has been generated containing Required Reasons API usage in the core React Native library. Please add the PrivacyInfo.xcprivacy file to your project and complete data use, tracking and any additional required reasons your app is using according to Apple's guidance: https://developer.apple.com/documentation/bundleresources/privacy_manifest_files. Then, you will need to manually add this file to your project in Xcode.".red | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@aleqsio I'm trying to backport this to 0.72 in advance of reactwg/react-native-releases#279 being done. I think this line might be naive/incorrect.
I believe I've integrated the changes into 0.72.14 correctly but when I try and pod install I get:
I've used pry to debug what's going on. It looks like the
.first
should be a.find
(first only returns the first item in a collection) and, in my case.first
is not a group, it's an xcconfig file:Equivalent project structure view in Xcode:
While debugging, I also discovered that the groups have
nil
names, I think we want to match againstpath
?I've changed the line as follows:
and it now integrates, finding the group name that matches my project name and adding the privacy manifest file to there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are absolutely correct, thanks for flagging 😍
The reason why it passed through my testing was that the privacymanifest file was the first in the array in rntester, AND the code block got swallowed without error – joys of ruby programming I guess.