1082815dcSEvan Baconimport { ExpoConfig } from '@expo/config-types';
2082815dcSEvan Bacon
3*8a424bebSJames Ideimport { AndroidManifest, ensureToolsAvailable, ManifestUsesPermission } from './Manifest';
4082815dcSEvan Baconimport { ConfigPlugin } from '../Plugin.types';
5082815dcSEvan Baconimport { withAndroidManifest } from '../plugins/android-plugins';
6082815dcSEvan Bacon
7082815dcSEvan Baconconst USES_PERMISSION = 'uses-permission';
8082815dcSEvan Bacon
9082815dcSEvan Baconexport const withPermissions: ConfigPlugin<string[] | void> = (config, permissions) => {
10082815dcSEvan Bacon  if (Array.isArray(permissions)) {
11082815dcSEvan Bacon    permissions = permissions.filter(Boolean);
12082815dcSEvan Bacon    if (!config.android) config.android = {};
13082815dcSEvan Bacon    if (!config.android.permissions) config.android.permissions = [];
14082815dcSEvan Bacon    config.android.permissions = [
15082815dcSEvan Bacon      // @ts-ignore
16082815dcSEvan Bacon      ...new Set(config.android.permissions.concat(permissions)),
17082815dcSEvan Bacon    ];
18082815dcSEvan Bacon  }
19082815dcSEvan Bacon  return withAndroidManifest(config, async (config) => {
20082815dcSEvan Bacon    config.modResults = await setAndroidPermissions(config, config.modResults);
21082815dcSEvan Bacon    return config;
22082815dcSEvan Bacon  });
23082815dcSEvan Bacon};
24082815dcSEvan Bacon
25082815dcSEvan Bacon/** Given a permission or list of permissions, block permissions in the final `AndroidManifest.xml` to ensure no installed library or plugin can add them. */
26082815dcSEvan Baconexport const withBlockedPermissions: ConfigPlugin<string[] | string> = (config, permissions) => {
278828a4a3SCedric van Putten  const resolvedPermissions = prefixAndroidPermissionsIfNecessary(
288828a4a3SCedric van Putten    (Array.isArray(permissions) ? permissions : [permissions]).filter(Boolean)
29082815dcSEvan Bacon  );
30082815dcSEvan Bacon
31082815dcSEvan Bacon  if (config?.android?.permissions && Array.isArray(config.android.permissions)) {
32082815dcSEvan Bacon    // Remove any static config permissions
338828a4a3SCedric van Putten    config.android.permissions = prefixAndroidPermissionsIfNecessary(
348828a4a3SCedric van Putten      config.android.permissions
358828a4a3SCedric van Putten    ).filter((permission) => !resolvedPermissions.includes(permission));
36082815dcSEvan Bacon  }
37082815dcSEvan Bacon
38082815dcSEvan Bacon  return withAndroidManifest(config, async (config) => {
39082815dcSEvan Bacon    config.modResults = ensureToolsAvailable(config.modResults);
40082815dcSEvan Bacon    config.modResults = addBlockedPermissions(config.modResults, resolvedPermissions);
41082815dcSEvan Bacon    return config;
42082815dcSEvan Bacon  });
43082815dcSEvan Bacon};
44082815dcSEvan Bacon
45082815dcSEvan Baconexport const withInternalBlockedPermissions: ConfigPlugin = (config) => {
46082815dcSEvan Bacon  // Only add permissions if the user defined the property and added some values
47082815dcSEvan Bacon  // this ensures we don't add the `tools:*` namespace extraneously.
48082815dcSEvan Bacon  if (config.android?.blockedPermissions?.length) {
49082815dcSEvan Bacon    return withBlockedPermissions(config, config.android.blockedPermissions);
50082815dcSEvan Bacon  }
51082815dcSEvan Bacon
52082815dcSEvan Bacon  return config;
53082815dcSEvan Bacon};
54082815dcSEvan Bacon
55082815dcSEvan Baconexport function addBlockedPermissions(androidManifest: AndroidManifest, permissions: string[]) {
56082815dcSEvan Bacon  if (!Array.isArray(androidManifest.manifest['uses-permission'])) {
57082815dcSEvan Bacon    androidManifest.manifest['uses-permission'] = [];
58082815dcSEvan Bacon  }
59082815dcSEvan Bacon
608828a4a3SCedric van Putten  for (const permission of prefixAndroidPermissionsIfNecessary(permissions)) {
61082815dcSEvan Bacon    androidManifest.manifest['uses-permission'] = ensureBlockedPermission(
62082815dcSEvan Bacon      androidManifest.manifest['uses-permission'],
63082815dcSEvan Bacon      permission
64082815dcSEvan Bacon    );
65082815dcSEvan Bacon  }
66082815dcSEvan Bacon
67082815dcSEvan Bacon  return androidManifest;
68082815dcSEvan Bacon}
69082815dcSEvan Bacon
70082815dcSEvan Bacon/**
71082815dcSEvan Bacon * Filter any existing permissions matching the provided permission name, then add a
72082815dcSEvan Bacon * restricted permission to overwrite any extra permissions that may be added in a
73082815dcSEvan Bacon * third-party package's AndroidManifest.xml.
74082815dcSEvan Bacon *
75082815dcSEvan Bacon * @param manifestPermissions manifest `uses-permissions` array.
76082815dcSEvan Bacon * @param permission `android:name` of the permission to restrict
77082815dcSEvan Bacon * @returns
78082815dcSEvan Bacon */
79082815dcSEvan Baconfunction ensureBlockedPermission(
80082815dcSEvan Bacon  manifestPermissions: ManifestUsesPermission[],
81082815dcSEvan Bacon  permission: string
82082815dcSEvan Bacon) {
83082815dcSEvan Bacon  // Remove permission if it currently exists
84082815dcSEvan Bacon  manifestPermissions = manifestPermissions.filter((e) => e.$['android:name'] !== permission);
85082815dcSEvan Bacon
86082815dcSEvan Bacon  // Add a permission with tools:node to overwrite any existing permission and ensure it's removed upon building.
87082815dcSEvan Bacon  manifestPermissions.push({
88082815dcSEvan Bacon    $: { 'android:name': permission, 'tools:node': 'remove' },
89082815dcSEvan Bacon  });
90082815dcSEvan Bacon  return manifestPermissions;
91082815dcSEvan Bacon}
92082815dcSEvan Bacon
93082815dcSEvan Baconfunction prefixAndroidPermissionsIfNecessary(permissions: string[]): string[] {
94082815dcSEvan Bacon  return permissions.map((permission) => {
95082815dcSEvan Bacon    if (!permission.includes('.')) {
96082815dcSEvan Bacon      return `android.permission.${permission}`;
97082815dcSEvan Bacon    }
98082815dcSEvan Bacon    return permission;
99082815dcSEvan Bacon  });
100082815dcSEvan Bacon}
101082815dcSEvan Bacon
102082815dcSEvan Baconexport function getAndroidPermissions(config: Pick<ExpoConfig, 'android'>): string[] {
103082815dcSEvan Bacon  return config.android?.permissions ?? [];
104082815dcSEvan Bacon}
105082815dcSEvan Bacon
106082815dcSEvan Baconexport function setAndroidPermissions(
107082815dcSEvan Bacon  config: Pick<ExpoConfig, 'android'>,
108082815dcSEvan Bacon  androidManifest: AndroidManifest
109082815dcSEvan Bacon) {
110082815dcSEvan Bacon  const permissions = getAndroidPermissions(config);
111082815dcSEvan Bacon  const providedPermissions = prefixAndroidPermissionsIfNecessary(permissions);
112082815dcSEvan Bacon  const permissionsToAdd = [...providedPermissions];
113082815dcSEvan Bacon
114082815dcSEvan Bacon  if (!androidManifest.manifest.hasOwnProperty('uses-permission')) {
115082815dcSEvan Bacon    androidManifest.manifest['uses-permission'] = [];
116082815dcSEvan Bacon  }
117082815dcSEvan Bacon  // manifest.manifest['uses-permission'] = [];
118082815dcSEvan Bacon
119082815dcSEvan Bacon  const manifestPermissions = androidManifest.manifest['uses-permission'] ?? [];
120082815dcSEvan Bacon
121082815dcSEvan Bacon  permissionsToAdd.forEach((permission) => {
122082815dcSEvan Bacon    if (!isPermissionAlreadyRequested(permission, manifestPermissions)) {
123082815dcSEvan Bacon      addPermissionToManifest(permission, manifestPermissions);
124082815dcSEvan Bacon    }
125082815dcSEvan Bacon  });
126082815dcSEvan Bacon
127082815dcSEvan Bacon  return androidManifest;
128082815dcSEvan Bacon}
129082815dcSEvan Bacon
130082815dcSEvan Baconexport function isPermissionAlreadyRequested(
131082815dcSEvan Bacon  permission: string,
132082815dcSEvan Bacon  manifestPermissions: ManifestUsesPermission[]
133082815dcSEvan Bacon): boolean {
134082815dcSEvan Bacon  return manifestPermissions.some((e) => e.$['android:name'] === permission);
135082815dcSEvan Bacon}
136082815dcSEvan Bacon
137082815dcSEvan Baconexport function addPermissionToManifest(
138082815dcSEvan Bacon  permission: string,
139082815dcSEvan Bacon  manifestPermissions: ManifestUsesPermission[]
140082815dcSEvan Bacon) {
141082815dcSEvan Bacon  manifestPermissions.push({ $: { 'android:name': permission } });
142082815dcSEvan Bacon  return manifestPermissions;
143082815dcSEvan Bacon}
144082815dcSEvan Bacon
145082815dcSEvan Baconexport function removePermissions(androidManifest: AndroidManifest, permissionNames?: string[]) {
146082815dcSEvan Bacon  const targetNames = permissionNames ? permissionNames.map(ensurePermissionNameFormat) : null;
147082815dcSEvan Bacon  const permissions = androidManifest.manifest[USES_PERMISSION] || [];
148082815dcSEvan Bacon  const nextPermissions = [];
149082815dcSEvan Bacon  for (const attribute of permissions) {
150082815dcSEvan Bacon    if (targetNames) {
151082815dcSEvan Bacon      // @ts-ignore: name isn't part of the type
152082815dcSEvan Bacon      const value = attribute.$['android:name'] || attribute.$.name;
153082815dcSEvan Bacon      if (!targetNames.includes(value)) {
154082815dcSEvan Bacon        nextPermissions.push(attribute);
155082815dcSEvan Bacon      }
156082815dcSEvan Bacon    }
157082815dcSEvan Bacon  }
158082815dcSEvan Bacon
159082815dcSEvan Bacon  androidManifest.manifest[USES_PERMISSION] = nextPermissions;
160082815dcSEvan Bacon}
161082815dcSEvan Bacon
162082815dcSEvan Baconexport function addPermission(androidManifest: AndroidManifest, permissionName: string): void {
163082815dcSEvan Bacon  const usesPermissions: ManifestUsesPermission[] = androidManifest.manifest[USES_PERMISSION] || [];
164082815dcSEvan Bacon  usesPermissions.push({
165082815dcSEvan Bacon    $: { 'android:name': permissionName },
166082815dcSEvan Bacon  });
167082815dcSEvan Bacon  androidManifest.manifest[USES_PERMISSION] = usesPermissions;
168082815dcSEvan Bacon}
169082815dcSEvan Bacon
170082815dcSEvan Baconexport function ensurePermissions(
171082815dcSEvan Bacon  androidManifest: AndroidManifest,
172082815dcSEvan Bacon  permissionNames: string[]
173082815dcSEvan Bacon): { [permission: string]: boolean } {
174082815dcSEvan Bacon  const permissions = getPermissions(androidManifest);
175082815dcSEvan Bacon
176082815dcSEvan Bacon  const results: { [permission: string]: boolean } = {};
177082815dcSEvan Bacon  for (const permissionName of permissionNames) {
178082815dcSEvan Bacon    const targetName = ensurePermissionNameFormat(permissionName);
179082815dcSEvan Bacon    if (!permissions.includes(targetName)) {
180082815dcSEvan Bacon      addPermission(androidManifest, targetName);
181082815dcSEvan Bacon      results[permissionName] = true;
182082815dcSEvan Bacon    } else {
183082815dcSEvan Bacon      results[permissionName] = false;
184082815dcSEvan Bacon    }
185082815dcSEvan Bacon  }
186082815dcSEvan Bacon  return results;
187082815dcSEvan Bacon}
188082815dcSEvan Bacon
189082815dcSEvan Baconexport function ensurePermission(
190082815dcSEvan Bacon  androidManifest: AndroidManifest,
191082815dcSEvan Bacon  permissionName: string
192082815dcSEvan Bacon): boolean {
193082815dcSEvan Bacon  const permissions = getPermissions(androidManifest);
194082815dcSEvan Bacon  const targetName = ensurePermissionNameFormat(permissionName);
195082815dcSEvan Bacon
196082815dcSEvan Bacon  if (!permissions.includes(targetName)) {
197082815dcSEvan Bacon    addPermission(androidManifest, targetName);
198082815dcSEvan Bacon    return true;
199082815dcSEvan Bacon  }
200082815dcSEvan Bacon  return false;
201082815dcSEvan Bacon}
202082815dcSEvan Bacon
203082815dcSEvan Baconexport function ensurePermissionNameFormat(permissionName: string): string {
204082815dcSEvan Bacon  if (permissionName.includes('.')) {
205082815dcSEvan Bacon    const com = permissionName.split('.');
206082815dcSEvan Bacon    const name = com.pop() as string;
207082815dcSEvan Bacon    return [...com, name.toUpperCase()].join('.');
208082815dcSEvan Bacon  } else {
209082815dcSEvan Bacon    // If shorthand form like `WRITE_CONTACTS` is provided, expand it to `android.permission.WRITE_CONTACTS`.
210082815dcSEvan Bacon    return ensurePermissionNameFormat(`android.permission.${permissionName}`);
211082815dcSEvan Bacon  }
212082815dcSEvan Bacon}
213082815dcSEvan Bacon
214082815dcSEvan Baconexport function getPermissions(androidManifest: AndroidManifest): string[] {
215082815dcSEvan Bacon  const usesPermissions: { [key: string]: any }[] = androidManifest.manifest[USES_PERMISSION] || [];
216082815dcSEvan Bacon  const permissions = usesPermissions.map((permissionObject) => {
217082815dcSEvan Bacon    return permissionObject.$['android:name'] || permissionObject.$.name;
218082815dcSEvan Bacon  });
219082815dcSEvan Bacon  return permissions;
220082815dcSEvan Bacon}
221