diff --git a/package-lock.json b/package-lock.json index 0ef732cd5..6f81cf9a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10527,6 +10527,11 @@ "dev": true, "optional": true }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, "import-cwd": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", @@ -11253,8 +11258,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -12559,6 +12563,17 @@ } } }, + "jszip": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.5.0.tgz", + "integrity": "sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + } + }, "just-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", @@ -12741,6 +12756,14 @@ "webpack-sources": "^1.2.0" } }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, "liftoff": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", @@ -14745,8 +14768,7 @@ "pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, "parallel-transform": { "version": "1.2.0", @@ -15936,8 +15958,7 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "progress": { "version": "2.0.3", @@ -16301,7 +16322,6 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -17465,6 +17485,11 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, "set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -18381,7 +18406,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -19543,8 +19567,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util-promisify": { "version": "2.1.0", diff --git a/package.json b/package.json index c77787bb4..c16199081 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "cordova-support-google-services": "^1.2.1", "cordova.plugins.diagnostic": "^6.0.2", "es6-promise-plugin": "^4.2.2", + "jszip": "^3.5.0", "moment": "^2.29.0", "nl.kingsquare.cordova.background-audio": "^1.0.1", "phonegap-plugin-multidex": "^1.0.0", diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index 53d44a614..185e30155 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -17,6 +17,8 @@ import { CommonModule } from '@angular/common'; import { IonicModule } from '@ionic/angular'; import { TranslateModule } from '@ngx-translate/core'; +import { CoreDownloadRefreshComponent } from './download-refresh/download-refresh'; +import { CoreFileComponent } from './file/file'; import { CoreIconComponent } from './icon/icon'; import { CoreIframeComponent } from './iframe/iframe'; import { CoreInputErrorsComponent } from './input-errors/input-errors'; @@ -31,6 +33,8 @@ import { CorePipesModule } from '@app/pipes/pipes.module'; @NgModule({ declarations: [ + CoreDownloadRefreshComponent, + CoreFileComponent, CoreIconComponent, CoreIframeComponent, CoreInputErrorsComponent, @@ -49,6 +53,8 @@ import { CorePipesModule } from '@app/pipes/pipes.module'; CorePipesModule, ], exports: [ + CoreDownloadRefreshComponent, + CoreFileComponent, CoreIconComponent, CoreIframeComponent, CoreInputErrorsComponent, diff --git a/src/app/components/download-refresh/core-download-refresh.html b/src/app/components/download-refresh/core-download-refresh.html new file mode 100644 index 000000000..633654ff2 --- /dev/null +++ b/src/app/components/download-refresh/core-download-refresh.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/components/download-refresh/download-refresh.scss b/src/app/components/download-refresh/download-refresh.scss new file mode 100644 index 000000000..b03f953f8 --- /dev/null +++ b/src/app/components/download-refresh/download-refresh.scss @@ -0,0 +1,8 @@ +:host { + font-size: 1.4rem; + display: flex; + flex-flow: row; + align-items: center; + justify-content: space-around; + align-content: center; +} diff --git a/src/app/components/download-refresh/download-refresh.ts b/src/app/components/download-refresh/download-refresh.ts new file mode 100644 index 000000000..ad7b9d7c9 --- /dev/null +++ b/src/app/components/download-refresh/download-refresh.ts @@ -0,0 +1,59 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { CoreConstants } from '@core/constants'; + +/** + * Component to show a download button with refresh option, the spinner and the status of it. + * + * Usage: + * + */ +@Component({ + selector: 'core-download-refresh', + templateUrl: 'core-download-refresh.html', + styleUrls: ['download-refresh.scss'], +}) +export class CoreDownloadRefreshComponent { + + @Input() status?: string; // Download status. + @Input() enabled = false; // Whether the download is enabled. + @Input() loading = true; // Force loading status when is not downloading. + @Input() canTrustDownload = false; // If false, refresh will be shown if downloaded. + @Output() action: EventEmitter; // Will emit an event when the item clicked. + + statusDownloaded = CoreConstants.DOWNLOADED; + statusNotDownloaded = CoreConstants.NOT_DOWNLOADED; + statusOutdated = CoreConstants.OUTDATED; + statusDownloading = CoreConstants.DOWNLOADING; + + constructor() { + this.action = new EventEmitter(); + } + + /** + * Download clicked. + * + * @param e Click event. + * @param refresh Whether it's refreshing. + */ + download(e: Event, refresh: boolean): void { + e.preventDefault(); + e.stopPropagation(); + + this.action.emit(refresh); + } + +} diff --git a/src/app/components/file/core-file.html b/src/app/components/file/core-file.html new file mode 100644 index 000000000..03401dbda --- /dev/null +++ b/src/app/components/file/core-file.html @@ -0,0 +1,20 @@ + + + + + +

{{fileName}}

+

{{ fileSizeReadable }}

+

{{ timemodified * 1000 | coreFormatDate }}

