UNPKG

box-ui-elements-mlh

Version:
614 lines (563 loc) 21.2 kB
// @flow import { getTypedFileId, getTypedFolderId, isGSuiteExtension } from '../../../utils/file'; import { checkIsExternalUser } from '../../../utils/parseEmails'; import { ACCESS_COLLAB, ACCESS_COMPANY, ACCESS_NONE, ACCESS_OPEN, INVITEE_ROLE_EDITOR, PERMISSION_CAN_DOWNLOAD, PERMISSION_CAN_PREVIEW, STATUS_ACCEPTED, STATUS_INACTIVE, TYPE_FOLDER, } from '../../../constants'; import { ALLOWED_ACCESS_LEVELS, ANYONE_IN_COMPANY, ANYONE_WITH_LINK, CAN_VIEW_DOWNLOAD, CAN_VIEW_ONLY, COLLAB_GROUP_TYPE, COLLAB_USER_TYPE, DISABLED_REASON_ACCESS_POLICY, DISABLED_REASON_MALICIOUS_CONTENT, PEOPLE_IN_ITEM, } from '../constants'; import { bdlDarkBlue50, bdlGray20, bdlGreenLight50, bdlLightBlue50, bdlOrange50, bdlPurpleRain50, bdlWatermelonRed50, bdlYellow50, } from '../../../styles/variables'; import { CLASSIFICATION_COLOR_ID_0, CLASSIFICATION_COLOR_ID_1, CLASSIFICATION_COLOR_ID_2, CLASSIFICATION_COLOR_ID_3, CLASSIFICATION_COLOR_ID_4, CLASSIFICATION_COLOR_ID_5, CLASSIFICATION_COLOR_ID_6, CLASSIFICATION_COLOR_ID_7, } from '../../classification/constants'; import type { AvatarURLMap, ContentSharingCollaborationsRequest, ContentSharingItemAPIResponse, ContentSharingItemDataType, ContentSharingUserDataType, ConvertCollabOptions, SharedLinkSettingsOptions, } from '../../../elements/content-sharing/types'; import type { BoxItemPermission, Collaborations, GroupCollection, SharedLink, User, UserCollection, } from '../../../common/types/core'; import type { accessLevelsDisabledReasonType, allowedAccessLevelsType, collaboratorsListType, collaboratorType, contactType, InviteCollaboratorsRequest, permissionLevelType, } from '../flowTypes'; /** * The following constants are used for converting API requests * and responses into objects expected by the USM, and vice versa */ export const API_TO_USM_ACCESS_LEVEL_MAP = { [ACCESS_COLLAB]: PEOPLE_IN_ITEM, [ACCESS_COMPANY]: ANYONE_IN_COMPANY, [ACCESS_OPEN]: ANYONE_WITH_LINK, [ACCESS_NONE]: '', }; export const API_TO_USM_PERMISSION_LEVEL_MAP = { [PERMISSION_CAN_DOWNLOAD]: CAN_VIEW_DOWNLOAD, [PERMISSION_CAN_PREVIEW]: CAN_VIEW_ONLY, }; export const USM_TO_API_ACCESS_LEVEL_MAP = { [ANYONE_IN_COMPANY]: ACCESS_COMPANY, [ANYONE_WITH_LINK]: ACCESS_OPEN, [PEOPLE_IN_ITEM]: ACCESS_COLLAB, }; export const USM_TO_API_PERMISSION_LEVEL_MAP = { [CAN_VIEW_DOWNLOAD]: PERMISSION_CAN_DOWNLOAD, [CAN_VIEW_ONLY]: PERMISSION_CAN_PREVIEW, }; const API_TO_USM_CLASSIFICATION_COLORS_MAP = { [bdlYellow50]: CLASSIFICATION_COLOR_ID_0, [bdlOrange50]: CLASSIFICATION_COLOR_ID_1, [bdlWatermelonRed50]: CLASSIFICATION_COLOR_ID_2, [bdlPurpleRain50]: CLASSIFICATION_COLOR_ID_3, [bdlLightBlue50]: CLASSIFICATION_COLOR_ID_4, [bdlDarkBlue50]: CLASSIFICATION_COLOR_ID_5, [bdlGreenLight50]: CLASSIFICATION_COLOR_ID_6, [bdlGray20]: CLASSIFICATION_COLOR_ID_7, }; const APP_USERS_DOMAIN_REGEXP = new RegExp('boxdevedition.com'); /** * Convert access levels disabled reasons into USM format. * * @param {{ [string]: string }} disabledReasons * @returns {accessLevelsDisabledReasonType | null} */ export const convertAccessLevelsDisabledReasons = (disabledReasons?: { [string]: typeof DISABLED_REASON_ACCESS_POLICY | typeof DISABLED_REASON_MALICIOUS_CONTENT | null, }): accessLevelsDisabledReasonType | null => { if (!disabledReasons) return null; const convertedReasons = {}; Object.entries(disabledReasons).forEach(([level, reason]) => { convertedReasons[API_TO_USM_ACCESS_LEVEL_MAP[level]] = reason; }); return convertedReasons; }; /** * Convert allowed access levels into USM format. * * @param {Array<string>} [levelsFromAPI] * @returns {allowedAccessLevelsType | null} */ export const convertAllowedAccessLevels = (levelsFromAPI?: Array<string>): allowedAccessLevelsType | null => { if (!levelsFromAPI) return null; const convertedLevels = { peopleInThisItem: false, peopleInYourCompany: false, peopleWithTheLink: false, }; levelsFromAPI.forEach(level => { convertedLevels[API_TO_USM_ACCESS_LEVEL_MAP[level]] = true; }); return convertedLevels; }; /** * Convert shared link permission into USM format, taking file type limitations into account. * * @param {string} effectivePermissionFromAPI * @param {string} extension * @returns {permissionLevelType} */ export const convertEffectiveSharedLinkPermission = ( effectivePermissionFromAPI: string, extension: string, ): permissionLevelType => { return isGSuiteExtension(extension) ? CAN_VIEW_ONLY : API_TO_USM_PERMISSION_LEVEL_MAP[effectivePermissionFromAPI]; }; /** * Convert isDownloadSettingAvailable into a value used by the USM, taking into account file type limitations. * * @param {boolean} isDownloadSettingAvailableFromAPI * @param {string} extension * @returns {boolean} */ export const convertIsDownloadSettingAvailable = ( isDownloadSettingAvailableFromAPI?: boolean, extension: string, ): boolean | void => { if (isDownloadSettingAvailableFromAPI === undefined) { return undefined; } return !isGSuiteExtension(extension) && isDownloadSettingAvailableFromAPI; }; /** * Convert a response from the Item API to the object that the USM expects. * * @param {BoxItem} itemAPIData * @returns {ContentSharingItemDataType} Object containing item and shared link information */ export const convertItemResponse = (itemAPIData: ContentSharingItemAPIResponse): ContentSharingItemDataType => { const { allowed_invitee_roles, allowed_shared_link_access_levels, allowed_shared_link_access_levels_disabled_reasons, classification, id, description, extension, name, owned_by: { id: ownerID, login: ownerEmail }, permissions, shared_link, shared_link_features: { download_url: isDirectLinkAvailable, password: isPasswordAvailable }, type, } = itemAPIData; const { can_download: isDownloadSettingAvailableFromApi, can_invite_collaborator: canInvite, can_preview: isPreviewAllowed, can_set_share_access: canChangeAccessLevel, can_share: itemShare, } = permissions; // Convert classification data for the item if available let classificationData = {}; if (classification) { const { color, definition, name: classificationName } = classification; classificationData = { bannerPolicy: { body: definition, colorID: API_TO_USM_CLASSIFICATION_COLORS_MAP[color], }, classification: classificationName, }; } const isEditAllowed = allowed_invitee_roles.indexOf(INVITEE_ROLE_EDITOR) !== -1; // The "canInvite" property is necessary even if the item does not have a shared link, // because it allows users to invite individual collaborators. let sharedLink = { canInvite: !!canInvite }; if (shared_link) { const { download_url: directLink, effective_access, effective_permission, is_password_enabled: isPasswordEnabled, password, unshared_at: expirationTimestamp, url, vanity_name: vanityName, } = shared_link; const isDownloadSettingAvailable = convertIsDownloadSettingAvailable( isDownloadSettingAvailableFromApi, extension, ); const accessLevel = effective_access ? API_TO_USM_ACCESS_LEVEL_MAP[effective_access] : ''; const permissionLevel = effective_permission ? convertEffectiveSharedLinkPermission(effective_permission, extension) : null; const isDownloadAllowed = permissionLevel === API_TO_USM_PERMISSION_LEVEL_MAP.can_download; const canChangeDownload = canChangeAccessLevel && isDownloadSettingAvailable && effective_access !== ACCESS_COLLAB; // access must be "company" or "open" const canChangePassword = canChangeAccessLevel && isPasswordAvailable; const canChangeExpiration = canChangeAccessLevel && isEditAllowed; sharedLink = { accessLevel, accessLevelsDisabledReason: convertAccessLevelsDisabledReasons(allowed_shared_link_access_levels_disabled_reasons) || {}, allowedAccessLevels: convertAllowedAccessLevels(allowed_shared_link_access_levels) || ALLOWED_ACCESS_LEVELS, // show all access levels by default canChangeAccessLevel, canChangeDownload, canChangeExpiration, canChangePassword, canChangeVanityName: false, // vanity URLs cannot be set via the API canInvite: !!canInvite, directLink, expirationTimestamp: expirationTimestamp ? new Date(expirationTimestamp).getTime() : null, // convert to milliseconds isDirectLinkAvailable, isDownloadAllowed, isDownloadAvailable: isDownloadSettingAvailable, isDownloadEnabled: isDownloadAllowed, isDownloadSettingAvailable, isEditAllowed, isNewSharedLink: false, isPasswordAvailable, isPasswordEnabled, isPreviewAllowed, password, permissionLevel, url, vanityName: vanityName || '', }; } return { item: { canUserSeeClassification: !!classification, description, extension, grantedPermissions: { itemShare: !!itemShare, }, hideCollaborators: false, // to do: connect to Collaborations API id, name, ownerEmail, // the owner email is used to determine whether collaborators are external ownerID, // the owner ID is used to determine whether external collaborator badges should be shown permissions, // the original permissions are necessary for PUT requests to the Item API type, typedID: type === TYPE_FOLDER ? getTypedFolderId(id) : getTypedFileId(id), ...classificationData, }, sharedLink, }; }; /** * Convert a response from the User API into the object that the USM expects. * * @param {User} userAPIData * @returns {ContentSharingUserDataType} Object containing user and enterprise information */ export const convertUserResponse = (userAPIData: User): ContentSharingUserDataType => { const { enterprise, hostname, id } = userAPIData; return { id, userEnterpriseData: { enterpriseName: enterprise ? enterprise.name : '', serverURL: hostname ? `${hostname}v/` : '', }, }; }; /** * Create a shared link permissions object for the API based on a USM permission level. * * @param {string} newSharedLinkPermissionLevel * @returns {$Shape<BoxItemPermission>} Object containing shared link permissions */ export const convertSharedLinkPermissions = (newSharedLinkPermissionLevel: string): $Shape<BoxItemPermission> => { const sharedLinkPermissions = {}; Object.keys(USM_TO_API_PERMISSION_LEVEL_MAP).forEach(level => { if (level === newSharedLinkPermissionLevel) { sharedLinkPermissions[USM_TO_API_PERMISSION_LEVEL_MAP[level]] = true; } else { sharedLinkPermissions[USM_TO_API_PERMISSION_LEVEL_MAP[level]] = false; } }); return sharedLinkPermissions; }; /** * Convert a shared link settings object from the USM into the format that the API expects. * This function compares the provided access level to both API and internal USM access level constants, to accommodate two potential flows: * - Changing the settings for a shared link right after the shared link has been created. The access level is saved directly from the data * returned by the API, so it is in API format. * - Changing the settings for a shared link in any other scenario. The access level is saved from the initial calls to the Item API and * convertItemResponse, so it is in internal USM format. * * @param {SharedLinkSettingsOptions} newSettings * @param {accessLevel} string * @param {serverURL} string * @returns {$Shape<SharedLink>} */ export const convertSharedLinkSettings = ( newSettings: SharedLinkSettingsOptions, accessLevel: string, isDownloadAvailable: boolean, serverURL: string, ): $Shape<SharedLink> => { const { expirationTimestamp, isDownloadEnabled: can_download, isExpirationEnabled, isPasswordEnabled, password, vanityName, } = newSettings; const convertedSettings: $Shape<SharedLink> = { unshared_at: expirationTimestamp && isExpirationEnabled ? new Date(expirationTimestamp).toISOString() : null, vanity_url: serverURL && vanityName ? `${serverURL}${vanityName}` : '', }; // Download permissions can only be set on "company" or "open" shared links. if (![ACCESS_COLLAB, PEOPLE_IN_ITEM].includes(accessLevel)) { const permissions: BoxItemPermission = { can_preview: !can_download }; if (isDownloadAvailable) { permissions.can_download = can_download; } convertedSettings.permissions = permissions; } /** * This block covers the following cases: * - Setting a new password: "isPasswordEnabled" is true, and "password" is a non-empty string. * - Removing a password: "isPasswordEnabled" is false, and "password" is an empty string. * The API only accepts non-empty strings and null values, so the empty string must be converted to null. * * Other notes: * - Passwords can only be set on "open" shared links. * - Attempting to set the password field on any other type of shared link will throw a 400 error. * - When other settings are updated, and a password has already been set, the SharedLinkSettingsModal * returns password = '' and isPasswordEnabled = true. In these cases, the password should *not* * be converted to null, because that would remove the existing password. */ if ([ANYONE_WITH_LINK, ACCESS_OPEN].includes(accessLevel)) { if (isPasswordEnabled && !!password) { convertedSettings.password = password; } else if (!isPasswordEnabled) { convertedSettings.password = null; } } return convertedSettings; }; /** * Convert a collaborator. * Note: We do not retrieve the avatar URL of collaborators right after inviting them, * so the avatar fields (hasCustomAvatar and imageURL) are not set in that case. * * @param {ConvertCollabOptions} options * @returns {collaboratorType | null} Object containing a collaborator */ export const convertCollab = ({ collab, avatarURLMap, ownerEmail, isCurrentUserOwner = false, }: ConvertCollabOptions): collaboratorType | null => { if (!collab || collab.status !== STATUS_ACCEPTED) return null; const ownerEmailDomain = ownerEmail && /@/.test(ownerEmail) ? ownerEmail.split('@')[1] : null; const { accessible_by: { id: userID, login: email, name, type }, id: collabID, expires_at: executeAt, role, } = collab; const avatarURL = avatarURLMap ? avatarURLMap[userID] : undefined; const convertedCollab: collaboratorType = { collabID: parseInt(collabID, 10), email, hasCustomAvatar: !!avatarURL, imageURL: avatarURL, isExternalCollab: checkIsExternalUser(isCurrentUserOwner, ownerEmailDomain, email), name, translatedRole: `${role[0].toUpperCase()}${role.slice(1)}`, // capitalize the user's role type, userID: parseInt(userID, 10), }; if (executeAt) { convertedCollab.expiration = { executeAt }; } return convertedCollab; }; /** * Convert a response from the Item Collaborations API into the object that the USM expects. * * @param {Collaborations} collabsAPIData * @param {AvatarURLMap | null} avatarURLMap * @param {string | null | undefined} ownerEmail * @param {boolean} isCurrentUserOwner * @returns {collaboratorsListType} Object containing an array of collaborators */ export const convertCollabsResponse = ( collabsAPIData: Collaborations, avatarURLMap: ?AvatarURLMap, ownerEmail: ?string, isCurrentUserOwner: boolean, ): collaboratorsListType => { const { entries = [] } = collabsAPIData; if (!entries.length) return { collaborators: [] }; const collaborators = []; entries // Only show accepted collaborations .filter(collab => collab.status === STATUS_ACCEPTED) .forEach(collab => { const convertedCollab = convertCollab({ collab, avatarURLMap, ownerEmail, isCurrentUserOwner }); if (convertedCollab) { // Necessary for Flow checking collaborators.push(convertedCollab); } }); return { collaborators }; }; /** * Convert a request from the USM (specifically the Invite Collaborators Modal) into the format expected by the Collaborations API. * ContentSharing/USM will only call this function when at least one properly-formatted email is entered into the "Invite People" field. * Within the context of this feature, groups are identified by IDs, whereas users are identified by their emails. * * @param {InviteCollaboratorsRequest} collabRequest * @returns {ContentSharingCollaborationsRequest} */ export const convertCollabsRequest = ( collabRequest: InviteCollaboratorsRequest, ): ContentSharingCollaborationsRequest => { const { emails, groupIDs, permission } = collabRequest; const emailArray = emails ? emails.split(',') : []; const groupIDArray = groupIDs ? groupIDs.split(',') : []; const roleSettings = { role: permission.toLowerCase(), // USM permissions are identical to API roles, except for the casing }; const groups = groupIDArray.map(groupID => ({ accessible_by: { id: groupID, type: COLLAB_GROUP_TYPE, }, ...roleSettings, })); const users = emailArray.map(email => ({ accessible_by: { login: email, type: COLLAB_USER_TYPE, }, ...roleSettings, })); return { groups, users }; }; const sortByName = ({ name: nameA = '' }, { name: nameB = '' }) => nameA.localeCompare(nameB); /** * Convert an enterprise users API response into an array of internal USM contacts. * * @param {UserCollection} contactsAPIData * @param {string|null} currentUserID * @returns {Array<contactType>} Array of USM contacts */ export const convertUserContactsResponse = ( contactsAPIData: UserCollection, currentUserID: string | null, ): Array<contactType> => { const { entries = [] } = contactsAPIData; // Return all active users except for the current user and app users return entries .filter( ({ id, login: email, status }) => id !== currentUserID && email && !APP_USERS_DOMAIN_REGEXP.test(email) && status && status !== STATUS_INACTIVE, ) .map(contact => { const { id, login: email, name, type } = contact; return { id, email, name, type, }; }) .sort(sortByName); }; /** * Convert an enterprise users API response into an object of internal USM contacts, keyed by email, which is * then passed to the mergeContacts function. * * @param {UserCollection} contactsAPIData * @returns { [string]: contactType } Object of USM contacts */ export const convertUserContactsByEmailResponse = (contactsAPIData: UserCollection): { [string]: contactType } => { const { entries = [] } = contactsAPIData; const contactsMap = {}; entries.forEach(contact => { const { id, login: email = '', name, type } = contact; contactsMap[email] = { id, email, name, type, }; }); return contactsMap; }; /** * Convert an enterprise groups API response into an array of internal USM contacts. * * @param {GroupCollection} contactsAPIData * @returns {Array<contactType>} Array of USM contacts */ export const convertGroupContactsResponse = (contactsAPIData: GroupCollection): Array<contactType> => { const { entries = [] } = contactsAPIData; // Only return groups with the correct permissions return entries .filter(({ permissions }) => { return permissions && permissions.can_invite_as_collaborator; }) .map(contact => { const { id, name, type } = contact; return { id, name, type, }; }) .sort(sortByName); };