• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
16let cert = requireInternal('security.cert');
17let webview = requireInternal('web.webview');
18let fileIo = requireNapi('file.fs');
19let fileUri = requireNapi('file.fileuri');
20let picker = requireNapi('file.picker');
21let photoAccessHelper = requireNapi('file.photoAccessHelper');
22let cameraPicker = requireNapi('multimedia.cameraPicker');
23let camera = requireNapi('multimedia.camera');
24let accessControl = requireNapi('abilityAccessCtrl');
25let deviceinfo = requireInternal('deviceInfo');
26let promptAction = requireNapi('promptAction');
27const PARAM_CHECK_ERROR = 401;
28
29const ERROR_MSG_INVALID_PARAM = 'Invalid input parameter';
30
31let errMsgMap = new Map();
32errMsgMap.set(PARAM_CHECK_ERROR, ERROR_MSG_INVALID_PARAM);
33let customDialogComponentId = 0;
34
35class BusinessError extends Error {
36  constructor(code, errorMsg = 'undefined') {
37    if (errorMsg === 'undefined') {
38      let msg = errMsgMap.get(code);
39      super(msg);
40    } else {
41      super(errorMsg);
42    }
43    this.code = code;
44  }
45}
46
47function getCertificatePromise(certChainData) {
48  let x509CertArray = [];
49  if (!(certChainData instanceof Array)) {
50    console.log('failed, cert chain data type is not array');
51    return Promise.all(x509CertArray);
52  }
53
54  for (let i = 0; i < certChainData.length; i++) {
55    let encodeBlobData = {
56      data: certChainData[i],
57      encodingFormat: cert.EncodingFormat.FORMAT_DER
58    };
59    x509CertArray[i] = cert.createX509Cert(encodeBlobData);
60  }
61
62  return Promise.all(x509CertArray);
63}
64
65function takePhoto(param, selectResult) {
66  try {
67    let pickerProfileOptions = {
68      'cameraPosition': camera.CameraPosition.CAMERA_POSITION_BACK,
69    };
70    let acceptTypes = param.getAcceptType();
71    let mediaType = [];
72    if (isContainImageMimeType(acceptTypes)) {
73      mediaType.push(cameraPicker.PickerMediaType.PHOTO);
74    }
75    if (isContainVideoMimeType(acceptTypes)) {
76      mediaType.push(cameraPicker.PickerMediaType.VIDEO);
77    }
78    cameraPicker.pick(getContext(this), mediaType, pickerProfileOptions)
79      .then((pickerResult) => {
80        selectResult.handleFileList([pickerResult.resultUri]);
81      }).catch((error) => {
82      console.log('selectFile error:' + JSON.stringify(error));
83      throw error;
84    });
85
86  } catch (error) {
87    console.log('the pick call failed, error code' + JSON.stringify(error));
88    selectResult.handleFileList([]);
89    promptAction.showToast({ message: '无法打开拍照功能,请检查是否具备拍照功能' });
90  }
91}
92
93function needShowDialog(params) {
94  let result = false;
95  try {
96    if (params.isCapture()) {
97      console.log('input element contain capture tag, not show dialog');
98      return false;
99    }
100    let acceptTypes = params.getAcceptType();
101    if (isContainImageMimeType(acceptTypes) || isContainVideoMimeType(acceptTypes)) {
102      result = true;
103    }
104  } catch (error) {
105    console.log('show dialog happend error:' + JSON.stringify(error));
106  }
107  return result;
108}
109
110function selectFile(param, result) {
111  try {
112    let documentPicker = new picker.DocumentViewPicker();
113    if (param.getMode() !== FileSelectorMode.FileSaveMode) {
114      documentPicker.select(createDocumentSelectionOptions(param))
115        .then((documentSelectResult) => {
116          let filePath = documentSelectResult;
117          result.handleFileList(filePath);
118        }).catch((error) => {
119        console.log('selectFile error: ' + JSON.stringify(error));
120        throw error;
121      });
122    } else {
123      documentPicker.save(createDocumentSaveOptions(param))
124        .then((documentSaveResult) => {
125          let filePaths = documentSaveResult;
126          let tempUri = '';
127          if (filePaths.length > 0) {
128            let fileName = filePaths[0].substr(filePaths[0].lastIndexOf('/'));
129            let tempPath = getContext(this).filesDir + fileName;
130            tempUri = fileUri.getUriFromPath(tempPath);
131            let randomAccessFile = fileIo.createRandomAccessFileSync(tempPath, fileIo.OpenMode.CREATE);
132            randomAccessFile.close();
133
134            let watcher = fileIo.createWatcher(tempPath, 0x4, () => {
135              fileIo.copy(tempUri, filePaths[0]).then(() => {
136                console.log('Web save file succeeded in copying.');
137                fileIo.unlink(tempPath);
138              }).catch((err) => {
139                console.error(`Web save file failed to copy: ${JSON.stringify(err)}.`);
140              }).finally(() => {
141                watcher.stop();
142              });
143            });
144            watcher.start();
145          }
146          result.handleFileList([tempUri]);
147        }).catch((error) => {
148        console.log('saveFile error: ' + JSON.stringify(error));
149        throw error;
150      });
151    }
152  } catch (error) {
153    console.log('picker error: ' + JSON.stringify(error));
154    result.handleFileList([]);
155    promptAction.showToast({ message: '无法打开文件功能,请检查是否具备文件功能' });
156  }
157}
158
159function createDocumentSelectionOptions(param) {
160  let documentSelectOptions = new picker.DocumentSelectOptions();
161  let currentDevice = deviceinfo.deviceType.toLowerCase();
162  try {
163    let defaultSelectNumber = 500;
164    let defaultSelectMode = picker.DocumentSelectMode.MIXED;
165    documentSelectOptions.maxSelectNumber = defaultSelectNumber;
166    documentSelectOptions.selectMode = defaultSelectMode;
167    let mode = param.getMode();
168    switch (mode) {
169      case FileSelectorMode.FileOpenMode:
170        documentSelectOptions.maxSelectNumber = 1;
171        documentSelectOptions.selectMode = picker.DocumentSelectMode.FILE;
172        break;
173      case FileSelectorMode.FileOpenMultipleMode:
174        documentSelectOptions.selectMode = picker.DocumentSelectMode.FILE;
175        break;
176      case FileSelectorMode.FileOpenFolderMode:
177        documentSelectOptions.selectMode = picker.DocumentSelectMode.FOLDER;
178        break;
179      default:
180        break;
181    }
182    documentSelectOptions.fileSuffixFilters = [];
183    let suffix = param.getAcceptType().join(',');
184    if (suffix) {
185      documentSelectOptions.fileSuffixFilters.push(suffix);
186    }
187    if (currentDevice !== 'phone') {
188      documentSelectOptions.fileSuffixFilters.push('.*');
189    }
190  } catch (error) {
191    console.log('selectFile error: ' + + JSON.stringify(error));
192  }
193  return documentSelectOptions;
194}
195
196function createDocumentSaveOptions(param) {
197  let documentSaveOptions = new picker.DocumentSaveOptions();
198  let currentDevice = deviceinfo.deviceType.toLowerCase();
199  try {
200    documentSaveOptions.pickerMode = picker.DocumentPickerMode.DEFAULT;
201    documentSaveOptions.fileSuffixChoices = [];
202    let suffix = param.getAcceptType().join(',');
203    if (suffix) {
204      documentSaveOptions.fileSuffixChoices.push(suffix);
205    }
206    if (currentDevice !== 'phone') {
207      documentSaveOptions.fileSuffixChoices.push('.*');
208    }
209  } catch (error) {
210    console.log('saveFile error: ' + + JSON.stringify(error));
211  }
212  return documentSaveOptions;
213}
214
215function isContainImageMimeType(acceptTypes) {
216  if (!(acceptTypes instanceof Array)) {
217    return false;
218  }
219  if (acceptTypes.length < 1) {
220    return true;
221  }
222
223  let imageTypes = ['tif', 'xbm', 'tiff', 'pjp', 'jfif', 'bmp', 'avif', 'apng', 'ico',
224    'webp', 'svg', 'gif', 'svgz', 'jpg', 'jpeg', 'png', 'pjpeg'];
225  for (let i = 0; i < acceptTypes.length; i++) {
226    for (let j = 0; j < imageTypes.length; j++) {
227      if (acceptTypes[i].includes(imageTypes[j])) {
228        return true;
229      }
230    }
231  }
232  return false;
233}
234
235function isContainVideoMimeType(acceptTypes) {
236  if (!(acceptTypes instanceof Array)) {
237    return false;
238  }
239  if (acceptTypes.length < 1) {
240    return true;
241  }
242
243  let videoTypes = ['ogm', 'ogv', 'mpg', 'mp4', 'mpeg', 'm4v', 'webm'];
244  for (let i = 0; i < acceptTypes.length; i++) {
245    for (let j = 0; j < videoTypes.length; j++) {
246      if (acceptTypes[i].includes(videoTypes[j])) {
247        return true;
248      }
249    }
250  }
251  return false;
252}
253
254function fileSelectorListItem(callback, sysResource, text, func) {
255  const itemCreation = (elmtId, isInitialRender) => {
256    ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
257    itemCreation2(elmtId, isInitialRender);
258    if (!isInitialRender) {
259      ListItem.pop();
260    }
261    ViewStackProcessor.StopGetAccessRecording();
262  };
263  const itemCreation2 = (elmtId, isInitialRender) => {
264    ListItem.create(deepRenderFunction, true);
265    ListItem.onClick(() => {
266      promptAction.closeCustomDialog(customDialogComponentId);
267      func(callback.fileparam, callback.fileresult);
268    });
269    ListItem.height(48);
270    ListItem.padding({
271      left: 24,
272      right: 24
273    });
274  };
275  const deepRenderFunction = (elmtId, isInitialRender) => {
276    itemCreation(elmtId, isInitialRender);
277    Row.create();
278    SymbolGlyph.create({ 'id': -1, 'type': -1, params: [sysResource], 'bundleName': 'com.example.selectdialog', 'moduleName': 'entry' });
279    SymbolGlyph.width(24);
280    SymbolGlyph.height(24);
281    SymbolGlyph.fontSize(24);
282    SymbolGlyph.margin({
283      right: 16
284    });
285    Row.create();
286    Row.width(deviceinfo.deviceType.toLowerCase() === '2in1' ? 312 : 240);
287    Row.border({ width: { bottom: 0.5 }, color: '#33000000' });
288    Text.create(text);
289    Text.fontSize(16);
290    Text.fontWeight(FontWeight.Medium);
291    Text.lineHeight(19);
292    Text.margin({
293      top: 13,
294      bottom: 13
295    });
296    Text.pop();
297    Row.pop();
298    Row.pop();
299    ListItem.pop();
300  };
301  itemCreation(ViewStackProcessor.AllocateNewElmetIdForNextComponent(), true);
302  ListItem.pop();
303}
304
305function fileSelectorDialog(callback) {
306  Row.create();
307  Row.height(56);
308  Text.create('选择上传');
309  Text.fontSize(20);
310  Text.fontWeight(FontWeight.Bold);
311  Text.lineHeight(23);
312  Text.margin({
313    top: 15,
314    bottom: 15,
315    left: 24,
316    right: 24,
317  });
318  Text.pop();
319  Row.pop();
320  List.create();
321  List.width('100%');
322  fileSelectorListItem(callback, 'sys.symbol.picture', '照片', selectPicture);
323  fileSelectorListItem(callback, 'sys.symbol.camera', '拍照', takePhoto);
324  fileSelectorListItem(callback, 'sys.symbol.doc_text', '文件', selectFile);
325  List.pop();
326}
327
328function fileSelectorDialogForPC(callback) {
329  Column.create();
330  Column.height(272);
331  Column.width(400);
332  fileSelectorDialog(callback);
333  Row.create();
334  Row.onClick(() => {
335    try {
336      console.log('Get Alert Dialog handled');
337      callback.fileresult.handleFileList([]);
338      promptAction.closeCustomDialog(customDialogComponentId);
339    }
340    catch (error) {
341      let message = error.message;
342      let code = error.code;
343      console.error(`closeCustomDialog error code is ${code}, message is ${message}`);
344    }
345  });
346  Row.width(368);
347  Row.height(40);
348  Row.margin(16);
349  Row.borderRadius(5);
350  Row.backgroundColor('#ededed');
351  Row.justifyContent(FlexAlign.Center);
352  Text.create('取消');
353  Text.fontSize(16);
354  Text.fontColor('#FF0A59F7');
355  Text.fontWeight(FontWeight.Medium);
356  Text.margin({
357    top: 10,
358    bottom: 10,
359    left: 16,
360    right: 16
361  });
362  Text.pop();
363  Row.pop();
364  Column.pop();
365}
366
367function fileSelectorDialogForPhone(callback) {
368  Column.create();
369  Column.height(264);
370  Column.width(328);
371  fileSelectorDialog(callback);
372  Row.create();
373  Row.onClick(() => {
374    try {
375      console.log('Get Alert Dialog handled');
376      callback.fileresult.handleFileList([]);
377      promptAction.closeCustomDialog(customDialogComponentId);
378    }
379    catch (error) {
380      let message = error.message;
381      let code = error.code;
382      console.error(`closeCustomDialog error code is ${code}, message is ${message}`);
383    }
384  });
385  Row.width(296);
386  Row.height(40);
387  Row.margin({
388    top: 8,
389    bottom: 16,
390    left: 16,
391    right: 16
392  });
393  Row.borderRadius(5);
394  Row.justifyContent(FlexAlign.Center);
395  Text.create('取消');
396  Text.fontSize(16);
397  Text.fontColor('#FF0A59F7');
398  Text.fontWeight(FontWeight.Medium);
399  Text.margin({
400    top: 10,
401    bottom: 10,
402    left: 104,
403    right: 104
404  });
405  Text.pop();
406  Row.pop();
407  Column.pop();
408}
409
410function selectPicture(param, selectResult) {
411  try {
412    let photoResultArray = [];
413    let photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
414    if (param.getMode() === FileSelectorMode.FileOpenMode) {
415      console.log('allow select single photo or video');
416      photoSelectOptions.maxSelectNumber = 1;
417    }
418    let acceptTypes = param.getAcceptType();
419    photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE;
420    if (isContainImageMimeType(acceptTypes) && !isContainVideoMimeType(acceptTypes)) {
421      photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
422    }
423    if (!isContainImageMimeType(acceptTypes) && isContainVideoMimeType(acceptTypes)) {
424      photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE;
425    }
426
427    let photoPicker = new photoAccessHelper.PhotoViewPicker();
428    photoPicker.select(photoSelectOptions).then((photoSelectResult) => {
429      for (let i = 0; i < photoSelectResult.photoUris.length; i++) {
430        photoResultArray.push(photoSelectResult.photoUris[i]);
431      }
432      selectResult.handleFileList(photoResultArray);
433    });
434  } catch (error) {
435    console.log('selectPicture error' + JSON.stringify(error));
436    selectResult.handleFileList([]);
437    promptAction.showToast({ message: '无法打开图片功能,请检查是否具备图片功能' });
438  }
439}
440
441Object.defineProperty(webview.WebviewController.prototype, 'getCertificate', {
442  value: function (callback) {
443    if (arguments.length !== 0 && arguments.length !== 1) {
444      throw new BusinessError(PARAM_CHECK_ERROR,
445        'BusinessError 401: Parameter error. The number of params must be zero or one.');
446    }
447
448    let certChainData = this.innerGetCertificate();
449    if (callback === undefined) {
450      console.log('get certificate promise');
451      return getCertificatePromise(certChainData);
452    } else {
453      console.log('get certificate async callback');
454      if (typeof callback !== 'function') {
455        throw new BusinessError(PARAM_CHECK_ERROR,
456          'BusinessError 401: Parameter error. The type of "callback" must be function.');
457      }
458      return getCertificatePromise(certChainData).then(x509CertArray => {
459        callback(undefined, x509CertArray);
460      }).catch(error => {
461        callback(error, undefined);
462      });
463    }
464  }
465});
466
467Object.defineProperty(webview.WebviewController.prototype, 'fileSelectorShowFromUserWeb', {
468  value: function (callback) {
469    let currentDevice = deviceinfo.deviceType.toLowerCase();
470    if (needShowDialog(callback.fileparam)) {
471      promptAction.openCustomDialog({
472        builder: () => {
473          if (currentDevice === '2in1') {
474            fileSelectorDialogForPC(callback);
475          } else {
476            fileSelectorDialogForPhone(callback);
477          }
478        },
479        onWillDismiss: (dismissDialogAction) => {
480          console.info('reason' + JSON.stringify(dismissDialogAction.reason));
481          console.log('dialog onWillDismiss');
482          if (dismissDialogAction.reason === DismissReason.PRESS_BACK) {
483            callback.fileresult.handleFileList([]);
484            dismissDialogAction.dismiss();
485          }
486          if (dismissDialogAction.reason === DismissReason.TOUCH_OUTSIDE) {
487            callback.fileresult.handleFileList([]);
488            dismissDialogAction.dismiss();
489          }
490        }
491      }).then((dialogId) => {
492        customDialogComponentId = dialogId;
493      })
494        .catch((error) => {
495          callback.fileresult.handleFileList([]);
496          console.error(`openCustomDialog error code is ${error.code}, message is ${error.message}`);
497        });
498    } else if (callback.fileparam.isCapture() &&
499      (isContainImageMimeType(callback.fileparam.getAcceptType()) || isContainVideoMimeType(callback.fileparam.getAcceptType()))) {
500      console.log('take photo will be directly invoked due to the capture property');
501      takePhoto(callback.fileparam, callback.fileresult);
502    } else {
503      console.log('selectFile will be invoked by web');
504      selectFile(callback.fileparam, callback.fileresult);
505    }
506  }
507});
508
509Object.defineProperty(webview.WebviewController.prototype, 'requestPermissionsFromUserWeb', {
510  value: function (callback) {
511    let accessManger = accessControl.createAtManager();
512    let abilityContext = getContext(this);
513    accessManger.requestPermissionsFromUser(abilityContext, ['ohos.permission.READ_PASTEBOARD'])
514      .then((PermissionRequestResult) => {
515        if (PermissionRequestResult.authResults[0] === 0) {
516          console.log('requestPermissionsFromUserWeb is allowed');
517          callback.request.grant(callback.request.getAccessibleResource());
518        }
519        else {
520          console.log('requestPermissionsFromUserWeb is refused');
521          callback.request.deny();
522        }
523      })
524      .catch((error) => {
525        callback.request.deny();
526      });
527  }
528});
529
530Object.defineProperty(webview.WebviewController.prototype, 'openAppLink', {
531  value: function (callback) {
532    let abilityContext = getContext(this);
533    try {
534      let option = {
535        appLinkingOnly: true
536      };
537      console.log('begin openAppLink');
538      abilityContext.openLink(callback.url, option, null)
539        .then(() => {
540          console.log('applink success openLink');
541          callback.result.cancelLoad();
542        })
543        .catch((error) => {
544          console.log(`applink openLink ErrorCode: ${error.code},  Message: ${error.message}`);
545          callback.result.continueLoad();
546        });
547    } catch (err) {
548      console.log(`applink openLink ErrorCode: ${err.code},  Message: ${err.message}`);
549      setTimeout(() => {
550        callback.result.continueLoad();
551      }, 1);
552    }
553  }
554});
555
556export default webview;
557