+
+
+ + + + + + +
+
diff --git a/src/app/components/file/file.scss b/src/app/components/file/file.scss new file mode 100644 index 000000000..98ce8714a --- /dev/null +++ b/src/app/components/file/file.scss @@ -0,0 +1,31 @@ +:host { + // @todo + // .card-md core-file + core-file > .item-md.item-block > .item-inner, + // core-file + core-file > .item-md.item-block > .item-inner { + // border-top: 1px solid $list-md-border-color; + // } + + // .card-ios core-file + core-file > .item-ios.item-block > .item-inner, + // core-file + core-file > .item-ios.item-block > .item-inner { + // border-top: $hairlines-width solid $list-ios-border-color; + // .buttons { + // min-height: 53px; + // min-width: 58px; + // } + // } + + // core-file > .item.item-block > .item-inner { + // border-bottom: 0; + // @include safe-area-padding(null, 0px, null, null); + // .buttons { + // display: flex; + // flex-flow: row; + // align-items: center; + // z-index: 1; + // justify-content: space-around; + // align-content: center; + // min-height: 52px; + // min-width: 53px; + // } + // } +} \ No newline at end of file diff --git a/src/app/components/file/file.ts b/src/app/components/file/file.ts new file mode 100644 index 000000000..fcdea964e --- /dev/null +++ b/src/app/components/file/file.ts @@ -0,0 +1,253 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, Input, Output, OnInit, OnDestroy, EventEmitter } from '@angular/core'; +import { CoreApp } from '@services/app'; +import { CoreFilepool } from '@services/filepool'; +import { CoreFileHelper } from '@services/file-helper'; +import { CorePluginFileDelegate } from '@services/plugin-file-delegate'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreMimetypeUtils } from '@services/utils/mimetype'; +import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreConstants } from '@core/constants'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreWSExternalFile } from '@/app/services/ws'; + +/** + * Component to handle a remote file. Shows the file name, icon (depending on mimetype) and a button + * to download/refresh it. + */ +@Component({ + selector: 'core-file', + templateUrl: 'core-file.html', + styleUrls: ['file.scss'], +}) +export class CoreFileComponent implements OnInit, OnDestroy { + + @Input() file?: CoreWSExternalFile; // The file. + @Input() component?: string; // Component the file belongs to. + @Input() componentId?: string | number; // Component ID. + @Input() canDelete?: boolean | string; // Whether file can be deleted. + @Input() alwaysDownload?: boolean | string; // Whether it should always display the refresh button when the file is downloaded. + @Input() canDownload?: boolean | string = true; // Whether file can be downloaded. + @Input() showSize?: boolean | string = true; // Whether show filesize. + @Input() showTime?: boolean | string = true; // Whether show file time modified. + @Output() onDelete: EventEmitter; // Will notify when the delete button is clicked. + + isDownloading?: boolean; + fileIcon?: string; + fileName!: string; + fileSizeReadable?: string; + state?: string; + timemodified!: number; + + protected fileUrl!: string; + protected siteId?: string; + protected fileSize?: number; + protected observer?: CoreEventObserver; + + constructor( + protected pluginFileDelegate: CorePluginFileDelegate, + ) { + this.onDelete = new EventEmitter(); + } + + /** + * Component being initialized. + */ + async ngOnInit(): Promise { + if (!this.file) { + return; + } + + this.canDelete = CoreUtils.instance.isTrueOrOne(this.canDelete); + this.alwaysDownload = CoreUtils.instance.isTrueOrOne(this.alwaysDownload); + this.canDownload = CoreUtils.instance.isTrueOrOne(this.canDownload); + + this.fileUrl = this.file.fileurl; + this.timemodified = this.file.timemodified || 0; + this.siteId = CoreSites.instance.getCurrentSiteId(); + this.fileSize = this.file.filesize; + this.fileName = this.file.filename || ''; + + if (CoreUtils.instance.isTrueOrOne(this.showSize) && this.fileSize && this.fileSize >= 0) { + this.fileSizeReadable = CoreTextUtils.instance.bytesToSize(this.fileSize, 2); + } + + this.showTime = CoreUtils.instance.isTrueOrOne(this.showTime) && this.timemodified > 0; + + if (this.file.isexternalfile) { + this.alwaysDownload = true; // Always show the download button in external files. + } + + this.fileIcon = this.file.mimetype ? CoreMimetypeUtils.instance.getMimetypeIcon(this.file.mimetype) : + CoreMimetypeUtils.instance.getFileIcon(this.fileName); + + if (this.canDownload) { + this.calculateState(); + + try { + // Update state when receiving events about this file. + const eventName = await CoreFilepool.instance.getFileEventNameByUrl(this.siteId, this.fileUrl); + + this.observer = CoreEvents.on(eventName, () => { + this.calculateState(); + }); + } catch (error) { + // File not downloadable. + } + } + } + + /** + * Convenience function to get the file state and set variables based on it. + * + * @return Promise resolved when state has been calculated. + */ + protected async calculateState(): Promise { + if (!this.siteId) { + return; + } + + const state = await CoreFilepool.instance.getFileStateByUrl(this.siteId, this.fileUrl, this.timemodified); + + const site = await CoreSites.instance.getSite(this.siteId); + + this.canDownload = site.canDownloadFiles(); + + this.state = state; + this.isDownloading = this.canDownload && state === CoreConstants.DOWNLOADING; + } + + /** + * Convenience function to open a file, downloading it if needed. + * + * @return Promise resolved when file is opened. + */ + protected openFile(): Promise { + return CoreFileHelper.instance.downloadAndOpenFile(this.file!, this.component, this.componentId, this.state, (event) => { + if (event && 'calculating' in event && event.calculating) { + // The process is calculating some data required for the download, show the spinner. + this.isDownloading = true; + } + }).catch((error) => { + CoreDomUtils.instance.showErrorModalDefault(error, 'core.errordownloading', true); + }); + } + + /** + * Download a file and, optionally, open it afterwards. + * + * @param e Click event. + * @param openAfterDownload Whether the file should be opened after download. + */ + async download(e?: Event, openAfterDownload: boolean = false): Promise { + e && e.preventDefault(); + e && e.stopPropagation(); + + if (!this.file || !this.siteId) { + return; + } + + if (this.isDownloading && !openAfterDownload) { + return; + } + + if (!this.canDownload || !this.state || this.state == CoreConstants.NOT_DOWNLOADABLE) { + // File cannot be downloaded, just open it. + if (CoreUrlUtils.instance.isLocalFileUrl(this.fileUrl)) { + CoreUtils.instance.openFile(this.fileUrl); + } else { + CoreUtils.instance.openOnlineFile(CoreUrlUtils.instance.unfixPluginfileURL(this.fileUrl)); + } + + return; + } + + if (!CoreApp.instance.isOnline() && (!openAfterDownload || (openAfterDownload && + !CoreFileHelper.instance.isStateDownloaded(this.state)))) { + CoreDomUtils.instance.showErrorModal('core.networkerrormsg', true); + + return; + } + + if (openAfterDownload) { + // File needs to be opened now. + try { + await this.openFile(); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'core.errordownloading', true); + } + } else { + try { + // File doesn't need to be opened (it's a prefetch). Show confirm modal if file size is defined and it's big. + const size = await this.pluginFileDelegate.getFileSize(this.file, this.siteId); + + if (size) { + await CoreDomUtils.instance.confirmDownloadSize({ size: size, total: true }); + } + + // User confirmed, add the file to queue. + // @todo: Is the invalidate really needed? + await CoreUtils.instance.ignoreErrors(CoreFilepool.instance.invalidateFileByUrl(this.siteId, this.fileUrl)); + + this.isDownloading = true; + + try { + await CoreFilepool.instance.addToQueueByUrl( + this.siteId, + this.fileUrl, + this.component, + this.componentId, + this.timemodified, + undefined, + undefined, + 0, + this.file, + ); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'core.errordownloading', true); + this.calculateState(); + } + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'core.errordownloading', true); + } + } + } + + /** + * Delete the file. + * + * @param e Click event. + */ + delete(e: Event): void { + e.preventDefault(); + e.stopPropagation(); + + if (this.canDelete) { + this.onDelete.emit(); + } + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.observer?.off(); + } + +} diff --git a/src/app/core/emulator/emulator.module.ts b/src/app/core/emulator/emulator.module.ts index dc81075e1..173d6beeb 100644 --- a/src/app/core/emulator/emulator.module.ts +++ b/src/app/core/emulator/emulator.module.ts @@ -13,6 +13,10 @@ // limitations under the License. import { NgModule } from '@angular/core'; +import { Platform } from '@ionic/angular'; + +import { CoreInitDelegate } from '@services/init'; +import { CoreEmulatorHelperProvider } from './services/helper'; // Ionic Native services. import { Clipboard } from '@ionic-native/clipboard/ngx'; @@ -36,6 +40,16 @@ import { StatusBar } from '@ionic-native/status-bar/ngx'; import { WebIntent } from '@ionic-native/web-intent/ngx'; import { Zip } from '@ionic-native/zip/ngx'; +// Mock services. +import { ClipboardMock } from './services/clipboard'; +import { FileMock } from './services/file'; +import { FileOpenerMock } from './services/file-opener'; +import { FileTransferMock } from './services/file-transfer'; +import { GeolocationMock } from './services/geolocation'; +import { InAppBrowserMock } from './services/inappbrowser'; +import { NetworkMock } from './services/network'; +import { ZipMock } from './services/zip'; + /** * This module handles the emulation of Cordova plugins in browser and desktop. * @@ -51,18 +65,47 @@ import { Zip } from '@ionic-native/zip/ngx'; imports: [ ], providers: [ - Clipboard, + CoreEmulatorHelperProvider, + { + provide: Clipboard, + deps: [Platform], // Use platform instead of AppProvider to prevent errors with singleton injection. + useFactory: (platform: Platform): Clipboard => platform.is('cordova') ? new Clipboard() : new ClipboardMock(), + }, Device, Diagnostic, - File, - FileOpener, - FileTransfer, - Geolocation, + { + provide: File, + deps: [Platform], + useFactory: (platform: Platform): File => platform.is('cordova') ? new File() : new FileMock(), + }, + { + provide: FileOpener, + deps: [Platform], + useFactory: (platform: Platform): FileOpener => platform.is('cordova') ? new FileOpener() : new FileOpenerMock(), + }, + { + provide: FileTransfer, + deps: [Platform], + useFactory: (platform: Platform): FileTransfer => platform.is('cordova') ? new FileTransfer() : new FileTransferMock(), + }, + { + provide: Geolocation, + deps: [Platform], + useFactory: (platform: Platform): Geolocation => platform.is('cordova') ? new Geolocation() : new GeolocationMock(), + }, HTTP, - InAppBrowser, + { + provide: InAppBrowser, + deps: [Platform], + useFactory: (platform: Platform): InAppBrowser => platform.is('cordova') ? new InAppBrowser() : new InAppBrowserMock(), + }, Keyboard, LocalNotifications, - Network, + { + provide: Network, + deps: [Platform], + useFactory: (platform: Platform): Network => platform.is('cordova') ? new Network() : new NetworkMock(), + }, Push, QRScanner, SplashScreen, @@ -70,7 +113,25 @@ import { Zip } from '@ionic-native/zip/ngx'; StatusBar, WebIntent, WebView, - Zip, + { + provide: Zip, + deps: [Platform, File], + useFactory: (platform: Platform, file: File): Zip => platform.is('cordova') ? new Zip() : new ZipMock(file), + }, ], }) -export class CoreEmulatorModule { } +export class CoreEmulatorModule { + + constructor( + platform: Platform, + initDelegate: CoreInitDelegate, + helper: CoreEmulatorHelperProvider, + ) { + + if (!platform.is('cordova')) { + // Register an init process to load the Mocks that need it. + initDelegate.registerProcess(helper); + } + } + +} diff --git a/src/app/core/emulator/services/clipboard.ts b/src/app/core/emulator/services/clipboard.ts new file mode 100644 index 000000000..2449a59d1 --- /dev/null +++ b/src/app/core/emulator/services/clipboard.ts @@ -0,0 +1,87 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { Clipboard } from '@ionic-native/clipboard/ngx'; + +/** + * Emulates the Cordova Clipboard plugin in browser. + */ +@Injectable() +export class ClipboardMock extends Clipboard { + + protected copyTextarea: HTMLTextAreaElement; + + constructor() { + super(); + + // In browser the text must be selected in order to copy it. Create a hidden textarea to put the text in it. + this.copyTextarea = document.createElement('textarea'); + this.copyTextarea.className = 'core-browser-copy-area'; + this.copyTextarea.setAttribute('aria-hidden', 'true'); + document.body.appendChild(this.copyTextarea); + } + + /** + * Copy some text to the clipboard. + * + * @param text The text to copy. + * @return Promise resolved when copied. + */ + copy(text: string): Promise { + return new Promise((resolve, reject): void => { + // Put the text in the hidden textarea and select it. + this.copyTextarea.innerHTML = text; + this.copyTextarea.select(); + + try { + if (document.execCommand('copy')) { + resolve(); + } else { + reject(); + } + } catch (err) { + reject(); + } + + this.copyTextarea.innerHTML = ''; + }); + } + + /* + * Get the text stored in the clipboard. + * + * @return Promise resolved with the text. + */ + paste(): Promise { + return new Promise((resolve, reject): void => { + // Paste the text in the hidden textarea and get it. + this.copyTextarea.innerHTML = ''; + this.copyTextarea.select(); + + try { + if (document.execCommand('paste')) { + resolve(this.copyTextarea.innerHTML); + } else { + reject(); + } + } catch (err) { + reject(); + } + + this.copyTextarea.innerHTML = ''; + }); + } + +} diff --git a/src/app/core/emulator/services/file-opener.ts b/src/app/core/emulator/services/file-opener.ts new file mode 100644 index 000000000..94d5397f3 --- /dev/null +++ b/src/app/core/emulator/services/file-opener.ts @@ -0,0 +1,75 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { FileOpener } from '@ionic-native/file-opener/ngx'; + +import { CoreFile } from '@services/file'; + +/** + * Emulates the FileOpener plugin in browser. + */ +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any */ +@Injectable() +export class FileOpenerMock extends FileOpener { + + /** + * Check if an app is already installed. + * + * @param packageId Package ID. + * @return Promise resolved when done. + */ + appIsInstalled(packageId: string): Promise { + return Promise.reject('appIsInstalled not supported in browser.'); + } + + /** + * Open an file. + * + * @param filePath File path. + * @param fileMIMEType File MIME type. + * @return Promise resolved when done. + */ + async open(filePath: string, fileMIMEType: string): Promise { + if (!filePath.match(/^filesystem:/)) { + // Just open the page. + window.open(filePath, '_blank'); + + return; + } + + try { + // Opening local files in browser just display a blank page. Convert the path to an object URL. + const fileEntry = await CoreFile.instance.getExternalFile(filePath); + + const file = await CoreFile.instance.getFileObjectFromFileEntry(fileEntry); + + window.open(window.URL.createObjectURL(file), '_blank'); + } catch (error) { + // File not found. Just open the URL even if it ends up being a blank page. + window.open(filePath, '_blank'); + } + } + + /** + * Uninstalls a package. + * + * @param packageId Package ID. + * @return Promise resolved when done. + */ + uninstall(packageId: string): Promise { + return Promise.reject('uninstall not supported in browser.'); + } + +} diff --git a/src/app/core/emulator/services/file-transfer.ts b/src/app/core/emulator/services/file-transfer.ts new file mode 100644 index 000000000..13b6b119d --- /dev/null +++ b/src/app/core/emulator/services/file-transfer.ts @@ -0,0 +1,379 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CoreTextUtils } from '@/app/services/utils/text'; +import { Injectable } from '@angular/core'; +import { FileTransfer, FileTransferObject, FileUploadResult, FileTransferError } from '@ionic-native/file-transfer/ngx'; + +import { CoreFile } from '@services/file'; + +/** + * Mock the File Transfer Error. + */ +export class FileTransferErrorMock implements FileTransferError { + + static readonly FILE_NOT_FOUND_ERR = 1; + static readonly INVALID_URL_ERR = 2; + static readonly CONNECTION_ERR = 3; + static readonly ABORT_ERR = 4; + static readonly NOT_MODIFIED_ERR = 5; + + constructor( + public code: number, + public source: string, + public target: string, + public http_status: number, + public body: string, + public exception: string, + ) { } + +} + +/** + * Emulates the Cordova FileTransfer plugin in desktop apps and in browser. + */ +@Injectable() +export class FileTransferMock extends FileTransfer { + + /** + * Creates a new FileTransferObjectMock object. + */ + create(): FileTransferObjectMock { + return new FileTransferObjectMock(); + } + +} + +/** + * Emulates the FileTransferObject class in desktop apps and in browser. + */ +export class FileTransferObjectMock extends FileTransferObject { + + progressListener?: (event: ProgressEvent) => void; + source?: string; + target?: string; + xhr?: XMLHttpRequest; + + protected reject?: (reason?: unknown) => void; + + /** + * Aborts an in-progress transfer. The onerror callback is passed a FileTransferError + * object which has an error code of FileTransferError.ABORT_ERR. + */ + abort(): void { + if (this.xhr) { + this.xhr.abort(); + this.reject!(new FileTransferErrorMock(FileTransferErrorMock.ABORT_ERR, this.source!, this.target!, 0, '', '')); + } + } + + /** + * Downloads a file from server. + * + * @param source URL of the server to download the file, as encoded by encodeURI(). + * @param target Filesystem url representing the file on the device. + * @param trustAllHosts If set to true, it accepts all security certificates. + * @param options Optional parameters, currently only supports headers. + * @return Returns a Promise that resolves to a FileEntry object. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + download(source: string, target: string, trustAllHosts?: boolean, options?: { [s: string]: any }): Promise { + return new Promise((resolve, reject): void => { + // Use XMLHttpRequest instead of HttpClient to support onprogress and abort. + const basicAuthHeader = this.getBasicAuthHeader(source); + const xhr = new XMLHttpRequest(); + + this.xhr = xhr; + this.source = source; + this.target = target; + this.reject = reject; + + if (basicAuthHeader) { + source = source.replace(this.getUrlCredentials(source) + '@', ''); + + options = options || {}; + options.headers = options.headers || {}; + options.headers[basicAuthHeader.name] = basicAuthHeader.value; + } + + const headers = options?.headers || null; + + // Prepare the request. + xhr.open('GET', source, true); + xhr.responseType = 'blob'; + for (const name in headers) { + xhr.setRequestHeader(name, headers[name]); + } + + xhr.onprogress = (ev: ProgressEvent): void => { + if (this.progressListener) { + this.progressListener(ev); + } + }; + + xhr.onerror = (): void => { + reject(new FileTransferErrorMock(-1, source, target, xhr.status, xhr.statusText, '')); + }; + + xhr.onload = async (): Promise => { + // Finished dowloading the file. + let response = xhr.response || xhr.responseText; + + const status = Math.max(xhr.status === 1223 ? 204 : xhr.status, 0); + if (status < 200 || status >= 300) { + // Request failed. Try to get the error message. + response = await this.parseResponse(response); + + reject(new FileTransferErrorMock(-1, source, target, xhr.status, response || xhr.statusText, '')); + + return; + } + + if (!response) { + reject(); + + return; + } + + const basePath = CoreFile.instance.getBasePathInstant(); + target = target.replace(basePath, ''); // Remove basePath from the target. + target = target.replace(/%20/g, ' '); // Replace all %20 with spaces. + + // eslint-disable-next-line promise/catch-or-return + CoreFile.instance.writeFile(target, response).then(resolve, reject); + }; + + xhr.send(); + }); + } + + /** + * Given a URL, check if it has a credentials in it and, if so, return them in a header object. + * This code is extracted from Cordova FileTransfer plugin. + * + * @param urlString The URL to get the credentials from. + * @return The header with the credentials, null if no credentials. + */ + protected getBasicAuthHeader(urlString: string): {name: string; value: string} | null { + let header: {name: string; value: string} | null = null; + + // MS Windows doesn't support credentials in http uris so we detect them by regexp and strip off from result url. + if (window.btoa) { + const credentials = this.getUrlCredentials(urlString); + if (credentials) { + header = { + name: 'Authorization', + value: 'Basic ' + window.btoa(credentials), + }; + } + } + + return header; + } + + /** + * Given an instance of XMLHttpRequest, get the response headers as an object. + * + * @param xhr XMLHttpRequest instance. + * @return Object with the headers. + */ + protected getHeadersAsObject(xhr: XMLHttpRequest): Record { + const headersString = xhr.getAllResponseHeaders(); + const result = {}; + + if (headersString) { + const headers = headersString.split('\n'); + for (const i in headers) { + const headerString = headers[i]; + const separatorPos = headerString.indexOf(':'); + if (separatorPos != -1) { + result[headerString.substr(0, separatorPos)] = headerString.substr(separatorPos + 1).trim(); + } + } + } + + return result; + } + + /** + * Get the credentials from a URL. + * This code is extracted from Cordova FileTransfer plugin. + * + * @param urlString The URL to get the credentials from. + * @return Retrieved credentials. + */ + protected getUrlCredentials(urlString: string): string | null { + const credentialsPattern = /^https?:\/\/(?:(?:(([^:@/]*)(?::([^@/]*))?)?@)?([^:/?#]*)(?::(\d*))?).*$/; + const credentials = credentialsPattern.exec(urlString); + + return credentials && credentials[1]; + } + + /** + * Registers a listener that gets called whenever a new chunk of data is transferred. + * + * @param listener Listener that takes a progress event. + */ + onProgress(listener: (event: ProgressEvent) => void): void { + this.progressListener = listener; + } + + /** + * Parse a response, converting it into text and the into an object if needed. + * + * @param response The response to parse. + * @return Promise resolved with the parsed response. + */ + protected async parseResponse(response: Blob | ArrayBuffer | string | null): Promise { + if (!response) { + return ''; + + } + + let responseText = ''; + + if (response instanceof Blob) { + responseText = await this.blobToText(response); + + } else if (response instanceof ArrayBuffer) { + // Convert the ArrayBuffer into text. + responseText = String.fromCharCode.apply(null, new Uint8Array(response)); + + } else { + responseText = response; + } + + return CoreTextUtils.instance.parseJSON(responseText, ''); + } + + /** + * Convert a Blob to text. + * + * @param blob Blob to convert. + * @return Promise resolved with blob contents. + */ + protected blobToText(blob: Blob): Promise { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onloadend = (): void => { + resolve( reader.result); + }; + reader.readAsText(blob); + }); + } + + /** + * Sends a file to a server. + * + * @param fileUrl Filesystem URL representing the file on the device or a data URI. + * @param url URL of the server to receive the file, as encoded by encodeURI(). + * @param options Optional parameters. + * @return Promise that resolves to a FileUploadResult and rejects with FileTransferError. + */ + upload(fileUrl: string, url: string, options?: FileUploadOptions): Promise { + return new Promise((resolve, reject): void => { + const basicAuthHeader = this.getBasicAuthHeader(url); + let fileKey: string | undefined; + let fileName: string | undefined; + let params: any; // eslint-disable-line @typescript-eslint/no-explicit-any + let headers: any; // eslint-disable-line @typescript-eslint/no-explicit-any + let httpMethod: string | undefined; + + if (basicAuthHeader) { + url = url.replace(this.getUrlCredentials(url) + '@', ''); + + options = options || {}; + options.headers = options.headers || {}; + options.headers[basicAuthHeader.name] = basicAuthHeader.value; + } + + if (options) { + fileKey = options.fileKey; + fileName = options.fileName; + headers = options.headers; + httpMethod = options.httpMethod || 'POST'; + + if (httpMethod.toUpperCase() == 'PUT') { + httpMethod = 'PUT'; + } else { + httpMethod = 'POST'; + } + + params = options.params || {}; + } + + // Add fileKey and fileName to the headers. + headers = headers || {}; + if (!headers['Content-Disposition']) { + headers['Content-Disposition'] = 'form-data;' + (fileKey ? ' name="' + fileKey + '";' : '') + + (fileName ? ' filename="' + fileName + '"' : ''); + } + + // Adding a Content-Type header with the mimeType makes the request fail (it doesn't detect the token in the params). + // Don't include this header, and delete it if it's supplied. + delete headers['Content-Type']; + + // Get the file to upload. + CoreFile.instance.getFile(fileUrl).then((fileEntry) => + CoreFile.instance.getFileObjectFromFileEntry(fileEntry)).then((file) => { + // Use XMLHttpRequest instead of HttpClient to support onprogress and abort. + const xhr = new XMLHttpRequest(); + xhr.open(httpMethod || 'POST', url); + for (const name in headers) { + // Filter "unsafe" headers. + if (name != 'Connection') { + xhr.setRequestHeader(name, headers[name]); + } + } + + xhr.onprogress = (ev: ProgressEvent): void => { + if (this.progressListener) { + this.progressListener(ev); + } + }; + + this.xhr = xhr; + this.source = fileUrl; + this.target = url; + this.reject = reject; + + xhr.onerror = (): void => { + reject(new FileTransferErrorMock(-1, fileUrl, url, xhr.status, xhr.statusText, '')); + }; + + xhr.onload = (): void => { + // Finished uploading the file. + resolve({ + bytesSent: file.size, + responseCode: xhr.status, + response: xhr.response, + headers: this.getHeadersAsObject(xhr), + }); + }; + + // Create a form data to send params and the file. + const fd = new FormData(); + for (const name in params) { + fd.append(name, params[name]); + } + fd.append('file', file, fileName); + + xhr.send(fd); + + return; + }).catch(reject); + }); + } + +} diff --git a/src/app/core/emulator/services/file.ts b/src/app/core/emulator/services/file.ts new file mode 100644 index 000000000..1d8356936 --- /dev/null +++ b/src/app/core/emulator/services/file.ts @@ -0,0 +1,797 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { File, Entry, DirectoryEntry, FileEntry, IWriteOptions, RemoveResult } from '@ionic-native/file/ngx'; + +import { CoreTextUtils } from '@services/utils/text'; + +/** + * Implement the File Error because the ionic-native plugin doesn't implement it. + */ +class FileError { + + static readonly NOT_FOUND_ERR = 1; + static readonly SECURITY_ERR = 2; + static readonly ABORT_ERR = 3; + static readonly NOT_READABLE_ERR = 4; + static readonly ENCODING_ERR = 5; + static readonly NO_MODIFICATION_ALLOWED_ERR = 6; + static readonly INVALID_STATE_ERR = 7; + static readonly SYNTAX_ERR = 8; + static readonly INVALID_MODIFICATION_ERR = 9; + static readonly QUOTA_EXCEEDED_ERR = 10; + static readonly TYPE_MISMATCH_ERR = 11; + static readonly PATH_EXISTS_ERR = 12; + + message?: string; + + constructor( + public code: number, + ) { } + +} + +/** + * Emulates the Cordova File plugin in browser. + * Most of the code is extracted from the File class of Ionic Native. + */ +@Injectable() +export class FileMock extends File { + + /** + * Check if a directory exists in a certain path, directory. + * + * @param path Base FileSystem. + * @param dir Name of directory to check + * @return Returns a Promise that resolves to true if the directory exists or rejects with an error. + */ + async checkDir(path: string, dir: string): Promise { + const fullPath = CoreTextUtils.instance.concatenatePaths(path, dir); + + await this.resolveDirectoryUrl(fullPath); + + return true; + } + + /** + * Check if a file exists in a certain path, directory. + * + * @param path Base FileSystem. + * @param file Name of file to check. + * @return Returns a Promise that resolves with a boolean or rejects with an error. + */ + async checkFile(path: string, file: string): Promise { + const entry = await this.resolveLocalFilesystemUrl(CoreTextUtils.instance.concatenatePaths(path, file)); + + if (entry.isFile) { + return true; + } else { + const error = new FileError(13); + error.message = 'input is not a file'; + + throw error; + } + } + + /** + * Copy a file or directory. + * + * @param srce The Entry to copy. + * @param destDir The directory where to put the copy. + * @param newName New name of the file/dir. + * @return Returns a Promise that resolves to the new Entry object or rejects with an error. + */ + private copyMock(srce: Entry, destDir: DirectoryEntry, newName: string): Promise { + return new Promise((resolve, reject): void => { + newName = newName.replace(/%20/g, ' '); // Replace all %20 with spaces. + + srce.copyTo(destDir, newName, (deste) => { + resolve(deste); + }, (err) => { + this.fillErrorMessageMock(err); + reject(err); + }); + }); + } + + /** + * Copy a directory in various methods. If destination directory exists, will fail to copy. + * + * @param path Base FileSystem. Please refer to the iOS and Android filesystems above. + * @param dirName Name of directory to copy. + * @param newPath Base FileSystem of new location. + * @param newDirName New name of directory to copy to (leave blank to remain the same). + * @return Returns a Promise that resolves to the new Entry object or rejects with an error. + */ + copyDir(path: string, dirName: string, newPath: string, newDirName: string): Promise { + return this.copyFileOrDir(path, dirName, newPath, newDirName); + } + + /** + * Copy a file in various methods. If file exists, will fail to copy. + * + * @param path Base FileSystem. Please refer to the iOS and Android filesystems above + * @param fileName Name of file to copy + * @param newPath Base FileSystem of new location + * @param newFileName New name of file to copy to (leave blank to remain the same) + * @return Returns a Promise that resolves to an Entry or rejects with an error. + */ + copyFile(path: string, fileName: string, newPath: string, newFileName: string): Promise { + return this.copyFileOrDir(path, fileName, newPath, newFileName || fileName); + } + + /** + * Copy a file or dir to a given path. + * + * @param sourcePath Path of the file/dir to copy. + * @param sourceName Name of file/dir to copy + * @param destPath Path where to copy. + * @param destName New name of file/dir. + * @return Returns a Promise that resolves to the new Entry or rejects with an error. + */ + async copyFileOrDir(sourcePath: string, sourceName: string, destPath: string, destName: string): Promise { + const destFixed = this.fixPathAndName(destPath, destName); + + const source = await this.resolveLocalFilesystemUrl(CoreTextUtils.instance.concatenatePaths(sourcePath, sourceName)); + + const destParentDir = await this.resolveDirectoryUrl(destFixed.path); + + return this.copyMock(source, destParentDir, destFixed.name); + } + + /** + * Creates a new directory in the specific path. + * The replace boolean value determines whether to replace an existing directory with the same name. + * If an existing directory exists and the replace value is false, the promise will fail and return an error. + * + * @param path Base FileSystem. + * @param dirName Name of directory to create + * @param replace If true, replaces file with same name. If false returns error + * @return Returns a Promise that resolves with a DirectoryEntry or rejects with an error. + */ + async createDir(path: string, dirName: string, replace: boolean): Promise { + const options: Flags = { + create: true, + }; + + if (!replace) { + options.exclusive = true; + } + + const parentDir = await this.resolveDirectoryUrl(path); + + return this.getDirectory(parentDir, dirName, options); + } + + /** + * Creates a new file in the specific path. + * The replace boolean value determines whether to replace an existing file with the same name. + * If an existing file exists and the replace value is false, the promise will fail and return an error. + * + * @param path Base FileSystem. + * @param fileName Name of file to create. + * @param replace If true, replaces file with same name. If false returns error. + * @return Returns a Promise that resolves to a FileEntry or rejects with an error. + */ + async createFile(path: string, fileName: string, replace: boolean): Promise { + const options: Flags = { + create: true, + }; + + if (!replace) { + options.exclusive = true; + } + + const parentDir = await this.resolveDirectoryUrl(path); + + return this.getFile(parentDir, fileName, options); + } + + /** + * Create a file writer for a certain file. + * + * @param fe File entry object. + * @return Promise resolved with the FileWriter. + */ + private createWriterMock(fe: FileEntry): Promise { + return new Promise((resolve, reject): void => { + fe.createWriter((writer) => { + resolve(writer); + }, (err) => { + this.fillErrorMessageMock(err); + reject(err); + }); + }); + } + + /** + * Fill the message for an error. + * + * @param error Error. + */ + private fillErrorMessageMock(error: FileError): void { + try { + error.message = this.cordovaFileError[error.code]; + } catch (e) { + // Ignore errors. + } + } + + /** + * Get a directory. + * + * @param directoryEntry Directory entry, obtained by resolveDirectoryUrl method + * @param directoryName Directory name + * @param flags Options + */ + getDirectory(directoryEntry: DirectoryEntry, directoryName: string, flags: Flags): Promise { + return new Promise((resolve, reject): void => { + try { + directoryName = directoryName.replace(/%20/g, ' '); // Replace all %20 with spaces. + + directoryEntry.getDirectory(directoryName, flags, (de) => { + resolve(de); + }, (err) => { + this.fillErrorMessageMock(err); + reject(err); + }); + } catch (xc) { + this.fillErrorMessageMock(xc); + reject(xc); + } + }); + } + + /** + * Get a file. + * + * @param directoryEntry Directory entry, obtained by resolveDirectoryUrl method + * @param fileName File name + * @param flags Options + */ + getFile(directoryEntry: DirectoryEntry, fileName: string, flags: Flags): Promise { + return new Promise((resolve, reject): void => { + try { + fileName = fileName.replace(/%20/g, ' '); // Replace all %20 with spaces. + + directoryEntry.getFile(fileName, flags, resolve, (err) => { + this.fillErrorMessageMock(err); + reject(err); + }); + } catch (xc) { + this.fillErrorMessageMock(xc); + reject(xc); + } + }); + } + + /** + * Get free disk space. + * + * @return Promise resolved with the free space. + */ + async getFreeDiskSpace(): Promise { + // Request a file system instance with a minimum size until we get an error. + if (window.requestFileSystem) { + let iterations = 0; + let maxIterations = 50; + const calculateByRequest = (size: number, ratio: number): Promise => + new Promise((resolve): void => { + window.requestFileSystem(LocalFileSystem.PERSISTENT, size, () => { + iterations++; + if (iterations > maxIterations) { + resolve(size); + + return; + } + // eslint-disable-next-line promise/catch-or-return + calculateByRequest(size * ratio, ratio).then(resolve); + }, () => { + resolve(size / ratio); + }); + }); + + // General calculation, base 1MB and increasing factor 1.3. + let size = await calculateByRequest(1048576, 1.3); + + // More accurate. Factor is 1.1. + iterations = 0; + maxIterations = 10; + + size = await calculateByRequest(size, 1.1); + + return size / 1024; // Return size in KB. + + } else { + throw new Error('File system not available.'); + } + } + + /** + * List files and directory from a given path. + * + * @param path Base FileSystem. Please refer to the iOS and Android filesystems above + * @param dirName Name of directory + * @return Returns a Promise that resolves to an array of Entry objects or rejects with an error. + */ + async listDir(path: string, dirName: string): Promise { + const parentDir = await this.resolveDirectoryUrl(path); + + const dirEntry = await this.getDirectory(parentDir, dirName, { create: false, exclusive: false }); + + return this.readEntriesMock(dirEntry.createReader()); + } + + /** + * Loads an initialize the API for browser. + * + * @return Promise resolved when loaded. + */ + load(): Promise { + return new Promise((resolve, reject): void => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const win = window; // Convert to to be able to use non-standard properties. + + if (typeof win.requestFileSystem == 'undefined') { + win.requestFileSystem = win.webkitRequestFileSystem; + } + if (typeof win.resolveLocalFileSystemURL == 'undefined') { + win.resolveLocalFileSystemURL = win.webkitResolveLocalFileSystemURL; + } + win.LocalFileSystem = { + PERSISTENT: 1, // eslint-disable-line @typescript-eslint/naming-convention + }; + + // Request a quota to use. Request 500MB. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ( navigator).webkitPersistentStorage.requestQuota(500 * 1024 * 1024, (granted) => { + window.requestFileSystem(LocalFileSystem.PERSISTENT, granted, (entry) => { + resolve(entry.root.toURL()); + }, reject); + }, reject); + }); + } + + /** + * Move a file or directory. + * + * @param srce The Entry to copy. + * @param destDir The directory where to move the file/dir. + * @param newName New name of the file/dir. + * @return Returns a Promise that resolves to the new Entry object or rejects with an error. + */ + private moveMock(srce: Entry, destDir: DirectoryEntry, newName: string): Promise { + return new Promise((resolve, reject): void => { + newName = newName.replace(/%20/g, ' '); // Replace all %20 with spaces. + + srce.moveTo(destDir, newName, (deste) => { + resolve(deste); + }, (err) => { + this.fillErrorMessageMock(err); + reject(err); + }); + }); + } + + /** + * Move a directory to a given path. + * + * @param path The source path to the directory. + * @param dirName The source directory name. + * @param newPath The destionation path to the directory. + * @param newDirName The destination directory name. + * @return Returns a Promise that resolves to the new DirectoryEntry object or rejects with + * an error. + */ + moveDir(path: string, dirName: string, newPath: string, newDirName: string): Promise { + return this.moveFileOrDir(path, dirName, newPath, newDirName); + } + + /** + * Move a file to a given path. + * + * @param path Base FileSystem. Please refer to the iOS and Android filesystems above + * @param fileName Name of file to move + * @param newPath Base FileSystem of new location + * @param newFileName New name of file to move to (leave blank to remain the same) + * @return Returns a Promise that resolves to the new Entry or rejects with an error. + */ + moveFile(path: string, fileName: string, newPath: string, newFileName: string): Promise { + return this.moveFileOrDir(path, fileName, newPath, newFileName || fileName); + } + + /** + * Move a file or dir to a given path. + * + * @param sourcePath Path of the file/dir to copy. + * @param sourceName Name of file/dir to copy + * @param destPath Path where to copy. + * @param destName New name of file/dir. + * @return Returns a Promise that resolves to the new Entry or rejects with an error. + */ + async moveFileOrDir(sourcePath: string, sourceName: string, destPath: string, destName: string): Promise { + const destFixed = this.fixPathAndName(destPath, destName); + + const source = await this.resolveLocalFilesystemUrl(CoreTextUtils.instance.concatenatePaths(sourcePath, sourceName)); + + const destParentDir = await this.resolveDirectoryUrl(destFixed.path); + + return this.moveMock(source, destParentDir, destFixed.name); + } + + /** + * Fix a path and name, making sure the name doesn't contain any folder. If it does, the folder will be moved to the path. + * + * @param path Path to fix. + * @param name Name to fix. + * @return Fixed values. + */ + protected fixPathAndName(path: string, name: string): {path: string; name: string} { + + const fullPath = CoreTextUtils.instance.concatenatePaths(path, name); + + return { + path: fullPath.substring(0, fullPath.lastIndexOf('/')), + name: fullPath.substr(fullPath.lastIndexOf('/') + 1), + }; + } + + /** + * Read file and return data as an ArrayBuffer. + * + * @param path Base FileSystem. + * @param file Name of file, relative to path. + * @return Returns a Promise that resolves with the contents of the file as ArrayBuffer or rejects + * with an error. + */ + readAsArrayBuffer(path: string, file: string): Promise { + return this.readFileMock(path, file, 'ArrayBuffer'); + } + + /** + * Read file and return data as a binary data. + * + * @param path Base FileSystem. + * @param file Name of file, relative to path. + * @return Returns a Promise that resolves with the contents of the file as string rejects with an error. + */ + readAsBinaryString(path: string, file: string): Promise { + return this.readFileMock(path, file, 'BinaryString'); + } + + /** + * Read file and return data as a base64 encoded data url. + * A data url is of the form: + * data: [][;base64], + * + * @param path Base FileSystem. + * @param file Name of file, relative to path. + * @return Returns a Promise that resolves with the contents of the file as data URL or rejects + * with an error. + */ + readAsDataURL(path: string, file: string): Promise { + return this.readFileMock(path, file, 'DataURL'); + } + + /** + * Read the contents of a file as text. + * + * @param path Base FileSystem. + * @param file Name of file, relative to path. + * @return Returns a Promise that resolves with the contents of the file as string or rejects with an error. + */ + readAsText(path: string, file: string): Promise { + return this.readFileMock(path, file, 'Text'); + } + + /** + * Read all the files and directories inside a directory. + * + * @param directoryReader The directory reader. + * @return Promise resolved with the list of files/dirs. + */ + private readEntriesMock(directoryReader: DirectoryReader): Promise { + return new Promise((resolve, reject): void => { + directoryReader.readEntries((entries: Entry[]) => { + resolve(entries); + }, (error: FileError) => { + this.fillErrorMessageMock(error); + reject(error); + }); + }); + } + + /** + * Read the contents of a file. + * + * @param path Base FileSystem. + * @param file Name of file, relative to path. + * @param readAs Format to read as. + * @return Returns a Promise that resolves with the contents of the file or rejects with an error. + */ + private async readFileMock( + path: string, + file: string, + readAs: 'ArrayBuffer' | 'BinaryString' | 'DataURL' | 'Text', + ): Promise { + const directoryEntry = await this.resolveDirectoryUrl(path); + + const fileEntry = await this.getFile(directoryEntry, file, { create: false }); + + const reader = new FileReader(); + + return new Promise((resolve, reject): void => { + reader.onloadend = (): void => { + if (reader.result !== undefined || reader.result !== null) { + resolve( reader.result); + } else if (reader.error !== undefined || reader.error !== null) { + reject(reader.error); + } else { + reject({ code: null, message: 'READER_ONLOADEND_ERR' }); + } + }; + + fileEntry.file((file) => { + reader[`readAs${readAs}`].call(reader, file); + }, (error) => { + reject(error); + }); + }); + } + + /** + * Delete a file. + * + * @param entry The file to remove. + * @return Promise resolved when done. + */ + private removeMock(entry: Entry): Promise { + return new Promise((resolve, reject): void => { + entry.remove(() => { + resolve({ success: true, fileRemoved: entry }); + }, (err) => { + this.fillErrorMessageMock(err); + reject(err); + }); + }); + } + + /** + * Remove a directory at a given path. + * + * @param path The path to the directory. + * @param dirName The directory name. + * @return Returns a Promise that resolves to a RemoveResult or rejects with an error. + */ + async removeDir(path: string, dirName: string): Promise { + const parentDir = await this.resolveDirectoryUrl(path); + + const dirEntry = await this.getDirectory(parentDir, dirName, { create: false }); + + return this.removeMock(dirEntry); + } + + /** + * Removes a file from a desired location. + * + * @param path Base FileSystem. + * @param fileName Name of file to remove. + * @return Returns a Promise that resolves to a RemoveResult or rejects with an error. + */ + async removeFile(path: string, fileName: string): Promise { + const parentDir = await this.resolveDirectoryUrl(path); + + const fileEntry = await this.getFile(parentDir, fileName, { create: false }); + + return this.removeMock(fileEntry); + } + + /** + * Removes all files and the directory from a desired location. + * + * @param path Base FileSystem. Please refer to the iOS and Android filesystems above + * @param dirName Name of directory + * @return Returns a Promise that resolves with a RemoveResult or rejects with an error. + */ + async removeRecursively(path: string, dirName: string): Promise { + const parentDir = await this.resolveDirectoryUrl(path); + + const dirEntry = await this.getDirectory(parentDir, dirName, { create: false }); + + return this.rimrafMock(dirEntry); + } + + /** + * Resolves a local directory url. + * + * @param directoryUrl directory system url + */ + async resolveDirectoryUrl(directoryUrl: string): Promise { + const dirEntry = await this.resolveLocalFilesystemUrl(directoryUrl); + + if (dirEntry.isDirectory) { + return dirEntry; + } else { + const error = new FileError(13); + error.message = 'input is not a directory'; + + throw error; + } + } + + /** + * Resolves a local file system URL. + * + * @param fileUrl file system url + */ + resolveLocalFilesystemUrl(fileUrl: string): Promise { + return new Promise((resolve, reject): void => { + try { + window.resolveLocalFileSystemURL(fileUrl, (entry: Entry) => { + resolve(entry); + }, (error: FileError) => { + this.fillErrorMessageMock(error); + reject(error); + }); + } catch (error) { + this.fillErrorMessageMock(error); + reject(error); + } + }); + } + + /** + * Remove a directory and all its contents. + * + * @param de Directory to remove. + * @return Promise resolved when done. + */ + private rimrafMock(de: DirectoryEntry): Promise { + return new Promise((resolve, reject): void => { + de.removeRecursively(() => { + resolve({ success: true, fileRemoved: de }); + }, (err) => { + this.fillErrorMessageMock(err); + reject(err); + }); + }); + } + + /** + * Write some data in a file. + * + * @param writer File writer. + * @param data The data to write. + * @return Promise resolved when done. + */ + protected writeMock(writer: FileWriter, data: string | Blob | ArrayBuffer): Promise { + if (data instanceof Blob) { + return this.writeFileInChunksMock(writer, data); + } + + if (data instanceof ArrayBuffer) { + // Convert to string. + data = String.fromCharCode.apply(null, new Uint8Array(data)); + } + + return new Promise((resolve, reject) => { + writer.onwriteend = (): void => { + if (writer.error) { + reject(writer.error); + } else { + resolve(); + } + }; + writer.write( data); + }); + } + + /** + * Write to an existing file. + * + * @param path Base FileSystem. + * @param fileName path relative to base path. + * @param text content or blob to write. + * @return Returns a Promise that resolves or rejects with an error. + */ + async writeExistingFile(path: string, fileName: string, text: string | Blob): Promise { + await this.writeFile(path, fileName, text, { replace: true }); + } + + /** + * Write a new file to the desired location. + * + * @param path Base FileSystem. Please refer to the iOS and Android filesystems above + * @param fileName path relative to base path + * @param text content or blob to write + * @param options replace file if set to true. See WriteOptions for more information. + * @return Returns a Promise that resolves to updated file entry or rejects with an error. + */ + async writeFile( + path: string, + fileName: string, + text: string | Blob | ArrayBuffer, + options: IWriteOptions = {}, + ): Promise { + const getFileOpts: Flags = { + create: !options.append, + exclusive: !options.replace, + }; + + const parentDir = await this.resolveDirectoryUrl(path); + + const fileEntry = await this.getFile(parentDir, fileName, getFileOpts); + + return this.writeFileEntryMock(fileEntry, text, options); + } + + /** + * Write content to FileEntry. + * + * @param fe File entry object. + * @param text Content or blob to write. + * @param options replace file if set to true. See WriteOptions for more information. + * @return Returns a Promise that resolves to updated file entry or rejects with an error. + */ + private writeFileEntryMock( + fileEntry: FileEntry, + text: string | Blob | ArrayBuffer, + options: IWriteOptions, + ): Promise { + return this.createWriterMock(fileEntry).then((writer) => { + if (options.append) { + writer.seek(writer.length); + } + + if (options.truncate) { + writer.truncate(options.truncate); + } + + return this.writeMock(writer, text); + }).then(() => fileEntry); + } + + /** + * Write a file in chunks. + * + * @param writer File writer. + * @param data Data to write. + * @return Promise resolved when done. + */ + private writeFileInChunksMock(writer: FileWriter, data: Blob): Promise { + let writtenSize = 0; + const BLOCK_SIZE = 1024 * 1024; + const writeNextChunk = () => { + const size = Math.min(BLOCK_SIZE, data.size - writtenSize); + const chunk = data.slice(writtenSize, writtenSize + size); + + writtenSize += size; + writer.write(chunk); + }; + + return new Promise((resolve, reject): void => { + writer.onerror = reject; + writer.onwriteend = (): void => { + if (writtenSize < data.size) { + writeNextChunk(); + } else { + resolve(); + } + }; + writeNextChunk(); + }); + } + +} diff --git a/src/app/core/emulator/services/geolocation.ts b/src/app/core/emulator/services/geolocation.ts new file mode 100644 index 000000000..12b1b334a --- /dev/null +++ b/src/app/core/emulator/services/geolocation.ts @@ -0,0 +1,61 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { Geolocation, GeolocationOptions, Geoposition } from '@ionic-native/geolocation/ngx'; +import { Observable, Subscriber, TeardownLogic } from 'rxjs'; + +/** + * Emulates the Cordova Geolocation plugin in desktop apps and in browser. + */ +@Injectable() +export class GeolocationMock extends Geolocation { + + /** + * Get the device's current position. + * + * @param options The geolocation options. + * @returns Returns a Promise that resolves with the position of the device, or rejects with an error. + */ + getCurrentPosition(options?: GeolocationOptions): Promise { + return new Promise((resolve, reject) => { + navigator.geolocation.getCurrentPosition((position) => { + // Convert to unknown first because some fields are incompatible due to null values. + resolve( position); + }, reject, options); + }); + } + + /** + * Watch the current device's position. Clear the watch by unsubscribing from + * Observable changes. + * + * @param options The geolocation options. + * @returns Returns an Observable that notifies with the position of the device, or errors. + */ + watchPosition(options?: GeolocationOptions): Observable { + return new Observable((subscriber: Subscriber): TeardownLogic => { + const watchId = navigator.geolocation.watchPosition( + subscriber.next.bind(subscriber), + subscriber.error.bind(subscriber), + options, + ); + + return (): void => { + navigator.geolocation.clearWatch(watchId); + }; + }); + } + +} diff --git a/src/app/core/emulator/services/helper.ts b/src/app/core/emulator/services/helper.ts new file mode 100644 index 000000000..b5c8c17e6 --- /dev/null +++ b/src/app/core/emulator/services/helper.ts @@ -0,0 +1,63 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { File } from '@ionic-native/file/ngx'; + +import { CoreFile } from '@services/file'; +import { CoreInitDelegate, CoreInitHandler } from '@services/init'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreLogger } from '@singletons/logger'; +import { FileMock } from './file'; +import { FileTransferErrorMock } from './file-transfer'; + +/** + * Helper service for the emulator feature. It also acts as an init handler. + */ +@Injectable() +export class CoreEmulatorHelperProvider implements CoreInitHandler { + + name = 'CoreEmulator'; + priority = CoreInitDelegate.MAX_RECOMMENDED_PRIORITY + 500; + blocking = true; + + protected logger: CoreLogger; + + constructor( + protected file: File, + ) { + this.logger = CoreLogger.getInstance('CoreEmulatorHelper'); + } + + /** + * Load the Mocks that need it. + * + * @return Promise resolved when loaded. + */ + load(): Promise { + const promises: Promise[] = []; + + window.FileTransferError = FileTransferErrorMock; + + promises.push(( this.file).load().then((basePath: string) => { + CoreFile.instance.setHTMLBasePath(basePath); + + return; + })); + + + return CoreUtils.instance.allPromises(promises); + } + +} diff --git a/src/app/core/emulator/services/inappbrowser.ts b/src/app/core/emulator/services/inappbrowser.ts new file mode 100644 index 000000000..bec1a4403 --- /dev/null +++ b/src/app/core/emulator/services/inappbrowser.ts @@ -0,0 +1,41 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { InAppBrowser, InAppBrowserObject } from '@ionic-native/in-app-browser/ngx'; + +/** + * Emulates the Cordova InAppBrowser plugin in desktop apps. + */ +@Injectable() +export class InAppBrowserMock extends InAppBrowser { + + /** + * Opens a URL in a new InAppBrowser instance, the current browser instance, or the system browser. + * + * @param url The URL to load. + * @param target The target in which to load the URL, an optional parameter that defaults to _self. + * @param options Options for the InAppBrowser. + * @return The new instance. + */ + create(url: string, target?: string, options: string = 'location=yes'): InAppBrowserObject { + if (options && typeof options !== 'string') { + // Convert to string. + options = Object.keys(options).map((key) => key + '=' + options[key]).join(','); + } + + return super.create(url, target, options); + } + +} diff --git a/src/app/core/emulator/services/network.ts b/src/app/core/emulator/services/network.ts new file mode 100644 index 000000000..7b1f79222 --- /dev/null +++ b/src/app/core/emulator/services/network.ts @@ -0,0 +1,82 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { Network } from '@ionic-native/network/ngx'; +import { Observable, Subject, merge } from 'rxjs'; + +/** + * Emulates the Cordova Network plugin in browser. + */ +@Injectable() +export class NetworkMock extends Network { + + type!: string; + + constructor() { + super(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ( window).Connection = { + UNKNOWN: 'unknown', // eslint-disable-line @typescript-eslint/naming-convention + ETHERNET: 'ethernet', // eslint-disable-line @typescript-eslint/naming-convention + WIFI: 'wifi', // eslint-disable-line @typescript-eslint/naming-convention + CELL_2G: '2g', // eslint-disable-line @typescript-eslint/naming-convention + CELL_3G: '3g', // eslint-disable-line @typescript-eslint/naming-convention + CELL_4G: '4g', // eslint-disable-line @typescript-eslint/naming-convention + CELL: 'cellular', // eslint-disable-line @typescript-eslint/naming-convention + NONE: 'none', // eslint-disable-line @typescript-eslint/naming-convention + }; + } + + /** + * Returns an observable to watch connection changes. + * + * @return Observable. + */ + onchange(): Observable { + return merge(this.onConnect(), this.onDisconnect()); + } + + /** + * Returns an observable to notify when the app is connected. + * + * @return Observable. + */ + onConnect(): Observable { + const observable = new Subject(); + + window.addEventListener('online', (ev) => { + observable.next(ev); + }, false); + + return observable; + } + + /** + * Returns an observable to notify when the app is disconnected. + * + * @return Observable. + */ + onDisconnect(): Observable { + const observable = new Subject(); + + window.addEventListener('offline', (ev) => { + observable.next(ev); + }, false); + + return observable; + } + +} diff --git a/src/app/core/emulator/services/zip.ts b/src/app/core/emulator/services/zip.ts new file mode 100644 index 000000000..d81398d1a --- /dev/null +++ b/src/app/core/emulator/services/zip.ts @@ -0,0 +1,129 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { File } from '@ionic-native/file/ngx'; +import { Zip } from '@ionic-native/zip/ngx'; +import * as JSZip from 'jszip'; + +import { CoreTextUtils } from '@services/utils/text'; + +/** + * Emulates the Cordova Zip plugin in browser. + */ +@Injectable() +export class ZipMock extends Zip { + + constructor(private file: File) { + super(); + } + + /** + * Create a directory. It creates all the foldes in dirPath 1 by 1 to prevent errors. + * + * @param destination Destination parent folder. + * @param dirPath Relative path to the folder. + * @return Promise resolved when done. + */ + protected async createDir(destination: string, dirPath: string): Promise { + // Create all the folders 1 by 1 in order, otherwise it fails. + const folders = dirPath.split('/'); + + for (let i = 0; i < folders.length; i++) { + const folder = folders[i]; + + await this.file.createDir(destination, folder, true); + + // Folder created, add it to the destination path. + destination = CoreTextUtils.instance.concatenatePaths(destination, folder); + } + } + + /** + * Extracts files from a ZIP archive. + * + * @param source Path to the source ZIP file. + * @param destination Destination folder. + * @param onProgress Optional callback to be called on progress update + * @return Promise that resolves with a number. 0 is success, -1 is error. + */ + async unzip(source: string, destination: string, onProgress?: (ev: {loaded: number; total: number}) => void): Promise { + + // Replace all %20 with spaces. + source = source.replace(/%20/g, ' '); + destination = destination.replace(/%20/g, ' '); + + const sourceDir = source.substring(0, source.lastIndexOf('/')); + const sourceName = source.substr(source.lastIndexOf('/') + 1); + const zip = new JSZip(); + + try { + // Read the file first. + const data = await this.file.readAsArrayBuffer(sourceDir, sourceName); + + // Now load the file using the JSZip library. + await zip.loadAsync(data); + + if (!zip.files || !Object.keys(zip.files).length) { + // Nothing to extract. + return 0; + } + + // First of all, create the directory where the files will be unzipped. + const destParent = destination.substring(0, destination.lastIndexOf('/')); + const destFolderName = destination.substr(destination.lastIndexOf('/') + 1); + + await this.file.createDir(destParent, destFolderName, true); + + const total = Object.keys(zip.files).length; + let loaded = 0; + + await Promise.all(Object.keys(zip.files).map(async (name) => { + const file = zip.files[name]; + + if (!file.dir) { + // It's a file. + const fileDir = name.substring(0, name.lastIndexOf('/')); + const fileName = name.substr(name.lastIndexOf('/') + 1); + + if (fileDir) { + // The file is in a subfolder, create it first. + await this.createDir(destination, fileDir); + } + + // Read the file contents as a Blob. + const fileData = await file.async('blob'); + + // File read and parent folder created, now write the file. + const parentFolder = CoreTextUtils.instance.concatenatePaths(destination, fileDir); + + await this.file.writeFile(parentFolder, fileName, fileData, { replace: true }); + } else { + // It's a folder, create it if it doesn't exist. + await this.createDir(destination, name); + } + + // File unzipped, call the progress. + loaded++; + onProgress && onProgress({ loaded: loaded, total: total }); + })); + + return 0; + } catch (error) { + // Error. + return -1; + } + } + +} diff --git a/src/app/services/file-helper.ts b/src/app/services/file-helper.ts index 336c6cc91..b9280a4ce 100644 --- a/src/app/services/file-helper.ts +++ b/src/app/services/file-helper.ts @@ -46,8 +46,8 @@ export class CoreFileHelperProvider { */ async downloadAndOpenFile( file: CoreWSExternalFile, - component: string, - componentId: string | number, + component?: string, + componentId?: string | number, state?: string, onProgress?: CoreFileHelperOnProgress, siteId?: string, diff --git a/src/app/services/file.ts b/src/app/services/file.ts index 980bc83d0..7ac755faf 100644 --- a/src/app/services/file.ts +++ b/src/app/services/file.ts @@ -251,7 +251,7 @@ export class CoreFileProvider { * @return Promise to be resolved when the file is created. */ async createFile(path: string, failIfExists?: boolean): Promise { - const entry = await this.create(true, path, failIfExists); + const entry = await this.create(false, path, failIfExists); return entry; } @@ -568,8 +568,7 @@ export class CoreFileProvider { // Create file (and parent folders) to prevent errors. const fileEntry = await this.createFile(path); - if (this.isHTMLAPI && - (typeof data == 'string' || data.toString() == '[object ArrayBuffer]')) { + if (this.isHTMLAPI && (typeof data == 'string' || data.toString() == '[object ArrayBuffer]')) { // We need to write Blobs. const extension = CoreMimetypeUtils.instance.getFileExtension(path); const type = extension ? CoreMimetypeUtils.instance.getMimeType(extension) : ''; diff --git a/src/app/services/utils/mimetype.ts b/src/app/services/utils/mimetype.ts index 63414cbd8..a82301b50 100644 --- a/src/app/services/utils/mimetype.ts +++ b/src/app/services/utils/mimetype.ts @@ -17,11 +17,14 @@ import { FileEntry } from '@ionic-native/file'; import { CoreFile } from '@services/file'; import { CoreTextUtils } from '@services/utils/text'; -import { makeSingleton, Translate, Http } from '@singletons/core.singletons'; +import { makeSingleton, Translate } from '@singletons/core.singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreWSExternalFile } from '@services/ws'; import { CoreUtils } from '@services/utils/utils'; +import extToMime from '@/assets/exttomime.json'; +import mimeToExt from '@/assets/mimetoext.json'; + interface MimeTypeInfo { type: string; icon?: string; @@ -52,17 +55,8 @@ export class CoreMimetypeUtilsProvider { constructor() { this.logger = CoreLogger.getInstance('CoreMimetypeUtilsProvider'); - Http.instance.get('assets/exttomime.json').subscribe((result: Record) => { - this.extToMime = result; - }, () => { - // Error, shouldn't happen. - }); - - Http.instance.get('assets/mimetoext.json').subscribe((result: Record) => { - this.mimeToExt = result; - }, () => { - // Error, shouldn't happen - }); + this.extToMime = extToMime; + this.mimeToExt = mimeToExt; } /** diff --git a/src/assets/img/files/archive-64.png b/src/assets/img/files/archive-64.png new file mode 100644 index 000000000..ba7111a02 Binary files /dev/null and b/src/assets/img/files/archive-64.png differ diff --git a/src/assets/img/files/audio-64.png b/src/assets/img/files/audio-64.png new file mode 100644 index 000000000..e98c78dd7 Binary files /dev/null and b/src/assets/img/files/audio-64.png differ diff --git a/src/assets/img/files/avi-64.png b/src/assets/img/files/avi-64.png new file mode 100644 index 000000000..ec1a942cd Binary files /dev/null and b/src/assets/img/files/avi-64.png differ diff --git a/src/assets/img/files/base-64.png b/src/assets/img/files/base-64.png new file mode 100644 index 000000000..cf698f525 Binary files /dev/null and b/src/assets/img/files/base-64.png differ diff --git a/src/assets/img/files/bmp-64.png b/src/assets/img/files/bmp-64.png new file mode 100644 index 000000000..562e7bbab Binary files /dev/null and b/src/assets/img/files/bmp-64.png differ diff --git a/src/assets/img/files/calc-64.png b/src/assets/img/files/calc-64.png new file mode 100644 index 000000000..b813dd2cb Binary files /dev/null and b/src/assets/img/files/calc-64.png differ diff --git a/src/assets/img/files/chart-64.png b/src/assets/img/files/chart-64.png new file mode 100644 index 000000000..4b8f85bd6 Binary files /dev/null and b/src/assets/img/files/chart-64.png differ diff --git a/src/assets/img/files/database-64.png b/src/assets/img/files/database-64.png new file mode 100644 index 000000000..33d5043c1 Binary files /dev/null and b/src/assets/img/files/database-64.png differ diff --git a/src/assets/img/files/document-64.png b/src/assets/img/files/document-64.png new file mode 100644 index 000000000..0888ebbbd Binary files /dev/null and b/src/assets/img/files/document-64.png differ diff --git a/src/assets/img/files/draw-64.png b/src/assets/img/files/draw-64.png new file mode 100644 index 000000000..1b827c7c6 Binary files /dev/null and b/src/assets/img/files/draw-64.png differ diff --git a/src/assets/img/files/eps-64.png b/src/assets/img/files/eps-64.png new file mode 100644 index 000000000..c42492441 Binary files /dev/null and b/src/assets/img/files/eps-64.png differ diff --git a/src/assets/img/files/epub-64.png b/src/assets/img/files/epub-64.png new file mode 100644 index 000000000..298d5dcd9 Binary files /dev/null and b/src/assets/img/files/epub-64.png differ diff --git a/src/assets/img/files/flash-64.png b/src/assets/img/files/flash-64.png new file mode 100644 index 000000000..01d28e03e Binary files /dev/null and b/src/assets/img/files/flash-64.png differ diff --git a/src/assets/img/files/folder-64.png b/src/assets/img/files/folder-64.png new file mode 100644 index 000000000..2508ab252 Binary files /dev/null and b/src/assets/img/files/folder-64.png differ diff --git a/src/assets/img/files/folder-open-64.png b/src/assets/img/files/folder-open-64.png new file mode 100644 index 000000000..27f7271bd Binary files /dev/null and b/src/assets/img/files/folder-open-64.png differ diff --git a/src/assets/img/files/gif-64.png b/src/assets/img/files/gif-64.png new file mode 100644 index 000000000..2373292b7 Binary files /dev/null and b/src/assets/img/files/gif-64.png differ diff --git a/src/assets/img/files/h5p-64.png b/src/assets/img/files/h5p-64.png new file mode 100644 index 000000000..cdd1c9b1b Binary files /dev/null and b/src/assets/img/files/h5p-64.png differ diff --git a/src/assets/img/files/html-64.png b/src/assets/img/files/html-64.png new file mode 100644 index 000000000..7f703bb83 Binary files /dev/null and b/src/assets/img/files/html-64.png differ diff --git a/src/assets/img/files/image-64.png b/src/assets/img/files/image-64.png new file mode 100644 index 000000000..2d8f9e4fa Binary files /dev/null and b/src/assets/img/files/image-64.png differ diff --git a/src/assets/img/files/impress-64.png b/src/assets/img/files/impress-64.png new file mode 100644 index 000000000..c279c62ef Binary files /dev/null and b/src/assets/img/files/impress-64.png differ diff --git a/src/assets/img/files/isf-64.png b/src/assets/img/files/isf-64.png new file mode 100644 index 000000000..ad5a18867 Binary files /dev/null and b/src/assets/img/files/isf-64.png differ diff --git a/src/assets/img/files/jpeg-64.png b/src/assets/img/files/jpeg-64.png new file mode 100644 index 000000000..b4fc0c998 Binary files /dev/null and b/src/assets/img/files/jpeg-64.png differ diff --git a/src/assets/img/files/markup-64.png b/src/assets/img/files/markup-64.png new file mode 100644 index 000000000..b89072713 Binary files /dev/null and b/src/assets/img/files/markup-64.png differ diff --git a/src/assets/img/files/math-64.png b/src/assets/img/files/math-64.png new file mode 100644 index 000000000..d98beea61 Binary files /dev/null and b/src/assets/img/files/math-64.png differ diff --git a/src/assets/img/files/moodle-64.png b/src/assets/img/files/moodle-64.png new file mode 100644 index 000000000..44ad3a37d Binary files /dev/null and b/src/assets/img/files/moodle-64.png differ diff --git a/src/assets/img/files/mp3-64.png b/src/assets/img/files/mp3-64.png new file mode 100644 index 000000000..13b8da0b8 Binary files /dev/null and b/src/assets/img/files/mp3-64.png differ diff --git a/src/assets/img/files/mpeg-64.png b/src/assets/img/files/mpeg-64.png new file mode 100644 index 000000000..05d77fa7b Binary files /dev/null and b/src/assets/img/files/mpeg-64.png differ diff --git a/src/assets/img/files/oth-64.png b/src/assets/img/files/oth-64.png new file mode 100644 index 000000000..8ffa8b466 Binary files /dev/null and b/src/assets/img/files/oth-64.png differ diff --git a/src/assets/img/files/pdf-64.png b/src/assets/img/files/pdf-64.png new file mode 100644 index 000000000..b7cdae7e9 Binary files /dev/null and b/src/assets/img/files/pdf-64.png differ diff --git a/src/assets/img/files/png-64.png b/src/assets/img/files/png-64.png new file mode 100644 index 000000000..3ecf3e5df Binary files /dev/null and b/src/assets/img/files/png-64.png differ diff --git a/src/assets/img/files/powerpoint-64.png b/src/assets/img/files/powerpoint-64.png new file mode 100644 index 000000000..4d44c7d2b Binary files /dev/null and b/src/assets/img/files/powerpoint-64.png differ diff --git a/src/assets/img/files/psd-64.png b/src/assets/img/files/psd-64.png new file mode 100644 index 000000000..0b800dffa Binary files /dev/null and b/src/assets/img/files/psd-64.png differ diff --git a/src/assets/img/files/publisher-64.png b/src/assets/img/files/publisher-64.png new file mode 100644 index 000000000..9633ef3dd Binary files /dev/null and b/src/assets/img/files/publisher-64.png differ diff --git a/src/assets/img/files/quicktime-64.png b/src/assets/img/files/quicktime-64.png new file mode 100644 index 000000000..90f2fbc0d Binary files /dev/null and b/src/assets/img/files/quicktime-64.png differ diff --git a/src/assets/img/files/sourcecode-64.png b/src/assets/img/files/sourcecode-64.png new file mode 100644 index 000000000..ad25537e4 Binary files /dev/null and b/src/assets/img/files/sourcecode-64.png differ diff --git a/src/assets/img/files/spreadsheet-64.png b/src/assets/img/files/spreadsheet-64.png new file mode 100644 index 000000000..00427c0e7 Binary files /dev/null and b/src/assets/img/files/spreadsheet-64.png differ diff --git a/src/assets/img/files/text-64.png b/src/assets/img/files/text-64.png new file mode 100644 index 000000000..7b397cea5 Binary files /dev/null and b/src/assets/img/files/text-64.png differ diff --git a/src/assets/img/files/tiff-64.png b/src/assets/img/files/tiff-64.png new file mode 100644 index 000000000..c11a85e28 Binary files /dev/null and b/src/assets/img/files/tiff-64.png differ diff --git a/src/assets/img/files/unknown-64.png b/src/assets/img/files/unknown-64.png new file mode 100644 index 000000000..7f703bb83 Binary files /dev/null and b/src/assets/img/files/unknown-64.png differ diff --git a/src/assets/img/files/video-64.png b/src/assets/img/files/video-64.png new file mode 100644 index 000000000..570c4b2b3 Binary files /dev/null and b/src/assets/img/files/video-64.png differ diff --git a/src/assets/img/files/wav-64.png b/src/assets/img/files/wav-64.png new file mode 100644 index 000000000..819781a9b Binary files /dev/null and b/src/assets/img/files/wav-64.png differ diff --git a/src/assets/img/files/wmv-64.png b/src/assets/img/files/wmv-64.png new file mode 100644 index 000000000..570c4b2b3 Binary files /dev/null and b/src/assets/img/files/wmv-64.png differ diff --git a/src/assets/img/files/writer-64.png b/src/assets/img/files/writer-64.png new file mode 100644 index 000000000..6285b6ffa Binary files /dev/null and b/src/assets/img/files/writer-64.png differ