diff --git a/package-lock.json b/package-lock.json index b04de5b67..8bf763956 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5263,6 +5263,11 @@ } } }, + "@moodlehq/cordova-plugin-file-opener": { + "version": "3.0.5-moodle.1", + "resolved": "https://registry.npmjs.org/@moodlehq/cordova-plugin-file-opener/-/cordova-plugin-file-opener-3.0.5-moodle.1.tgz", + "integrity": "sha512-YOa76EQnnanOHDR7swDH2/K6z29rcBkdZPEak7m8lhb2O5f/kvcpJxXjf1JqVsuetsOQ2hdRDAuFLMX5mRKUJw==" + }, "@moodlehq/cordova-plugin-file-transfer": { "version": "1.7.1-moodle.5", "resolved": "https://registry.npmjs.org/@moodlehq/cordova-plugin-file-transfer/-/cordova-plugin-file-transfer-1.7.1-moodle.5.tgz", @@ -5273,6 +5278,11 @@ "resolved": "https://registry.npmjs.org/@moodlehq/cordova-plugin-inappbrowser/-/cordova-plugin-inappbrowser-5.0.0-moodle.3.tgz", "integrity": "sha512-BDW53W8BzHIJY6lqV3IyYIO9Rh3qi/nA3qkwZjvJiw7iohlQMeR67LV+bXjM4I8N1PTGoBSXiS5BmaS9NFi/1A==" }, + "@moodlehq/cordova-plugin-intent": { + "version": "2.2.0-moodle.1", + "resolved": "https://registry.npmjs.org/@moodlehq/cordova-plugin-intent/-/cordova-plugin-intent-2.2.0-moodle.1.tgz", + "integrity": "sha512-Pmd+Xa146LcNlU39z8NLElAk7dxp2g75IRQ+zyXkRTzj7h+0JtNgWWw14jFVxOINoMWo0C7ZihJfeQdn+vrVmA==" + }, "@moodlehq/cordova-plugin-ionic-webview": { "version": "5.0.0-moodle.1", "resolved": "https://registry.npmjs.org/@moodlehq/cordova-plugin-ionic-webview/-/cordova-plugin-ionic-webview-5.0.0-moodle.1.tgz", @@ -13250,11 +13260,6 @@ "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" }, - "com-darryncampbell-cordova-plugin-intent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/com-darryncampbell-cordova-plugin-intent/-/com-darryncampbell-cordova-plugin-intent-2.2.0.tgz", - "integrity": "sha512-4ESoeYghE9GGuxKi4pnG+6CUJyYjS2j1tOmvlXXEM/9d5aBU47EpWbKKU1gjcfZFM4KCUbyba1NX6xNcH/L/wA==" - }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -13339,7 +13344,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", @@ -14230,11 +14235,6 @@ "resolved": "https://registry.npmjs.org/cordova-plugin-file/-/cordova-plugin-file-6.0.2.tgz", "integrity": "sha512-m7cughw327CjONN/qjzsTpSesLaeybksQh420/gRuSXJX5Zt9NfgsSbqqKDon6jnQ9Mm7h7imgyO2uJ34XMBtA==" }, - "cordova-plugin-file-opener2": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/cordova-plugin-file-opener2/-/cordova-plugin-file-opener2-3.0.5.tgz", - "integrity": "sha512-tjLHDamH5+y0bJZYVe2967L1S4R8tL4Y0rJUzJGoxsyiw3FUlrJNS199POOpzZZ6Xhlntn9a2o7+84r1dMN21A==" - }, "cordova-plugin-geolocation": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cordova-plugin-geolocation/-/cordova-plugin-geolocation-4.1.0.tgz", @@ -27333,7 +27333,7 @@ "properties-parser": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/properties-parser/-/properties-parser-0.3.1.tgz", - "integrity": "sha1-ExbpU5/7/ZOEXjabIRAiq9R4dxo=", + "integrity": "sha512-AkSQxQAviJ89x4FIxOyHGfO3uund0gvYo7lfD0E+Gp7gFQKrTNgtoYQklu8EhrfHVZUzTwKGZx2r/KDSfnljcA==", "requires": { "string.prototype.codepointat": "^0.2.0" } diff --git a/package.json b/package.json index f4e368461..830c49903 100644 --- a/package.json +++ b/package.json @@ -74,8 +74,10 @@ "@ionic-native/web-intent": "^5.36.0", "@ionic-native/zip": "^5.36.0", "@ionic/angular": "^5.9.2", + "@moodlehq/cordova-plugin-file-opener": "^3.0.5-moodle.1", "@moodlehq/cordova-plugin-file-transfer": "1.7.1-moodle.5", "@moodlehq/cordova-plugin-inappbrowser": "5.0.0-moodle.3", + "@moodlehq/cordova-plugin-intent": "^2.2.0-moodle.1", "@moodlehq/cordova-plugin-ionic-webview": "5.0.0-moodle.1", "@moodlehq/cordova-plugin-local-notification": "0.9.0-moodle.7", "@moodlehq/cordova-plugin-qrscanner": "3.0.1-moodle.4", @@ -87,7 +89,6 @@ "@types/cordova": "0.0.34", "@types/dom-mediacapture-record": "^1.0.7", "chart.js": "^2.9.4", - "com-darryncampbell-cordova-plugin-intent": "^2.2.0", "cordova": "^11.0.0", "cordova-android": "^10.1.1", "cordova-clipboard": "^1.3.0", @@ -100,7 +101,6 @@ "cordova-plugin-customurlscheme": "^5.0.2", "cordova-plugin-device": "^2.1.0", "cordova-plugin-file": "6.0.2", - "cordova-plugin-file-opener2": "^3.0.5", "cordova-plugin-geolocation": "^4.1.0", "cordova-plugin-ionic-keyboard": "^2.2.0", "cordova-plugin-media": "5.0.4", @@ -207,7 +207,7 @@ "ANDROID_PATHPREFIX": "/" }, "cordova-plugin-device": {}, - "cordova-plugin-file-opener2": { + "@moodlehq/cordova-plugin-file-opener": { "ANDROID_SUPPORT_V4_VERSION": "27.+" }, "cordova-plugin-geolocation": { @@ -235,7 +235,7 @@ "ANDROIDX_CORE_VERSION": "1.6.+", "FCM_VERSION": "23.+" }, - "com-darryncampbell-cordova-plugin-intent": {}, + "@moodlehq/cordova-plugin-intent": {}, "nl.kingsquare.cordova.background-audio": {}, "cordova.plugins.diagnostic": { "ANDROID_SUPPORT_VERSION": "28.+", @@ -251,4 +251,4 @@ "optionalDependencies": { "keytar": "^7.2.0" } -} +} \ No newline at end of file diff --git a/scripts/langindex.json b/scripts/langindex.json index 1ef97ed16..7cd0b2a57 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1474,6 +1474,7 @@ "core.cannotconnecttrouble": "local_moodlemobileapp", "core.cannotconnectverify": "local_moodlemobileapp", "core.cannotdownloadfiles": "local_moodlemobileapp", + "core.cannotinstallapk": "local_moodlemobileapp", "core.cannotlogoutpageblocks": "local_moodlemobileapp", "core.cannotopeninapp": "local_moodlemobileapp", "core.cannotopeninappdownload": "local_moodlemobileapp", diff --git a/src/core/lang.json b/src/core/lang.json index e61537e0e..bdfd886f4 100644 --- a/src/core/lang.json +++ b/src/core/lang.json @@ -18,6 +18,7 @@ "cannotconnecttrouble": "We're having trouble connecting to your site.", "cannotconnectverify": "Please check the address is correct.", "cannotdownloadfiles": "File downloading is disabled. Please contact your site administrator.", + "cannotinstallapk": "For security reasons, you can't install unknown apps on your device from this app. Please open the file using a browser.", "cannotlogoutpageblocks": "Please save or discard your changes before continuing.", "cannotopeninapp": "This file may not work as expected on this device. Would you like to open it anyway?", "cannotopeninappdownload": "This file may not work as expected on this device. Would you like to download it anyway?", diff --git a/src/core/services/file-helper.ts b/src/core/services/file-helper.ts index 7817c4d7b..cc245e3ef 100644 --- a/src/core/services/file-helper.ts +++ b/src/core/services/file-helper.ts @@ -30,6 +30,7 @@ import { makeSingleton, Translate } from '@singletons'; import { CoreNetworkError } from '@classes/errors/network-error'; import { CoreConfig } from './config'; import { CoreCanceledError } from '@classes/errors/cancelederror'; +import { CoreMimetypeUtils } from '@services/utils/mimetype'; /** * Provider to provide some helper functions regarding files and packages. @@ -306,18 +307,23 @@ export class CoreFileHelperProvider { } /** - * Whether the file has to be opened in browser (external repository). - * The file must have a mimetype attribute. + * Whether the file has to be opened in browser. * * @param file The file to check. * @return Whether the file should be opened in browser. */ shouldOpenInBrowser(file: CoreWSFile): boolean { - if (!file || !('isexternalfile' in file) || !file.isexternalfile || !file.mimetype) { + if (!file.mimetype) { return false; } const mimetype = file.mimetype; + + if (!('isexternalfile' in file) || !file.isexternalfile) { + return mimetype === 'application/vnd.android.package-archive' + || CoreMimetypeUtils.getFileExtension(file.filename ?? '') === 'apk'; + } + if (mimetype.indexOf('application/vnd.google-apps.') != -1) { // Google Docs file, always open in browser. return true; diff --git a/src/core/services/file.ts b/src/core/services/file.ts index 7a8e59ca5..a6123084a 100644 --- a/src/core/services/file.ts +++ b/src/core/services/file.ts @@ -217,8 +217,7 @@ export class CoreFileProvider { ): Promise { await this.init(); - // Remove basePath if it's in the path. - path = this.removeStartingSlash(path.replace(this.basePath, '')); + path = this.removeBasePath(path); base = base || this.basePath; if (path.indexOf('/') == -1) { @@ -280,8 +279,7 @@ export class CoreFileProvider { async removeDir(path: string): Promise { await this.init(); - // Remove basePath if it's in the path. - path = this.removeStartingSlash(path.replace(this.basePath, '')); + path = this.removeBasePath(path); this.logger.debug('Remove directory: ' + path); await File.removeRecursively(this.basePath, path); @@ -296,8 +294,7 @@ export class CoreFileProvider { async removeFile(path: string): Promise { await this.init(); - // Remove basePath if it's in the path. - path = this.removeStartingSlash(path.replace(this.basePath, '')); + path = this.removeBasePath(path); this.logger.debug('Remove file: ' + path); try { @@ -333,8 +330,7 @@ export class CoreFileProvider { async getDirectoryContents(path: string): Promise<(FileEntry | DirectoryEntry)[]> { await this.init(); - // Remove basePath if it's in the path. - path = this.removeStartingSlash(path.replace(this.basePath, '')); + path = this.removeBasePath(path); this.logger.debug('Get contents of dir: ' + path); const result = await File.listDir(this.basePath, path); @@ -402,8 +398,7 @@ export class CoreFileProvider { * @return Promise to be resolved when the size is calculated. */ getDirectorySize(path: string): Promise { - // Remove basePath if it's in the path. - path = this.removeStartingSlash(path.replace(this.basePath, '')); + path = this.removeBasePath(path); this.logger.debug('Get size of dir: ' + path); @@ -417,8 +412,7 @@ export class CoreFileProvider { * @return Promise to be resolved when the size is calculated. */ getFileSize(path: string): Promise { - // Remove basePath if it's in the path. - path = this.removeStartingSlash(path.replace(this.basePath, '')); + path = this.removeBasePath(path); this.logger.debug('Get size of file: ' + path); @@ -491,8 +485,7 @@ export class CoreFileProvider { if (!folder) { folder = this.basePath; - // Remove basePath if it's in the path. - path = this.removeStartingSlash(path.replace(this.basePath, '')); + path = this.removeBasePath(path); } this.logger.debug(`Read file ${path} with format ${format} in folder ${folder}`); @@ -593,8 +586,7 @@ export class CoreFileProvider { async writeFile(path: string, data: string | Blob, append?: boolean): Promise { await this.init(); - // Remove basePath if it's in the path. - path = this.removeStartingSlash(path.replace(this.basePath, '')); + path = this.removeBasePath(path); this.logger.debug('Write file: ' + path); // Create file (and parent folders) to prevent errors. @@ -840,9 +832,8 @@ export class CoreFileProvider { await this.init(); - // Paths cannot start with "/". Remove basePath if present. - from = this.removeStartingSlash(from.replace(this.basePath, '')); - to = this.removeStartingSlash(to.replace(this.basePath, '')); + from = this.removeBasePath(from); + to = this.removeBasePath(to); const toFileAndDir = this.getFileAndDirectoryFromPath(to); @@ -925,17 +916,13 @@ export class CoreFileProvider { } /** - * Remove the base path from a path. If basePath isn't found, return false. + * Remove the base path from a path. * * @param path Path to treat. - * @return Path without basePath if basePath was found, undefined otherwise. + * @return Path without basePath. */ removeBasePath(path: string): string { - if (path.indexOf(this.basePath) > -1) { - return path.replace(this.basePath, ''); - } - - return path; + return CoreText.removeStartingSlash(path.replace(this.basePath, '')); } /** @@ -1036,13 +1023,10 @@ export class CoreFileProvider { * * @param path Path. * @return Path without a slash in the first position. + * @deprecated since 4.1. Use CoreText.removeStartingSlash instead. */ removeStartingSlash(path: string): string { - if (path[0] == '/') { - return path.substring(1); - } - - return path; + return CoreText.removeStartingSlash(path); } /** diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index fbe4a2e2b..73c7dd48d 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -1410,6 +1410,19 @@ export class CoreFilepoolProvider { return this.getFilePath(siteId, fileId); } + /** + * Get the url of a file form its path. + * + * @param siteId The site ID. + * @param path File path. + * @returns File url. + */ + async getFileUrlByPath(siteId: string, path: string): Promise { + const record = await this.filesTables[siteId].getOne({ path }); + + return record.url; + } + /** * Get site Filepool Folder Path * diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index 73e253df3..3ee795149 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -36,6 +36,9 @@ import { CoreWindow } from '@singletons/window'; import { CoreColors } from '@singletons/colors'; import { CorePromisedValue } from '@classes/promised-value'; import { CorePlatform } from '@services/platform'; +import { CoreErrorWithOptions } from '@classes/errors/errorwithtitle'; +import { CoreFilepool } from '@services/filepool'; +import { CoreSites } from '@services/sites'; export type TreeNode = T & { children: TreeNode[] }; @@ -988,6 +991,23 @@ export class CoreUtilsProvider { this.openInApp(path); return; + } else if (extension === 'apk' && CoreApp.isAndroid()) { + const url = await CoreUtils.ignoreErrors( + CoreFilepool.getFileUrlByPath(CoreSites.getCurrentSiteId(), CoreFile.removeBasePath(path)), + ); + + // @todo MOBILE-4167: Handle urls with expired tokens. + + throw new CoreErrorWithOptions( + Translate.instant('core.cannotinstallapk'), + undefined, + url + ? [{ + text: Translate.instant('core.openinbrowser'), + handler: () => this.openInBrowser(url), + }] + : undefined, + ); } // Path needs to be decoded, the file won't be opened if the path has %20 instead of spaces and so.