diff --git a/src/addon/mod/data/components/index/index.ts b/src/addon/mod/data/components/index/index.ts index 0b3e22870..af1ab14b6 100644 --- a/src/addon/mod/data/components/index/index.ts +++ b/src/addon/mod/data/components/index/index.ts @@ -124,18 +124,8 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp this.selectedGroup = this.group || 0; this.loadContent(false, true).then(() => { - if (!this.data || !this.data.id) { - return; - } - - this.dataProvider.logView(this.data.id, this.data.name).then(() => { - this.courseProvider.checkModuleCompletion(this.courseId, this.module.completiondata); - }).catch(() => { - // Ignore errors. - }); + return this.logView(true); }); - - // Setup search modal. } /** @@ -362,7 +352,10 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp this.loaded = false; this.search.page = page; - return this.fetchEntriesData().catch((message) => { + return this.fetchEntriesData().then(() => { + // Log activity view for coherence with Moodle web. + return this.logView(); + }).catch((message) => { this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); }).finally(() => { this.loaded = true; @@ -392,7 +385,10 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp this.selectedGroup = groupId; this.search.page = 0; - return this.fetchEntriesData().catch((message) => { + return this.fetchEntriesData().then(() => { + // Log activity view for coherence with Moodle web. + return this.logView(); + }).catch((message) => { this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); return Promise.reject(null); @@ -454,6 +450,26 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp return result.updated; } + /** + * Log viewing the activity. + * + * @param checkCompletion Whether to check completion. + * @return Promise resolved when done. + */ + protected logView(checkCompletion?: boolean): Promise { + if (!this.data || !this.data.id) { + return Promise.resolve(); + } + + return this.dataProvider.logView(this.data.id, this.data.name).then(() => { + if (checkCompletion) { + this.courseProvider.checkModuleCompletion(this.courseId, this.module.completiondata); + } + }).catch(() => { + // Ignore errors, the user could be offline. + }); + } + /** * Component being destroyed. */ diff --git a/src/addon/mod/data/pages/entry/entry.ts b/src/addon/mod/data/pages/entry/entry.ts index 8bbf7207f..ed8e2a84e 100644 --- a/src/addon/mod/data/pages/entry/entry.ts +++ b/src/addon/mod/data/pages/entry/entry.ts @@ -96,7 +96,9 @@ export class AddonModDataEntryPage implements OnDestroy { */ ionViewDidLoad(): void { this.commentsEnabled = !this.commentsProvider.areCommentsDisabledInSite(); - this.fetchEntryData(); + this.fetchEntryData().then(() => { + this.logView(); + }); // Refresh data if this discussion is synchronized automatically. this.syncObserver = this.eventsProvider.on(AddonModDataSyncProvider.AUTO_SYNCED, (data) => { @@ -199,7 +201,9 @@ export class AddonModDataEntryPage implements OnDestroy { this.entry = null; this.entryLoaded = false; - return this.fetchEntryData(); + return this.fetchEntryData().then(() => { + this.logView(); + }); } /** @@ -258,7 +262,9 @@ export class AddonModDataEntryPage implements OnDestroy { this.entryId = null; this.entryLoaded = false; - return this.fetchEntryData(); + return this.fetchEntryData().then(() => { + this.logView(); + }); } /** @@ -363,6 +369,21 @@ export class AddonModDataEntryPage implements OnDestroy { this.dataProvider.invalidateEntryData(this.data.id, this.entryId); } + /** + * Log viewing the activity. + * + * @return Promise resolved when done. + */ + protected logView(): Promise { + if (!this.data || !this.data.id) { + return Promise.resolve(); + } + + return this.dataProvider.logView(this.data.id, this.data.name).catch(() => { + // Ignore errors, the user could be offline. + }); + } + /** * Component being destroyed. */ diff --git a/src/core/fileuploader/providers/fileuploader.ts b/src/core/fileuploader/providers/fileuploader.ts index 520e5e631..a061b8e29 100644 --- a/src/core/fileuploader/providers/fileuploader.ts +++ b/src/core/fileuploader/providers/fileuploader.ts @@ -150,20 +150,25 @@ export class CoreFileUploaderProvider { * @return Options. */ getCameraUploadOptions(uri: string, isFromAlbum?: boolean): CoreFileUploaderOptions { - const extension = this.mimeUtils.getExtension(uri), - mimetype = this.mimeUtils.getMimeType(extension), - isIOS = this.platform.is('ios'), - options: CoreFileUploaderOptions = { + const extension = this.mimeUtils.guessExtensionFromUrl(uri); + const mimetype = this.mimeUtils.getMimeType(extension); + const isIOS = this.platform.is('ios'); + const options: CoreFileUploaderOptions = { deleteAfterUpload: !isFromAlbum, mimeType: mimetype }; + const fileName = this.fileProvider.getFileAndDirectoryFromPath(uri).name; if (isIOS && (mimetype == 'image/jpeg' || mimetype == 'image/png')) { // In iOS, the pictures can have repeated names, even if they come from the album. - options.fileName = 'image_' + this.timeUtils.readableTimestamp() + '.' + extension; + // Add a timestamp to the filename to make it unique. + const split = fileName.split('.'); + split[0] += '_' + this.timeUtils.readableTimestamp(); + + options.fileName = split.join('.'); } else { // Use the same name that the file already has. - options.fileName = this.fileProvider.getFileAndDirectoryFromPath(uri).name; + options.fileName = fileName; } if (isFromAlbum) { diff --git a/src/core/fileuploader/providers/helper.ts b/src/core/fileuploader/providers/helper.ts index b812f8014..fb1ac27a0 100644 --- a/src/core/fileuploader/providers/helper.ts +++ b/src/core/fileuploader/providers/helper.ts @@ -128,10 +128,12 @@ export class CoreFileUploaderHelperProvider { * @param defaultExt Defaut extension to use if the file doesn't have any. * @return Promise resolved with the copied file. */ - protected copyToTmpFolder(path: string, shouldDelete: boolean, maxSize?: number, defaultExt?: string): Promise { - let fileName = this.fileProvider.getFileAndDirectoryFromPath(path).name, - promise, - fileTooLarge; + protected copyToTmpFolder(path: string, shouldDelete: boolean, maxSize?: number, defaultExt?: string, + options?: CoreFileUploaderOptions): Promise { + + const fileName = (options && options.fileName) || this.fileProvider.getFileAndDirectoryFromPath(path).name; + let promise; + let fileTooLarge; // Check that size isn't too large. if (typeof maxSize != 'undefined' && maxSize != -1) { @@ -154,9 +156,6 @@ export class CoreFileUploaderHelperProvider { } // File isn't too large. - // Picking an image from album in Android adds a timestamp at the end of the file. Delete it. - fileName = fileName.replace(/(\.[^\.]*)\?[^\.]*$/, '$1'); - // Get a unique name in the folder to prevent overriding another file. return this.fileProvider.getUniqueNameInFolder(CoreFileProvider.TMPFOLDER, fileName, defaultExt); }).then((newName) => { @@ -468,11 +467,13 @@ export class CoreFileUploaderHelperProvider { path = 'file://' + path; } + const options = this.fileUploaderProvider.getMediaUploadOptions(media); + if (upload) { - return this.uploadFile(path, maxSize, true, this.fileUploaderProvider.getMediaUploadOptions(media)); + return this.uploadFile(path, maxSize, true, options); } else { // Copy or move the file to our temporary folder. - return this.copyToTmpFolder(path, true, maxSize); + return this.copyToTmpFolder(path, true, maxSize, undefined, options); } }, (error) => { const defaultError = isAudio ? 'core.fileuploader.errorcapturingaudio' : 'core.fileuploader.errorcapturingvideo'; @@ -552,11 +553,13 @@ export class CoreFileUploaderHelperProvider { return Promise.reject(error); } + const options = this.fileUploaderProvider.getCameraUploadOptions(path, fromAlbum); + if (upload) { - return this.uploadFile(path, maxSize, true, this.fileUploaderProvider.getCameraUploadOptions(path, fromAlbum)); + return this.uploadFile(path, maxSize, true, options); } else { // Copy or move the file to our temporary folder. - return this.copyToTmpFolder(path, !fromAlbum, maxSize, 'jpg'); + return this.copyToTmpFolder(path, !fromAlbum, maxSize, 'jpg', options); } }, (error) => { const defaultError = fromAlbum ? 'core.fileuploader.errorgettingimagealbum' : 'core.fileuploader.errorcapturingimage'; diff --git a/src/core/h5p/classes/content-validator.ts b/src/core/h5p/classes/content-validator.ts index 3d4baac9f..146fb10c1 100644 --- a/src/core/h5p/classes/content-validator.ts +++ b/src/core/h5p/classes/content-validator.ts @@ -503,7 +503,7 @@ export class CoreH5PContentValidator { continue; } - // Find semantics for name=$key + // Find semantics for name=key. let found = false, fn = null, field = null; @@ -729,7 +729,7 @@ export class CoreH5PContentValidator { } if (slash != '') { - return ''; + return ''; } // Is there a closing XHTML slash at the end of the attributes? @@ -871,8 +871,8 @@ export class CoreH5PContentValidator { * Processes an HTML attribute value and strips dangerous protocols from URLs. * * @param str The string with the attribute value. - * @param decode Whether to decode entities in the $string. - * @return Cleaned up and HTML-escaped version of $string. + * @param decode Whether to decode entities in the str. + * @return Cleaned up and HTML-escaped version of str. */ filterXssBadProtocol(str: string, decode: boolean = true): string { // Get the plain text representation of the attribute value (i.e. its meaning).