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 picker = requireNapi('file.picker');
19let photoAccessHelper = requireNapi('file.photoAccessHelper');
20let cameraPicker = requireNapi('multimedia.cameraPicker');
21let camera = requireNapi('multimedia.camera');
22let accessControl = requireNapi('abilityAccessCtrl');
23let deviceinfo = requireInternal('deviceInfo');
24const PARAM_CHECK_ERROR = 401;
25
26const ERROR_MSG_INVALID_PARAM = 'Invalid input parameter';
27
28let errMsgMap = new Map();
29errMsgMap.set(PARAM_CHECK_ERROR, ERROR_MSG_INVALID_PARAM);
30
31class BusinessError extends Error {
32  constructor(code, errorMsg = 'undefined') {
33    if (errorMsg === 'undefined') {
34      let msg = errMsgMap.get(code);
35      super(msg);
36    } else {
37      super(errorMsg);
38    }
39    this.code = code;
40  }
41}
42
43function getCertificatePromise(certChainData) {
44  let x509CertArray = [];
45  if (!(certChainData instanceof Array)) {
46    console.log('failed, cert chain data type is not array');
47    return Promise.all(x509CertArray);
48  }
49
50  for (let i = 0; i < certChainData.length; i++) {
51    let encodeBlobData = {
52      data: certChainData[i],
53      encodingFormat: cert.EncodingFormat.FORMAT_DER
54    };
55    x509CertArray[i] = cert.createX509Cert(encodeBlobData);
56  }
57
58  return Promise.all(x509CertArray);
59}
60
61function takePhoto(param, selectResult) {
62  try {
63    let pickerProfileOptions = {
64      'cameraPosition': camera.CameraPosition.CAMERA_POSITION_BACK,
65    };
66    let acceptTypes = param.getAcceptType();
67    let mediaType = [];
68    if (isContainImageMimeType(acceptTypes) && !isContainVideoMimeType(acceptTypes)) {
69      mediaType.push(cameraPicker.PickerMediaType.PHOTO);
70    } else if (!isContainImageMimeType(acceptTypes) && isContainVideoMimeType(acceptTypes)) {
71      mediaType.push(cameraPicker.PickerMediaType.VIDEO);
72    } else {
73      mediaType.push(cameraPicker.PickerMediaType.PHOTO);
74      mediaType.push(cameraPicker.PickerMediaType.VIDEO);
75    }
76    cameraPicker.pick(getContext(this), mediaType, pickerProfileOptions)
77    .then((pickerResult) => {
78      if (pickerResult.resultCode === 0) {
79        selectResult.handleFileList([pickerResult.resultUri]);
80      }
81    }).catch((error) => {
82      console.log('selectFile error:' + JSON.stringify(error));
83    });
84
85  } catch (error) {
86    console.log('the pick call failed, error code' + JSON.stringify(error));
87  }
88}
89
90function needShowDialog(params) {
91  let result = false;
92  try {
93    let currentDevice = deviceinfo.deviceType.toLowerCase();
94    if (currentDevice !== 'phone') {
95      return false;
96    }
97    if (params.isCapture()) {
98      console.log('input element contain capture tag, not show dialog');
99      return false;
100    }
101    let acceptTypes = params.getAcceptType();
102    if (isContainImageMimeType(acceptTypes) || isContainVideoMimeType(acceptTypes)) {
103      result = true;
104    }
105  } catch (error) {
106    console.log('show dialog happend error:' + JSON.stringify(error));
107  }
108  return result;
109}
110
111function selectFile(param, result) {
112  try {
113    let documentSelectOptions = createDocumentSelectionOptions(param);
114    let documentPicker = new picker.DocumentViewPicker();
115    documentPicker.select(documentSelectOptions)
116      .then((documentSelectResult) => {
117        if (documentSelectResult && documentSelectResult.length > 0) {
118          let filePath = documentSelectResult;
119          result.handleFileList(filePath);
120        }
121      }).catch((error) => {
122        console.log('selectFile error: ' + JSON.stringify(error));
123      });
124  } catch (error) {
125    console.log('picker error: ' + JSON.stringify(error));
126  }
127}
128
129function createDocumentSelectionOptions(param) {
130  let documentSelectOptions = new picker.DocumentSelectOptions();
131  try {
132    let defaultSelectNumber = 500;
133    let defaultSelectMode = picker.DocumentSelectMode.MIXED;
134    documentSelectOptions.maxSelectNumber = defaultSelectNumber;
135    documentSelectOptions.selectMode = defaultSelectMode;
136    let mode = param.getMode();
137    switch (mode) {
138      case FileSelectorMode.FileOpenMode:
139        documentSelectOptions.maxSelectNumber = 1;
140        documentSelectOptions.selectMode = picker.DocumentSelectMode.FILE;
141        break;
142      case FileSelectorMode.FileOpenMultipleMode:
143        documentSelectOptions.selectMode = picker.DocumentSelectMode.FILE;
144        break;
145      case FileSelectorMode.FileOpenFolderMode:
146        documentSelectOptions.selectMode = picker.DocumentSelectMode.FOLDER;
147        break;
148      case FileSelectorMode.FileSaveMode:
149        break;
150      default:
151        break;
152    }
153    documentSelectOptions.fileSuffixFilters = [];
154    let suffix = param.getAcceptType().join(',');
155    if (suffix) {
156      documentSelectOptions.fileSuffixFilters.push(suffix);
157    }
158 } catch (error) {
159    console.log('selectFile error: ' + + JSON.stringify(error));
160    return documentSelectOptions;
161 }
162  return documentSelectOptions;
163}
164
165function isContainImageMimeType(acceptTypes) {
166  if (!(acceptTypes instanceof Array) || acceptTypes.length < 1) {
167    return false;
168  }
169
170  let imageTypes = ['tif', 'xbm', 'tiff', 'pjp', 'jfif', 'bmp', 'avif', 'apng', 'ico',
171                    'webp', 'svg', 'gif', 'svgz', 'jpg', 'jpeg', 'png', 'pjpeg'];
172  for (let i = 0; i < acceptTypes.length; i++) {
173    for (let j = 0; j < imageTypes.length; j++) {
174      if (acceptTypes[i].includes(imageTypes[j])) {
175        return true;
176      }
177    }
178  }
179  return false;
180}
181
182function isContainVideoMimeType(acceptTypes) {
183  if (!(acceptTypes instanceof Array) || acceptTypes.length < 1) {
184    return false;
185  }
186
187  let videoTypes = ['ogm', 'ogv', 'mpg', 'mp4', 'mpeg', 'm4v', 'webm'];
188  for (let i = 0; i < acceptTypes.length; i++) {
189    for (let j = 0; j < videoTypes.length; j++) {
190      if (acceptTypes[i].includes(videoTypes[j])) {
191        return true;
192      }
193    }
194  }
195  return false;
196}
197
198function selectPicture(param, selectResult) {
199  try {
200    let photoResultArray = [];
201    let photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
202    if (param.getMode() === FileSelectorMode.FileOpenMode) {
203      console.log('allow select single photo or video');
204      photoSelectOptions.maxSelectNumber = 1;
205    }
206    let acceptTypes = param.getAcceptType();
207    photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE;
208    if (isContainImageMimeType(acceptTypes) && !isContainVideoMimeType(acceptTypes)) {
209      photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
210    }
211    if (!isContainImageMimeType(acceptTypes) && isContainVideoMimeType(acceptTypes)) {
212      photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE;
213    }
214
215    let photoPicker = new photoAccessHelper.PhotoViewPicker();
216    photoPicker.select(photoSelectOptions).then((photoSelectResult) => {
217      if (photoSelectResult.photoUris.length <= 0) {
218        return;
219      }
220      for (let i = 0; i < photoSelectResult.photoUris.length; i++) {
221        photoResultArray.push(photoSelectResult.photoUris[i]);
222      }
223      selectResult.handleFileList(photoResultArray);
224    });
225  } catch (error) {
226    console.log('selectPicture error' + JSON.stringify(error));
227  }
228}
229
230Object.defineProperty(webview.WebviewController.prototype, 'getCertificate', {
231  value: function (callback) {
232    if (arguments.length !== 0 && arguments.length !== 1) {
233      throw new BusinessError(PARAM_CHECK_ERROR,
234        'BusinessError 401: Parameter error. The number of params must be zero or one.');
235    }
236
237    let certChainData = this.innerGetCertificate();
238    if (callback === undefined) {
239      console.log('get certificate promise');
240      return getCertificatePromise(certChainData);
241    } else {
242      console.log('get certificate async callback');
243      if (typeof callback !== 'function') {
244        throw new BusinessError(PARAM_CHECK_ERROR,
245          'BusinessError 401: Parameter error. The type of "callback" must be function.' );
246      }
247      return getCertificatePromise(certChainData).then(x509CertArray => {
248        callback(undefined, x509CertArray);
249      }).catch(error => {
250        callback(error, undefined);
251      });
252    }
253  }
254});
255
256Object.defineProperty(webview.WebviewController.prototype, 'fileSelectorShowFromUserWeb', {
257  value:  function (callback) {
258    let currentDevice = deviceinfo.deviceType.toLowerCase();
259    if (needShowDialog(callback.fileparam)) {
260      ActionSheet.show({
261        title: '选择上传',
262        autoCancel: true,
263        confirm: {
264          defaultFocus: true,
265          value: '取消',
266          style: DialogButtonStyle.DEFAULT,
267          action: () => {
268            console.log('Get Alert Dialog handled');
269          }
270        },
271        cancel: () => {
272          console.log('actionSheet canceled');
273        },
274        alignment: DialogAlignment.Bottom,
275        offset: { dx: 0, dy: -10 },
276        sheets: [
277          {
278            icon: $r('sys.media.ohos_ic_public_albums'),
279            title: '图片',
280            action: () => {
281              selectPicture(callback.fileparam, callback.fileresult);
282            }
283          },
284          {
285            icon: $r('sys.media.ohos_ic_public_camera'),
286            title: '拍照',
287            action: () => {
288              takePhoto(callback.fileparam, callback.fileresult);
289             }
290          },
291          {
292            icon: $r('sys.media.ohos_ic_public_email'),
293            title: '文件',
294            action: () => {
295              selectFile(callback.fileparam, callback.fileresult);
296            }
297          }
298        ]
299      });
300    } else if (currentDevice === 'phone' && callback.fileparam.isCapture()) {
301      console.log('take photo will be directly invoked due to the capture property');
302      takePhoto(callback.fileparam, callback.fileresult);
303    } else {
304      console.log('selectFile will be invoked by web');
305      selectFile(callback.fileparam, callback.fileresult);
306    }
307  }
308});
309
310Object.defineProperty(webview.WebviewController.prototype, 'requestPermissionsFromUserWeb', {
311  value:  function (callback) {
312    let accessManger = accessControl.createAtManager();
313    let abilityContext = getContext(this);
314    accessManger.requestPermissionsFromUser(abilityContext, ['ohos.permission.READ_PASTEBOARD'])
315      .then((PermissionRequestResult) => {
316        if (PermissionRequestResult.authResults[0] === 0) {
317          console.log('requestPermissionsFromUserWeb is allowed');
318          callback.request.grant(callback.request.getAccessibleResource());
319        }
320        else {
321          console.log('requestPermissionsFromUserWeb is refused');
322          callback.request.deny();
323        }
324      })
325      .catch((error) => {
326        callback.request.deny();
327      });
328  }
329});
330
331Object.defineProperty(webview.WebviewController.prototype, 'openAppLink', {
332  value:  function (callback) {
333    let abilityContext = getContext(this);
334    try {
335      let option = {
336        appLinkingOnly: true
337      };
338      console.log('begin openAppLink');
339      abilityContext.openLink(callback.url, option, null)
340        .then(() => {
341          console.log('applink success openLink');
342          callback.result.cancelLoad();
343        })
344        .catch((error) => {
345          console.log(`applink openLink ErrorCode: ${error.code},  Message: ${error.message}`);
346          callback.result.continueLoad();
347        });
348    } catch (err) {
349      console.log(`applink openLink ErrorCode: ${err.code},  Message: ${err.message}`);
350      callback.result.continueLoad();
351    }
352  }
353});
354
355export default webview;
356