From 2274ec84cb717bcb211bd9cc8409d50e837f3e5d Mon Sep 17 00:00:00 2001
From: bruvzg <7645683+bruvzg@users.noreply.github.com>
Date: Tue, 30 Apr 2024 09:39:20 +0300
Subject: [PATCH] [3.x, iOS export] Add support for privacy manifest
configuration.
---
misc/dist/ios_xcode/PrivacyInfo.xcprivacy | 10 +
.../godot_ios.xcodeproj/project.pbxproj | 4 +
platform/iphone/export/export.cpp | 222 ++++++++++++++++++
3 files changed, 236 insertions(+)
create mode 100644 misc/dist/ios_xcode/PrivacyInfo.xcprivacy
diff --git a/misc/dist/ios_xcode/PrivacyInfo.xcprivacy b/misc/dist/ios_xcode/PrivacyInfo.xcprivacy
new file mode 100644
index 00000000000..bc4a893d587
--- /dev/null
+++ b/misc/dist/ios_xcode/PrivacyInfo.xcprivacy
@@ -0,0 +1,10 @@
+
+
+
+
+ NSPrivacyAccessedAPITypes
+ $priv_api_types
+ $priv_tracking
+ $priv_collection
+
+
diff --git a/misc/dist/ios_xcode/godot_ios.xcodeproj/project.pbxproj b/misc/dist/ios_xcode/godot_ios.xcodeproj/project.pbxproj
index c172952f6db..c816515c24d 100644
--- a/misc/dist/ios_xcode/godot_ios.xcodeproj/project.pbxproj
+++ b/misc/dist/ios_xcode/godot_ios.xcodeproj/project.pbxproj
@@ -15,6 +15,7 @@
D07CD44E1C5D589C00B7FB28 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D07CD44D1C5D589C00B7FB28 /* Images.xcassets */; };
D0BCFE4618AEBDA2004A7AAE /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0BCFE4418AEBDA2004A7AAE /* InfoPlist.strings */; };
D0BCFE7818AEBFEB004A7AAE /* $binary.pck in Resources */ = {isa = PBXBuildFile; fileRef = D0BCFE7718AEBFEB004A7AAE /* $binary.pck */; };
+ F965960D2BC2C3A800579C7E /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F965960C2BC2C3A800579C7E /* PrivacyInfo.xcprivacy */; };
$pbx_launch_screen_build_reference
/* End PBXBuildFile section */
@@ -45,6 +46,7 @@
D0BCFE4518AEBDA2004A7AAE /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; };
$pbx_locale_file_reference
D0BCFE7718AEBFEB004A7AAE /* $binary.pck */ = {isa = PBXFileReference; lastKnownFileType = file; path = "$binary.pck"; sourceTree = ""; };
+ F965960C2BC2C3A800579C7E /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; };
$pbx_launch_screen_file_reference
/* End PBXFileReference section */
@@ -72,6 +74,7 @@
D0BCFE4118AEBDA2004A7AAE /* $binary */,
D0BCFE3618AEBDA2004A7AAE /* Frameworks */,
D0BCFE3518AEBDA2004A7AAE /* Products */,
+ F965960C2BC2C3A800579C7E /* PrivacyInfo.xcprivacy */,
$additional_pbx_resources_refs
);
sourceTree = "";
@@ -182,6 +185,7 @@
D0BCFE7818AEBFEB004A7AAE /* $binary.pck in Resources */,
$pbx_launch_screen_build_phase
D0BCFE4618AEBDA2004A7AAE /* InfoPlist.strings in Resources */,
+ F965960D2BC2C3A800579C7E /* PrivacyInfo.xcprivacy in Resources */,
$additional_pbx_resources_build
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/platform/iphone/export/export.cpp b/platform/iphone/export/export.cpp
index 0fbb170ca8c..506097d00b5 100644
--- a/platform/iphone/export/export.cpp
+++ b/platform/iphone/export/export.cpp
@@ -321,6 +321,94 @@ Vector EditorExportPlatformIOS::_ge
return archs;
}
+struct APIAccessInfo {
+ String prop_name;
+ String type_name;
+ String prop_flag_value[5];
+ String prop_flag_name[5];
+ int default_value;
+};
+
+static const APIAccessInfo api_info[] = {
+ { "file_timestamp",
+ "NSPrivacyAccessedAPICategoryFileTimestamp",
+ { "DDA9.1", "C617.1", "3B52.1", String() },
+ { "Display to user on-device:", "Inside app or group container", "Files provided to app by user", String() },
+ 3 },
+ { "system_boot_time",
+ "NSPrivacyAccessedAPICategorySystemBootTime",
+ { "35F9.1", "8FFB.1", "3D61.1", String() },
+ { "Measure time on-device", "Calculate absolute event timestamps", "User-initiated bug report", String() },
+ 1 },
+ { "disk_space",
+ "NSPrivacyAccessedAPICategoryDiskSpace",
+ { "E174.1", "85F4.1", "7D9E.1", "B728.1", String() },
+ { "Write or delete file on-device", "Display to user on-device", "User-initiated bug report", "Health research app", String() },
+ 3 },
+ { "active_keyboard",
+ "NSPrivacyAccessedAPICategoryActiveKeyboards",
+ { "3EC4.1", "54BD.1", String() },
+ { "Custom keyboard app on-device", "Customize UI on-device:2", String() },
+ 0 },
+ { "user_defaults",
+ "NSPrivacyAccessedAPICategoryUserDefaults",
+ { "1C8F.1", "AC6B.1", "CA92.1", String() },
+ { "Access info from same App Group", "Access managed app configuration", "Access info from same app", String() },
+ 0 }
+};
+
+struct DataCollectionInfo {
+ String prop_name;
+ String type_name;
+};
+
+static const DataCollectionInfo data_collect_type_info[] = {
+ { "name", "NSPrivacyCollectedDataTypeName" },
+ { "email_address", "NSPrivacyCollectedDataTypeEmailAddress" },
+ { "phone_number", "NSPrivacyCollectedDataTypePhoneNumber" },
+ { "physical_address", "NSPrivacyCollectedDataTypePhysicalAddress" },
+ { "other_contact_info", "NSPrivacyCollectedDataTypeOtherUserContactInfo" },
+ { "health", "NSPrivacyCollectedDataTypeHealth" },
+ { "fitness", "NSPrivacyCollectedDataTypeFitness" },
+ { "payment_info", "NSPrivacyCollectedDataTypePaymentInfo" },
+ { "credit_info", "NSPrivacyCollectedDataTypeCreditInfo" },
+ { "other_financial_info", "NSPrivacyCollectedDataTypeOtherFinancialInfo" },
+ { "precise_location", "NSPrivacyCollectedDataTypePreciseLocation" },
+ { "coarse_location", "NSPrivacyCollectedDataTypeCoarseLocation" },
+ { "sensitive_info", "NSPrivacyCollectedDataTypeSensitiveInfo" },
+ { "contacts", "NSPrivacyCollectedDataTypeContacts" },
+ { "emails_or_text_messages", "NSPrivacyCollectedDataTypeEmailsOrTextMessages" },
+ { "photos_or_videos", "NSPrivacyCollectedDataTypePhotosorVideos" },
+ { "audio_data", "NSPrivacyCollectedDataTypeAudioData" },
+ { "gameplay_content", "NSPrivacyCollectedDataTypeGameplayContent" },
+ { "customer_support", "NSPrivacyCollectedDataTypeCustomerSupport" },
+ { "other_user_content", "NSPrivacyCollectedDataTypeOtherUserContent" },
+ { "browsing_history", "NSPrivacyCollectedDataTypeBrowsingHistory" },
+ { "search_hhistory", "NSPrivacyCollectedDataTypeSearchHistory" },
+ { "user_id", "NSPrivacyCollectedDataTypeUserID" },
+ { "device_id", "NSPrivacyCollectedDataTypeDeviceID" },
+ { "purchase_history", "NSPrivacyCollectedDataTypePurchaseHistory" },
+ { "product_interaction", "NSPrivacyCollectedDataTypeProductInteraction" },
+ { "advertising_data", "NSPrivacyCollectedDataTypeAdvertisingData" },
+ { "other_usage_data", "NSPrivacyCollectedDataTypeOtherUsageData" },
+ { "crash_data", "NSPrivacyCollectedDataTypeCrashData" },
+ { "performance_data", "NSPrivacyCollectedDataTypePerformanceData" },
+ { "other_diagnostic_data", "NSPrivacyCollectedDataTypeOtherDiagnosticData" },
+ { "environment_scanning", "NSPrivacyCollectedDataTypeEnvironmentScanning" },
+ { "hands", "NSPrivacyCollectedDataTypeHands" },
+ { "head", "NSPrivacyCollectedDataTypeHead" },
+ { "other_data_types", "NSPrivacyCollectedDataTypeOtherDataTypes" },
+};
+
+static const DataCollectionInfo data_collect_purpose_info[] = {
+ { "Analytics", "NSPrivacyCollectedDataTypePurposeAnalytics" },
+ { "App Functionality", "NSPrivacyCollectedDataTypePurposeAppFunctionality" },
+ { "Developer Advertising", "NSPrivacyCollectedDataTypePurposeDeveloperAdvertising" },
+ { "Third-party Advertising", "NSPrivacyCollectedDataTypePurposeThirdPartyAdvertising" },
+ { "Product Personalization", "NSPrivacyCollectedDataTypePurposeProductPersonalization" },
+ { "Other", "NSPrivacyCollectedDataTypePurposeOther" },
+};
+
void EditorExportPlatformIOS::get_export_options(List *r_options) {
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
@@ -393,6 +481,37 @@ void EditorExportPlatformIOS::get_export_options(List *r_options)
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/microphone_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the microphone"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/photolibrary_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need access to the photo library"), ""));
+ for (uint64_t i = 0; i < sizeof(api_info) / sizeof(api_info[0]); ++i) {
+ String prop_name = vformat("privacy/%s_access_reasons", api_info[i].prop_name);
+ String hint;
+ for (int j = 0; api_info[i].prop_flag_value[j] != String(); j++) {
+ if (j != 0) {
+ hint += ",";
+ }
+ hint += vformat("%s - %s:%d", api_info[i].prop_flag_value[j], api_info[i].prop_flag_name[j], (1 << j));
+ }
+ r_options->push_back(ExportOption(PropertyInfo(Variant::INT, prop_name, PROPERTY_HINT_FLAGS, hint), api_info[i].default_value));
+ }
+
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "privacy/tracking_enabled"), false));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::POOL_STRING_ARRAY, "privacy/tracking_domains"), Vector()));
+
+ {
+ String hint;
+ for (uint64_t i = 0; i < sizeof(data_collect_purpose_info) / sizeof(data_collect_purpose_info[0]); ++i) {
+ if (i != 0) {
+ hint += ",";
+ }
+ hint += vformat("%s:%d", data_collect_purpose_info[i].prop_name, (1 << i));
+ }
+ for (uint64_t i = 0; i < sizeof(data_collect_type_info) / sizeof(data_collect_type_info[0]); ++i) {
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("privacy/collected_data/%s/collected", data_collect_type_info[i].prop_name)), false));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("privacy/collected_data/%s/linked_to_user", data_collect_type_info[i].prop_name)), false));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("privacy/collected_data/%s/used_for_tracking", data_collect_type_info[i].prop_name)), false));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::INT, vformat("privacy/collected_data/%s/collection_purposes", data_collect_type_info[i].prop_name), PROPERTY_HINT_FLAGS, hint), 0));
+ }
+ }
+
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/iphone_120x120", PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), "")); // Home screen on iPhone/iPod Touch with Retina display
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/iphone_180x180", PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), "")); // Home screen on iPhone with Retina HD display
@@ -696,6 +815,87 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref &p_
} else if (lines[i].find("$swift_runtime_build_phase") != -1) {
String value = !p_config.use_swift_runtime ? "" : "90B4C2B62680C7E90039117A /* dummy.swift */,";
strnew += lines[i].replace("$swift_runtime_build_phase", value) + "\n";
+ } else if (lines[i].find("$priv_collection") != -1) {
+ bool section_opened = false;
+ for (uint64_t j = 0; j < sizeof(data_collect_type_info) / sizeof(data_collect_type_info[0]); ++j) {
+ bool data_collected = p_preset->get(vformat("privacy/collected_data/%s/collected", data_collect_type_info[j].prop_name));
+ bool linked = p_preset->get(vformat("privacy/collected_data/%s/linked_to_user", data_collect_type_info[j].prop_name));
+ bool tracking = p_preset->get(vformat("privacy/collected_data/%s/used_for_tracking", data_collect_type_info[j].prop_name));
+ int purposes = p_preset->get(vformat("privacy/collected_data/%s/collection_purposes", data_collect_type_info[j].prop_name));
+ if (data_collected) {
+ if (!section_opened) {
+ section_opened = true;
+ strnew += "\tNSPrivacyCollectedDataTypes\n";
+ strnew += "\t\n";
+ }
+ strnew += "\t\t\n";
+ strnew += "\t\t\tNSPrivacyCollectedDataType\n";
+ strnew += vformat("\t\t\t%s\n", data_collect_type_info[j].type_name);
+ strnew += "\t\t\t\tNSPrivacyCollectedDataTypeLinked\n";
+ if (linked) {
+ strnew += "\t\t\t\t\n";
+ } else {
+ strnew += "\t\t\t\t\n";
+ }
+ strnew += "\t\t\t\tNSPrivacyCollectedDataTypeTracking\n";
+ if (tracking) {
+ strnew += "\t\t\t\t\n";
+ } else {
+ strnew += "\t\t\t\t\n";
+ }
+ if (purposes != 0) {
+ strnew += "\t\t\t\tNSPrivacyCollectedDataTypePurposes\n";
+ strnew += "\t\t\t\t\n";
+ for (uint64_t k = 0; k < sizeof(data_collect_purpose_info) / sizeof(data_collect_purpose_info[0]); ++k) {
+ if (purposes & (1 << k)) {
+ strnew += vformat("\t\t\t\t\t%s\n", data_collect_purpose_info[k].type_name);
+ }
+ }
+ strnew += "\t\t\t\t\n";
+ }
+ strnew += "\t\t\t\n";
+ }
+ }
+ if (section_opened) {
+ strnew += "\t\n";
+ }
+ } else if (lines[i].find("$priv_tracking") != -1) {
+ bool tracking = p_preset->get("privacy/tracking_enabled");
+ strnew += "\tNSPrivacyTracking\n";
+ if (tracking) {
+ strnew += "\t\n";
+ } else {
+ strnew += "\t\n";
+ }
+ Vector tracking_domains = p_preset->get("privacy/tracking_domains");
+ if (!tracking_domains.empty()) {
+ strnew += "\tNSPrivacyTrackingDomains\n";
+ strnew += "\t\n";
+ for (int j = 0; j < tracking_domains.size(); j++) {
+ strnew += "\t\t" + tracking_domains[j] + "\n";
+ }
+ strnew += "\t\n";
+ }
+ } else if (lines[i].find("$priv_api_types") != -1) {
+ strnew += "\t\n";
+ for (uint64_t j = 0; j < sizeof(api_info) / sizeof(api_info[0]); ++j) {
+ int api_access = p_preset->get(vformat("privacy/%s_access_reasons", api_info[j].prop_name));
+ if (api_access != 0) {
+ strnew += "\t\t\n";
+ strnew += "\t\t\tNSPrivacyAccessedAPITypeReasons\n";
+ strnew += "\t\t\t\n";
+ for (int k = 0; api_info[j].prop_flag_value[k] != String(); k++) {
+ if (api_access & (1 << k)) {
+ strnew += vformat("\t\t\t\t%s\n", api_info[j].prop_flag_value[k]);
+ }
+ }
+ strnew += "\t\t\t\n";
+ strnew += "\t\t\tNSPrivacyAccessedAPIType\n";
+ strnew += vformat("\t\t\t%s\n", api_info[j].type_name);
+ strnew += "\t\t\n";
+ }
+ }
+ strnew += "\t\n";
} else {
strnew += lines[i] + "\n";
}
@@ -1701,6 +1901,7 @@ Error EditorExportPlatformIOS::export_project(const Ref &p_p
files_to_parse.insert("godot_ios.xcodeproj/xcshareddata/xcschemes/godot_ios.xcscheme");
files_to_parse.insert("godot_ios/godot_ios.entitlements");
files_to_parse.insert("godot_ios/Launch Screen.storyboard");
+ files_to_parse.insert("PrivacyInfo.xcprivacy");
IOSConfigData config_data = {
pkg_name,
@@ -2034,6 +2235,27 @@ bool EditorExportPlatformIOS::has_valid_project_configuration(const Refget("privacy/file_timestamp_access_reasons");
+ if (access == 0) {
+ err += TTR("At least one file timestamp access reason should be selected.") + "\n";
+ valid = false;
+ }
+ }
+ {
+ int access = p_preset->get("privacy/disk_space_access_reasons");
+ if (access == 0) {
+ err += TTR("At least one disk space access reason should be selected.") + "\n";
+ valid = false;
+ }
+ }
+ {
+ int access = p_preset->get("privacy/system_boot_time_access_reasons");
+ if (access == 0) {
+ err += TTR("At least one system boot time access reason should be selected.") + "\n";
+ valid = false;
+ }
+ }
String etc_error = test_etc2_or_pvrtc();
if (etc_error != String()) {