1/* 2 * Copyright (c) 2023 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import UIAbility from '@ohos.app.ability.UIAbility'; 17import datafile from '@ohos.file.fileAccess'; 18import picker from '@ohos.file.picker'; 19import StartOptions from '@ohos.app.ability.StartOptions'; 20import abilityAccessCtrl from '@ohos.abilityAccessCtrl'; 21import { Permissions } from '@ohos.abilityAccessCtrl'; 22import GlobalContext from '../common/GlobalContext'; 23import Want from '@ohos.app.ability.Want'; 24import common from '@ohos.app.ability.common'; 25import AbilityConstant from '@ohos.app.ability.AbilityConstant'; 26import window from '@ohos.window'; 27import { BusinessError } from '@ohos.base'; 28import ability from '@ohos.ability.ability'; 29import * as ns from '@ohos.app.ability.UIAbility'; 30import dlpPermission from '@ohos.dlpPermission'; 31import fs from '@ohos.file.fs'; 32import abilityManager from '@ohos.app.ability.abilityManager'; 33import { startAlertAbility, getFileUriByPath, getFileFd, isValidPath } from '../common/utils'; 34import Constants from '../common/constant'; 35import fileAccess from '@ohos.file.fileAccess'; 36 37const TAG = '[DLPManager_SaveAs]'; 38let permissionList: Array<Permissions> = [ 39 'ohos.permission.READ_MEDIA', 40 'ohos.permission.WRITE_MEDIA', 41 'ohos.permission.FILE_ACCESS_MANAGER' 42]; 43 44class option_ { 45 offset: number = 0 46 length: number = 0 47} 48 49let defaultDlpFile: dlpPermission.DLPFile = { 50 dlpProperty: { 51 ownerAccount: '', 52 ownerAccountType: GlobalContext.load('domainAccount') as Boolean 53 ? dlpPermission.AccountType.DOMAIN_ACCOUNT : dlpPermission.AccountType.CLOUD_ACCOUNT, 54 authUserList: [], 55 contactAccount: '', 56 offlineAccess: true, 57 ownerAccountID: '', 58 everyoneAccessList: [] 59 }, 60 recoverDLPFile: async () => { 61 }, 62 closeDLPFile: async () => { 63 }, 64 addDLPLinkFile: async () => { 65 }, 66 stopFuseLink: async () => { 67 }, 68 resumeFuseLink: async () => { 69 }, 70 replaceDLPLinkFile: async () => { 71 }, 72 deleteDLPLinkFile: async () => { 73 } 74}; 75 76const SUFFIX_INDEX = 2; 77const HEAD_LENGTH_IN_BYTE = 20; 78const HEAD_LENGTH_IN_U32 = 5; 79const TXT_OFFSET = 3; 80const SIZE_OFFSET = 4; 81const ARGS_ZERO = 0; 82const ARGS_ONE = 1; 83const ARGS_TWO = 2; 84const ACTION: Record<string, string> = { 85 'SELECT_ACTION': 'ohos.want.action.OPEN_FILE', 86 'SELECT_ACTION_MODAL': 'ohos.want.action.OPEN_FILE_SERVICE', 87 'SAVE_ACTION': 'ohos.want.action.CREATE_FILE', 88 'SAVE_ACTION_MODAL': 'ohos.want.action.CREATE_FILE_SERVICE', 89}; 90 91const ErrCode: Record<string, number> = { 92 'INVALID_ARGS': 13900020, 93 'RESULT_ERROR': 13900042, 94 'NAME_TOO_LONG': 13900030, 95}; 96 97const ERRCODE_MAP = new Map([ 98 [ErrCode.INVALID_ARGS, 'Invalid argument'], 99 [ErrCode.RESULT_ERROR, 'Unknown error'], 100 [ErrCode.NAME_TOO_LONG, 'File name too long'], 101]); 102export default class SaveAsAbility extends UIAbility { 103 result: ability.AbilityResult = { 104 resultCode: -1, 105 want: { 106 bundleName: '', 107 abilityName: '', 108 parameters: { 109 pick_path_return: [], 110 pick_fd_return: 0 111 } 112 } 113 }; 114 dlpFile: dlpPermission.DLPFile = defaultDlpFile; 115 sandboxBundleName: string = ''; 116 resultUri: string = ''; 117 tokenId: number = -1; 118 requestCode: number = -1; 119 fileName: string = ''; 120 suffix: string = ''; 121 authPerm: dlpPermission.DLPFileAccess = dlpPermission.DLPFileAccess.READ_ONLY; 122 isOK: boolean = true; // use with startAlertAbility 123 124 async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> { 125 console.info(TAG, 'onCreate'); 126 GlobalContext.store('abilityWant', want); 127 GlobalContext.store('context', this.context); 128 GlobalContext 129 .store('dsHelper', await datafile.createFileAccessHelper(GlobalContext 130 .load('context') as common.UIAbilityContext)); 131 try { 132 let atManager = abilityAccessCtrl.createAtManager(); 133 await atManager.requestPermissionsFromUser(GlobalContext 134 .load('context') as common.UIAbilityContext, permissionList); 135 } catch (err) { 136 console.error(TAG, 'requestPermissionsFromUser failed', err.code, err.message); 137 return; 138 } 139 await this.prepareDlpFile(); 140 try { 141 await abilityManager.notifySaveAsResult(this.result, this.requestCode); 142 } catch (err) { 143 console.error(TAG, 'notifySaveAsResult failed ', (err as BusinessError).code, (err as BusinessError).message); 144 } 145 if (this.isOK === true) { 146 (GlobalContext.load('context') as common.UIAbilityContext).terminateSelf(); 147 } 148 } 149 150 onDestroy(): void { 151 console.info(TAG, 'onDestroy'); 152 } 153 154 async onWindowStageCreate(windowStage: window.WindowStage): Promise<void> { 155 // Main window is created, set main page for this ability 156 console.info(TAG, 'onWindowStageCreate: ', GlobalContext 157 .load('context') as common.UIAbilityContext); 158 159 } 160 161 onWindowStageDestroy(): void { 162 // Main window is destroyed, release UI related resources 163 console.info(TAG, 'onWindowStageDestroy'); 164 } 165 166 onForeground(): void { 167 // Ability has brought to foreground 168 console.info(TAG, 'onForeground'); 169 } 170 171 onBackground(): void { 172 // Ability has back to background 173 console.info(TAG, 'onBackground'); 174 } 175 176 async parseParams(): Promise<boolean | void> { 177 if (GlobalContext.load('abilityWant') === undefined || 178 (GlobalContext.load('abilityWant') as Want).parameters === undefined) { 179 console.error(TAG, 'invalid abilityWant'); 180 return false; 181 } 182 183 this.requestCode = (GlobalContext.load('abilityWant') as Want) 184 .parameters?.['requestCode'] as number; 185 if (this.requestCode === undefined) { 186 console.error(TAG, 'invalid requestCode'); 187 return false; 188 } 189 190 this.tokenId = (GlobalContext.load('abilityWant') as Want) 191 .parameters?.['ohos.aafwk.param.callerToken'] as number; 192 if (this.tokenId === undefined) { 193 console.error(TAG, 'invalid tokenId'); 194 return false; 195 } 196 197 this.authPerm = (GlobalContext.load('token2File') as 198 Map<number, (number | string | dlpPermission.DLPFile | dlpPermission.DLPFileAccess)[]>) 199 .get(this.tokenId)?.[3] as dlpPermission.DLPFileAccess; 200 201 if (this.authPerm != dlpPermission.DLPFileAccess.CONTENT_EDIT 202 && this.authPerm != dlpPermission.DLPFileAccess.FULL_CONTROL) { 203 console.error(TAG, 'invalid authPerm ', this.authPerm); 204 this.isOK = false; 205 await startAlertAbility(GlobalContext.load('context') as common.UIAbilityContext, 206 { 207 code: Constants.ERR_JS_DLP_FILE_READ_ONLY 208 } as BusinessError); 209 return false; 210 } 211 if (!(GlobalContext.load('token2File') as Map<number, Object[]>).has(this.tokenId)) { 212 console.error(TAG, 'invalid token2File'); 213 return; 214 } 215 216 this.fileName = (GlobalContext.load('abilityWant') as Want) 217 .parameters?.['key_pick_file_name'] as string; 218 if (this.fileName === undefined) { 219 console.error(TAG, 'invalid fileName'); 220 return false; 221 } 222 this.fileName = String(this.fileName); 223 224 let splitNames = this.fileName.split('.'); 225 console.debug(TAG, 'splitNames:', splitNames); 226 if (splitNames.length <= SUFFIX_INDEX) { 227 console.error(TAG, 'get suffix failed'); 228 return; 229 } 230 this.suffix = splitNames[splitNames.length - SUFFIX_INDEX]; 231 console.info(TAG, 'suffix is', this.suffix); 232 return true; 233 } 234 235 async copyDlpHead(srcFd: number, dstFd: number): Promise<boolean> { 236 try { 237 let z = new ArrayBuffer(HEAD_LENGTH_IN_BYTE); 238 let option: option_ = { 239 offset: 0, 240 length: HEAD_LENGTH_IN_BYTE 241 }; 242 let num = fs.readSync(srcFd, z, option); 243 let buf = new Uint32Array(z, 0, HEAD_LENGTH_IN_U32); 244 245 let txtOffset = buf[TXT_OFFSET]; 246 let head = new ArrayBuffer(txtOffset); 247 option = { 248 offset: 0, 249 length: txtOffset 250 }; 251 num = fs.readSync(srcFd, head, option); 252 let buf2 = new Uint32Array(head, 0, HEAD_LENGTH_IN_U32); 253 buf2[SIZE_OFFSET] = 0; 254 num = fs.writeSync(dstFd, head, option); 255 } catch (err) { 256 console.error(TAG, 'copyDlpHead ', (err as BusinessError).code, (err as BusinessError).message); 257 return false; 258 } 259 260 return true; 261 } 262 263 parseDocumentPickerSaveOption(args: picker.DocumentSaveOptions[], action: string) { 264 let config: Record<string, string | Record<string, Object>> = { 265 'action': action, 266 'parameters': { 267 'startMode': 'save', 268 } as Record<string, Object> 269 }; 270 271 if (args.length > ARGS_ZERO && typeof args[ARGS_ZERO] === 'object') { 272 let option: picker.DocumentSaveOptions = args[ARGS_ZERO]; 273 if ((option.newFileNames !== undefined) && option.newFileNames.length > 0) { 274 config.parameters['key_pick_file_name'] = option.newFileNames; 275 config.parameters['saveFile'] = option.newFileNames[0]; 276 } 277 278 if (option.defaultFilePathUri !== undefined) { 279 config.parameters['key_pick_dir_path'] = option.defaultFilePathUri; 280 } 281 if ((option.fileSuffixChoices !== undefined) && option.fileSuffixChoices.length > 0) { 282 config.parameters['key_file_suffix_choices'] = option.fileSuffixChoices; 283 } 284 } 285 286 console.log(TAG, '[picker] Save config: ' + JSON.stringify(config)); 287 return config; 288 } 289 290 getDocumentPickerSaveResult(args: ability.AbilityResult) { 291 let saveResult: Record<string, BusinessError | string[]> = { 292 'error': {} as BusinessError, 293 'data': [] 294 }; 295 296 if ((args.resultCode !== undefined && args.resultCode === 0)) { 297 if (args.want && args.want.parameters) { 298 saveResult.data = args.want.parameters.pick_path_return as string[]; 299 } 300 } else if ((args.resultCode !== undefined && args.resultCode === -1)) { 301 saveResult.data = []; 302 } else { 303 saveResult.error = this.getErr(ErrCode.RESULT_ERROR) as BusinessError; 304 } 305 306 console.log(TAG, '[picker] Save saveResult: ' + JSON.stringify(saveResult)); 307 return saveResult; 308 } 309 310 getErr(errCode: number) { 311 return {code: errCode, message: ERRCODE_MAP.get(errCode)} as BusinessError; 312 } 313 async documentPickerSave(...args: Object[]): Promise<BusinessError | string[] | undefined> { 314 let context = GlobalContext.load('context') as common.UIAbilityContext; 315 let config: Record<string, string | Record<string, Object>>; 316 let result: ability.AbilityResult; 317 try { 318 config = this.parseDocumentPickerSaveOption(args, ACTION.SAVE_ACTION_MODAL); 319 config = this.parseDocumentPickerSaveOption(args, ACTION.SAVE_ACTION); 320 result = await context.startAbilityForResult(config, {windowMode: 0}); 321 } catch (error) { 322 console.log(TAG, '[picker] error: ' + error); 323 return undefined; 324 } 325 console.log(TAG, '[picker] Save result: ' + JSON.stringify(result)); 326 try { 327 const saveResult: Record<string, BusinessError | string[]> = this.getDocumentPickerSaveResult(result); 328 if (args.length === ARGS_TWO && typeof args[ARGS_ONE] === 'function') { 329 return (args[ARGS_ONE] as Function)(saveResult.error, saveResult.data); 330 } else if (args.length === ARGS_ONE && typeof args[ARGS_ZERO] === 'function') { 331 return (args[ARGS_ZERO] as Function)(saveResult.error, saveResult.data); 332 } 333 return new Promise<BusinessError | string[]>((resolve, reject) => { 334 if (saveResult.data !== undefined) { 335 resolve(saveResult.data); 336 } else { 337 reject(saveResult.error); 338 } 339 }) 340 } catch (resultError) { 341 console.log(TAG, '[picker] Result error: ' + resultError); 342 } 343 return undefined; 344 } 345 346 async prepareDlpFile(): Promise<void> { 347 console.info(TAG, 'getFile start:', JSON.stringify(GlobalContext.load('abilityWant'))); 348 let uri = ''; 349 let displayName = ''; 350 351 let ret = await this.parseParams(); 352 if (!ret) { 353 return; 354 } 355 let DocumentSaveOptions = new picker.DocumentSaveOptions(); 356 displayName = this.fileName; 357 DocumentSaveOptions.newFileNames = [displayName]; 358 let documentPicker = new picker.DocumentViewPicker(); 359 let dstFd: number; 360 let file: fs.File | undefined; 361 362 try { 363 364 let saveRes: BusinessError | string[] | undefined = await this.documentPickerSave(DocumentSaveOptions); 365 if (saveRes == undefined || (saveRes instanceof Array && saveRes.length == 0)) { 366 console.error(TAG, 'fail to get uri'); 367 return; 368 } 369 console.info(TAG, 'get uri', saveRes) 370 uri = saveRes[0] 371 if (!isValidPath(uri)){ 372 console.error(TAG, 'invalid uri'); 373 return; 374 } 375 try { 376 file = await fs.open(uri, fs.OpenMode.READ_WRITE); 377 dstFd = file.fd; 378 } catch (err) { 379 console.error(TAG, 'open', uri, 'failed', (err as BusinessError).code, (err as BusinessError).message); 380 try { 381 if (file != undefined) { 382 await fs.close(file); 383 } 384 } catch (err) { 385 console.log(TAG, 'close fail', (err as BusinessError).code, (err as BusinessError).message); 386 } 387 await (GlobalContext.load('dsHelper') as fileAccess.FileAccessHelper).delete(uri); 388 this.isOK = false; 389 await startAlertAbility(GlobalContext.load('context') as common.UIAbilityContext, 390 { 391 code: Constants.ERR_JS_APP_INSIDE_ERROR 392 } as BusinessError); 393 return; 394 } 395 396 let token2File_value: (number | string | dlpPermission.DLPFile | dlpPermission.DLPFileAccess)[] 397 = (GlobalContext 398 .load('token2File') as Map<number, (number | string | dlpPermission.DLPFile | dlpPermission.DLPFileAccess)[]>) 399 .get(this.tokenId) as (number | string | dlpPermission.DLPFile | dlpPermission.DLPFileAccess)[]; 400 this.dlpFile = token2File_value[0] as dlpPermission.DLPFile; 401 this.sandboxBundleName = token2File_value[1] as string; 402 let appId: number = token2File_value[2] as number; 403 let srcUri: string = token2File_value[4] as string; 404 405 let srcFd = getFileFd(srcUri); 406 let copyRes = await this.copyDlpHead(srcFd, dstFd); 407 if (!copyRes) { 408 try { 409 await fs.close(file); 410 } catch (err) { 411 console.log(TAG, 'close fail', (err as BusinessError).code, (err as BusinessError).message); 412 } 413 await (GlobalContext.load('dsHelper') as fileAccess.FileAccessHelper).delete(uri); 414 fs.closeSync(srcFd); 415 this.isOK = false; 416 await startAlertAbility(GlobalContext.load('context') as common.UIAbilityContext, 417 { 418 code: Constants.ERR_JS_APP_INSIDE_ERROR 419 } as BusinessError); 420 return; 421 } 422 let newDlpFile: dlpPermission.DLPFile; 423 try { 424 newDlpFile = await dlpPermission.openDLPFile(dstFd); 425 } catch (err) { 426 console.error(TAG, 'generateDlpFile', dstFd, 'failed', (err as BusinessError).code, (err as BusinessError).message); 427 try { 428 await fs.close(file); 429 } catch (err) { 430 console.log(TAG, 'close fail', (err as BusinessError).code, (err as BusinessError).message); 431 } 432 await (GlobalContext.load('dsHelper') as fileAccess.FileAccessHelper).delete(uri); 433 fs.closeSync(srcFd); 434 return; 435 } 436 437 let date = new Date(); 438 let timestamp = new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 439 date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), date.getMilliseconds()).getTime(); 440 441 let linkFileName = String(this.sandboxBundleName).substring(0, Constants.BUNDLE_LEN) + '_' + appId + 442 '_' + timestamp + String(Math.random()).substring(Constants.RAND_START, Constants.RAND_END) + '.' + 443 this.suffix + '.dlp.link'; 444 445 try { 446 await newDlpFile.addDLPLinkFile(linkFileName); 447 } catch (err) { 448 console.error(TAG, 'addDlpLinkFile failed', (err as BusinessError).code, (err as BusinessError).message); 449 try { 450 await newDlpFile.closeDLPFile(); 451 } catch (err) { 452 console.error(TAG, 'closeDlpFile failed', (err as BusinessError).code, (err as BusinessError).message); 453 } 454 try { 455 await fs.close(file); 456 } catch (err) { 457 console.log(TAG, 'close fail', (err as BusinessError).code, (err as BusinessError).message); 458 } 459 await (GlobalContext.load('dsHelper') as fileAccess.FileAccessHelper).delete(uri); 460 fs.closeSync(srcFd); 461 this.isOK = false; 462 await startAlertAbility(GlobalContext.load('context') as common.UIAbilityContext, 463 { 464 code: Constants.ERR_JS_APP_INSIDE_ERROR 465 } as BusinessError); 466 return; 467 } 468 469 let linkFilePath = Constants.FUSE_PATH + linkFileName; 470 let linkUri = getFileUriByPath(linkFilePath); 471 (GlobalContext.load("token2File") as Map<number, Object[]>) 472 .set(this.tokenId, [this.dlpFile, this.sandboxBundleName, appId, this.authPerm, srcUri]); 473 let sandbox2linkFile: Map<string, (number | string | dlpPermission.DLPFile)[][]> 474 = GlobalContext 475 .load('sandbox2linkFile') as Map<string, (number | string | dlpPermission.DLPFile)[][]>; 476 sandbox2linkFile.get(this.sandboxBundleName + appId)?.push([newDlpFile, linkFileName, dstFd, this.tokenId]); 477 478 (GlobalContext.load("fileOpenHistory") as Map<string, Object[]>) 479 .set(uri, [this.sandboxBundleName, appId, linkFileName, linkUri]); 480 481 (GlobalContext.load("linkSet") as Set<string>).add(linkUri); 482 483 this.resultUri = getFileUriByPath(linkFilePath); 484 485 console.info(TAG, 'result uri is', this.resultUri); 486 487 (this.result.want?.parameters?.pick_path_return as string[]).push(this.resultUri); 488 this.result.resultCode = 0; 489 fs.closeSync(srcFd); 490 return; 491 } catch (err) { 492 console.error(TAG, 'DocumentViewPicker failed', (err as BusinessError).code, (err as BusinessError).message); 493 try { 494 if (file != undefined) { 495 await fs.close(file); 496 } 497 } catch (err) { 498 console.log(TAG, 'close fail', (err as BusinessError).code, (err as BusinessError).message); 499 } 500 await (GlobalContext.load('dsHelper') as fileAccess.FileAccessHelper).delete(uri); 501 this.isOK = false; 502 await startAlertAbility(GlobalContext.load('context') as common.UIAbilityContext, 503 { 504 code: Constants.ERR_JS_APP_INSIDE_ERROR 505 } as BusinessError); 506 return; 507 } 508 } 509 510}; 511