Skip to content
Source
Sign inSign up

expo-sharing@58.0.22.patch

241 lines11.3 KB

1diff --git a/android/src/main/java/expo/modules/sharing/SharingRecords.kt b/android/src/main/java/expo/modules/sharing/SharingRecords.kt
2index 1113a0adbb951081dea5214a711fe1dbb5a3e814..e89529a43258ae3bd6ec730c19453aed5e5efac4 100644
3--- a/android/src/main/java/expo/modules/sharing/SharingRecords.kt
4+++ b/android/src/main/java/expo/modules/sharing/SharingRecords.kt
5@@ -51,7 +51,8 @@ enum class ContentType(val value: String) : Enumerable {
6 internal data class SharePayload(
7 @Field var value: String = "",
8 @Field var shareType: ShareType = ShareType.Text,
9- @Field var mimeType: String = "text/plain"
10+ @Field var mimeType: String = "text/plain",
11+ @Field var originalName: String? = null
12 ) : Record
14 @OptimizedRecord
15diff --git a/android/src/main/java/expo/modules/sharing/dataParsers/ResolvingShareIntentDataParser.kt b/android/src/main/java/expo/modules/sharing/dataParsers/ResolvingShareIntentDataParser.kt
16index 0a4c92e4b6459632933027827a86a4ec994ce72f..54ff2b5f44df740c9b971e409397e4a08a973717 100644
17--- a/android/src/main/java/expo/modules/sharing/dataParsers/ResolvingShareIntentDataParser.kt
18+++ b/android/src/main/java/expo/modules/sharing/dataParsers/ResolvingShareIntentDataParser.kt
19@@ -9,6 +9,7 @@ import java.io.File
20 import java.io.FileOutputStream
21 import java.net.HttpURLConnection
22 import java.net.URL
23+import java.util.UUID
24 import kotlin.text.indexOf
25 import kotlin.text.substring
27@@ -56,14 +57,20 @@ class ResolvingShareIntentDataParser {
29 private fun resolveUri(context: Context, uri: Uri, mimeType: String): ResolvedSharePayload {
30 val contentResolver = context.contentResolver
31- val fileName = getFileName(contentResolver, uri) ?: "unknown"
32+ val fileName = safeFileName(getFileName(contentResolver, uri))
33 val fileSize = getFileSize(contentResolver, uri)
34 val specificMimeType = contentResolver.getType(uri) ?: mimeType
35 val type = ShareType.fromMimeType(specificMimeType)
37- // Copy to cache dir to ensure access
38- val file = File(context.cacheDir, fileName)
39+ // The sending app controls the name, so copy into a fresh directory and refuse any
40+ // destination that resolves outside cacheDir.
41+ val cacheDir = context.cacheDir.canonicalFile
42+ val file = File(File(cacheDir, "shared-${UUID.randomUUID()}"), fileName)
43+ if (!file.canonicalPath.startsWith(cacheDir.path + File.separator)) {
44+ throw SecurityException("Shared file name escapes the cache directory")
45+ }
46 try {
47+ file.parentFile?.mkdirs()
48 contentResolver.openInputStream(uri)?.use { input ->
49 FileOutputStream(file).use { output ->
50 input.copyTo(output)
51@@ -85,6 +92,11 @@ class ResolvingShareIntentDataParser {
52 }
53 }
55+ private fun safeFileName(name: String?): String {
56+ val baseName = name?.let { File(it).name }
57+ return if (baseName.isNullOrBlank() || baseName == "." || baseName == "..") "unknown" else baseName
58+ }
60 private fun getFileName(resolver: ContentResolver, uri: Uri): String? {
61 val cursor = resolver.query(uri, null, null, null, null)
62 val name = cursor?.use { cursor ->
63diff --git a/android/src/main/java/expo/modules/sharing/dataParsers/SimpleShareIntentDataParser.kt b/android/src/main/java/expo/modules/sharing/dataParsers/SimpleShareIntentDataParser.kt
64index 3381c5fd207ebb4cd8e0301aac59d307939ff162..481cdb4bc64ea5ae49919071f686324f6d56d55c 100644
65--- a/android/src/main/java/expo/modules/sharing/dataParsers/SimpleShareIntentDataParser.kt
66+++ b/android/src/main/java/expo/modules/sharing/dataParsers/SimpleShareIntentDataParser.kt
67@@ -3,6 +3,7 @@ package expo.modules.sharing
68 import android.content.Context
69 import android.content.Intent
70 import android.net.Uri
71+import android.provider.OpenableColumns
73 internal class SimpleShareIntentDataParser {
74 companion object {
75@@ -17,25 +18,26 @@ internal class SimpleShareIntentDataParser {
76 }
78 private fun handleSendAction(context: Context, intent: Intent, type: String): List<SharePayload> {
79- return if (type == "text/plain") {
80- val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return emptyList()
81- val isUrl = android.util.Patterns.WEB_URL.matcher(text).matches()
83- listOf(
84+ val stream = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
85+ val text = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()
86+ if (stream == null) {
87+ if (!type.startsWith("text/") || text == null) return emptyList()
88+ return listOf(
89 SharePayload().apply {
90 value = text
91- shareType = if (isUrl) {
92- ShareType.Url
93- } else {
94- ShareType.Text
95- }
96- mimeType = "text/plain"
97+ shareType = if (android.util.Patterns.WEB_URL.matcher(text).matches()) ShareType.Url else ShareType.Text
98+ mimeType = type
99 }
100 )
101- } else {
102- val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)
103- listOfNotNull(uri?.let { createUriPayload(context, it, type) })
104 }
105+ val filePayload = createUriPayload(context, stream, type)
106+ if (text.isNullOrBlank()) return listOf(filePayload)
107+ val textPayload = SharePayload().apply {
108+ value = text
109+ shareType = if (android.util.Patterns.WEB_URL.matcher(text).matches()) ShareType.Url else ShareType.Text
110+ mimeType = "text/plain"
111+ }
112+ return listOf(textPayload, filePayload)
113 }
115 private fun handleSendMultipleAction(context: Context, intent: Intent, type: String): List<SharePayload> {
116@@ -45,10 +47,25 @@ internal class SimpleShareIntentDataParser {
118 private fun createUriPayload(context: Context, uri: Uri, defaultType: String): SharePayload {
119 val specificType = context.contentResolver.getType(uri) ?: defaultType
120+ // A blank DISPLAY_NAME must stay null: consumers treat originalName as
121+ // the attachment name, and an empty name fails contract validation.
122+ val displayName = runCatching {
123+ context.contentResolver.query(
124+ uri,
125+ arrayOf(OpenableColumns.DISPLAY_NAME),
126+ null,
127+ null,
128+ null
129+ )?.use { cursor ->
130+ val column = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
131+ if (column >= 0 && cursor.moveToFirst()) cursor.getString(column) else null
132+ }
133+ }.getOrNull()?.takeIf { it.isNotBlank() }
134 return SharePayload().apply {
135 value = uri.toString()
136- shareType = ShareType.fromMimeType(specificType)
137+ shareType = if (specificType.startsWith("text/")) ShareType.File else ShareType.fromMimeType(specificType)
138 mimeType = specificType
139+ originalName = displayName
140 }
141 }
142 }
143diff --git a/build/Sharing.types.d.ts b/build/Sharing.types.d.ts
144index 74544e0728d5f845351693d8d90596abae42eaea..92bd2f53108ac717f1ef7292fe89aea05c676c5e 100644
145--- a/build/Sharing.types.d.ts
146+++ b/build/Sharing.types.d.ts
147@@ -100,6 +100,11 @@ export type SharePayload = {
148 * @default 'text/plain'
149 */
150 mimeType?: string;
151+ /**
152+ * The display name of a shared file, when the source app provided one.
153+ * Populated on Android from the content resolver's `DISPLAY_NAME`.
154+ */
155+ originalName?: string | null;
156 };
157 export type BaseResolvedSharePayload = SharePayload & {
158 /**
159diff --git a/plugin/build/ios/xcode/addPbxGroup.js b/plugin/build/ios/xcode/addPbxGroup.js
160index 0867938bdc6df7aeba560cb84dc9c9156c842d95..6ff5be15de702c79cc932def1af68e4c801db48e 100644
161--- a/plugin/build/ios/xcode/addPbxGroup.js
162+++ b/plugin/build/ios/xcode/addPbxGroup.js
163@@ -29,9 +29,23 @@ function addPbxGroup(xcodeProject, targetName, shareExtensionFiles, platformProj
164 }).flat();
165 // Add generated files
166 targetFiles.push(`${targetName}.entitlements`);
167- targetFiles.push(`Info.plist`);
168 const sharedFiles = Object.values(shareExtensionFiles.sharedFiles ?? []).flat().map((file)=>_path().default.join(sharedFilesPath, file)).map((file)=>_path().default.relative(_path().default.join(platformProjectRoot, targetName, 'shared'), file));
169- const { uuid: mainGroupUuid } = xcodeProject.addPbxGroup(targetFiles, targetName, targetName);
170+ const { uuid: mainGroupUuid, pbxGroup: mainGroup } = xcodeProject.addPbxGroup(targetFiles, targetName, targetName);
171+ // Xcode 27.2 rejects file references shared between groups. Bypass
172+ // addPbxGroup's path deduplication so the widget keeps its own Info.plist.
173+ const infoPlistReference = xcodeProject.generateUuid();
174+ const fileReferences = xcodeProject.pbxFileReferenceSection();
175+ fileReferences[infoPlistReference] = {
176+ isa: 'PBXFileReference',
177+ lastKnownFileType: 'text.plist.xml',
178+ path: 'Info.plist',
179+ sourceTree: '"<group>"'
180+ };
181+ fileReferences[`${infoPlistReference}_comment`] = 'Info.plist';
182+ mainGroup.children.push({
183+ value: infoPlistReference,
184+ comment: 'Info.plist'
185+ });
186 const { uuid: sharedGroupUuid } = xcodeProject.addPbxGroup(sharedFiles, 'shared', 'shared');
187 if (mainGroupUuid && sharedGroupUuid) {
188 // @ts-expect-error: TODO(@kitten): This used to be untyped, and is failing now
189diff --git a/plugin/src/ios/xcode/addPbxGroup.ts b/plugin/src/ios/xcode/addPbxGroup.ts
190index 943c263c0cf58be595bfc9745c91827da81befec..3a99a1822beb25fea3835123b9a4ad7ac3b4f8d5 100644
191--- a/plugin/src/ios/xcode/addPbxGroup.ts
192+++ b/plugin/src/ios/xcode/addPbxGroup.ts
193@@ -19,14 +19,29 @@ export function addPbxGroup(
195 // Add generated files
196 targetFiles.push(`${targetName}.entitlements`);
197- targetFiles.push(`Info.plist`);
199 const sharedFiles = Object.values(shareExtensionFiles.sharedFiles ?? [])
200 .flat()
201 .map((file) => path.join(sharedFilesPath, file))
202 .map((file) => path.relative(path.join(platformProjectRoot, targetName, 'shared'), file));
204- const { uuid: mainGroupUuid } = xcodeProject.addPbxGroup(targetFiles, targetName, targetName);
205+ const { uuid: mainGroupUuid, pbxGroup: mainGroup } = xcodeProject.addPbxGroup(
206+ targetFiles,
207+ targetName,
208+ targetName
209+ );
210+ // Xcode 27.2 rejects file references shared between groups. Bypass
211+ // addPbxGroup's path deduplication so the widget keeps its own Info.plist.
212+ const infoPlistReference = xcodeProject.generateUuid();
213+ const fileReferences = xcodeProject.pbxFileReferenceSection();
214+ fileReferences[infoPlistReference] = {
215+ isa: 'PBXFileReference',
216+ lastKnownFileType: 'text.plist.xml',
217+ path: 'Info.plist',
218+ sourceTree: '"<group>"',
219+ };
220+ fileReferences[`${infoPlistReference}_comment`] = 'Info.plist';
221+ mainGroup.children.push({ value: infoPlistReference, comment: 'Info.plist' });
222 const { uuid: sharedGroupUuid } = xcodeProject.addPbxGroup(sharedFiles, 'shared', 'shared');
224 if (mainGroupUuid && sharedGroupUuid) {
225diff --git a/src/Sharing.types.ts b/src/Sharing.types.ts
226index 18af320420e7c01a8ffbd7fa5347320409f905d8..3a52b70a20efbc588ad5ca974c38c951e5757c97 100644
227--- a/src/Sharing.types.ts
228+++ b/src/Sharing.types.ts
229@@ -108,6 +108,12 @@ export type SharePayload = {
230 * @default 'text/plain'
231 */
232 mimeType?: string;
234+ /**
235+ * The display name of a shared file, when the source app provided one.
236+ * Populated on Android from the content resolver's `DISPLAY_NAME`.
237+ */
238+ originalName?: string | null;
239 };
241 export type BaseResolvedSharePayload = SharePayload & {