diff --git a/gulpfile.js b/gulpfile.js
index 717d913d651..c6d3ce48124 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -125,6 +125,10 @@ function treatMergedData(data) {
addProperties(merged, data[filepath], 'mm.core.country-');
+ } else if (filepath.indexOf('core/assets/mimetypes') === 0) {
+
+ addProperties(merged, data[filepath], 'mm.core.mimetype-');
+
}
}
@@ -352,6 +356,7 @@ var paths = {
'./www/core/components/**/lang/',
'./www/addons/**/lang/',
'./www/core/assets/countries/',
+ './www/core/assets/mimetypes/',
'!./www/**/' + remoteAddonPackageFolder + '/*.json',
'!./www/**/' + remoteAddonPackageFolder + '/**/*.json',
],
diff --git a/upgrade.txt b/upgrade.txt
index 60e7bd17501..ec3731e84f7 100644
--- a/upgrade.txt
+++ b/upgrade.txt
@@ -1,6 +1,10 @@
This files describes API changes in the Moodle Mobile app,
information provided here is intended especially for developers.
+=== 3.3.2 ===
+
+ * Handlers registered in $mmFileUploaderDelegate now need to implement a function getSupportedMimeTypes. This function will receive a list of mimetypes and needs to return the ones supported by the handler. Also, the handler's action and afterRender functions now receive a new parameter: mimetypes.
+
=== 3.3 ===
* The project now supports Ionic CLI v2 and Node 6.9. We recommend updating node, npm, Ionic CLI and project dependencies:
diff --git a/www/addons/mod/assign/submission/file/directive.js b/www/addons/mod/assign/submission/file/directive.js
index 96ffcf8ade2..5b64e392949 100644
--- a/www/addons/mod/assign/submission/file/directive.js
+++ b/www/addons/mod/assign/submission/file/directive.js
@@ -22,7 +22,81 @@ angular.module('mm.addons.mod_assign')
* @name mmaModAssignSubmissionFile
*/
.directive('mmaModAssignSubmissionFile', function($mmaModAssign, $mmFileSession, mmaModAssignComponent, $mmaModAssignHelper,
- $mmaModAssignOffline, mmaModAssignSubmissionFileName, $mmFileUploaderHelper, $q) {
+ $mmaModAssignOffline, mmaModAssignSubmissionFileName, $mmFileUploaderHelper, $q, $mmFS) {
+
+ /**
+ * Add a dot to the beginning of an extension.
+ *
+ * @param {String} extension Extension.
+ * @return {String} Treated extension.
+ */
+ function addDot(extension) {
+ return '.' + extension;
+ }
+
+ /**
+ * Parse filetypeslist to get the list of allowed mimetypes and the data to render information.
+ *
+ * @param {Object} scope Directive's scope.
+ * @return {Void}
+ */
+ function treatFileTypes(scope) {
+ var mimetypes = {}, // Use an object to prevent duplicates.
+ filetypes = scope.configs.filetypeslist.replace(/,/g, ';').split(';');
+
+ scope.typesInfo = [];
+
+ angular.forEach(filetypes, function(filetype) {
+ filetype = filetype.trim();
+
+ if (filetype) {
+ if (filetype.indexOf('/') != -1) {
+ // It's a mimetype.
+ mimetypes[filetype] = true;
+
+ scope.typesInfo.push({
+ type: 'mimetype',
+ value: {
+ name: $mmFS.getMimetypeDescription(filetype),
+ extlist: $mmFS.getExtensions(filetype).map(addDot).join(' ')
+ }
+ });
+ } else if (filetype.indexOf('.') === 0) {
+ // It's an extension.
+ var mimetype = $mmFS.getMimeType(filetype);
+ if (mimetype) {
+ mimetypes[mimetype] = true;
+ }
+
+ scope.typesInfo.push({
+ type: 'extension',
+ value: filetype
+ });
+ } else {
+ // It's a group.
+ var groupMimetypes = $mmFS.getGroupMimeInfo(filetype, 'mimetypes'),
+ groupExtensions = $mmFS.getGroupMimeInfo(filetype, 'extensions');
+
+ angular.forEach(groupMimetypes, function(mimetype) {
+ if (mimetype) {
+ mimetypes[mimetype] = true;
+ }
+ });
+
+ scope.typesInfo.push({
+ type: 'mimetype',
+ value: {
+ name: $mmFS.getTranslatedGroupName(filetype),
+ extlist: groupExtensions ? groupExtensions.map(addDot).join(' ') : ''
+ }
+ });
+ }
+ }
+ });
+
+ scope.mimetypes = Object.keys(mimetypes);
+ }
+
return {
restrict: 'A',
priority: 100,
@@ -32,6 +106,10 @@ angular.module('mm.addons.mod_assign')
return;
}
+ if (scope.edit && scope.configs && scope.configs.filetypeslist && scope.configs.filetypeslist.trim()) {
+ treatFileTypes(scope);
+ }
+
// Get the offline data.
$mmaModAssignOffline.getSubmission(scope.assign.id).catch(function() {
// Error getting data, assume there's no offline submission.
diff --git a/www/addons/mod/assign/submission/file/lang/en.json b/www/addons/mod/assign/submission/file/lang/en.json
index 7262ba21715..f0239b984d4 100644
--- a/www/addons/mod/assign/submission/file/lang/en.json
+++ b/www/addons/mod/assign/submission/file/lang/en.json
@@ -1,3 +1,5 @@
{
+ "filesofthesetypes": "Files of these types may be added to the submission:",
+ "filetypewithexts": "{{$a.name}} — {{$a.extlist}}",
"pluginname": "File submissions"
}
\ No newline at end of file
diff --git a/www/addons/mod/assign/submission/file/template.html b/www/addons/mod/assign/submission/file/template.html
index 08de6c3caa6..6af89e936ee 100644
--- a/www/addons/mod/assign/submission/file/template.html
+++ b/www/addons/mod/assign/submission/file/template.html
@@ -1,7 +1,16 @@
{{plugin.name}}
-
+
+
+
{{ 'mma.mod_assign_submission_file.filesofthesetypes' | translate }}
+
+ -
+
{{typeInfo.value}}
+
+
+
+
diff --git a/www/core/assets/mimetypes/en.json b/www/core/assets/mimetypes/en.json
new file mode 100644
index 00000000000..f94347be44f
--- /dev/null
+++ b/www/core/assets/mimetypes/en.json
@@ -0,0 +1,54 @@
+{
+ "application/epub_zip": "EPUB ebook",
+ "application/msword": "Word document",
+ "application/pdf": "PDF document",
+ "application/vnd.moodle.backup": "Moodle backup",
+ "application/vnd.ms-excel": "Excel spreadsheet",
+ "application/vnd.ms-excel.sheet.macroEnabled.12": "Excel 2007 macro-enabled workbook",
+ "application/vnd.ms-powerpoint": "Powerpoint presentation",
+ "application/vnd.oasis.opendocument.spreadsheet": "OpenDocument Spreadsheet",
+ "application/vnd.oasis.opendocument.spreadsheet-template": "OpenDocument Spreadsheet template",
+ "application/vnd.oasis.opendocument.text": "OpenDocument Text document",
+ "application/vnd.oasis.opendocument.text-template": "OpenDocument Text template",
+ "application/vnd.oasis.opendocument.text-web": "OpenDocument Web page template",
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": "Powerpoint 2007 presentation",
+ "application/vnd.openxmlformats-officedocument.presentationml.slideshow": "Powerpoint 2007 slideshow",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "Excel 2007 spreadsheet",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.template": "Excel 2007 template",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "Word 2007 document",
+ "application/x-iwork-keynote-sffkey": "iWork Keynote presentation",
+ "application/x-iwork-numbers-sffnumbers": "iWork Numbers spreadsheet",
+ "application/x-iwork-pages-sffpages": "iWork Pages document",
+ "application/x-javascript": "JavaScript source",
+ "application/x-mspublisher": "Publisher document",
+ "application/x-shockwave-flash": "Flash animation",
+ "application/xhtml_xml": "XHTML document",
+ "archive": "Archive ({{$a.EXT}})",
+ "audio": "Audio file ({{$a.EXT}})",
+ "default": "{{$a.mimetype}}",
+ "document/unknown": "File",
+ "group:archive": "Archive files",
+ "group:audio": "Audio files",
+ "group:document": "Document files",
+ "group:html_audio": "Audio files natively supported by browsers",
+ "group:html_track": "HTML track files",
+ "group:html_video": "Video files natively supported by browsers",
+ "group:image": "Image files",
+ "group:presentation": "Presentation files",
+ "group:sourcecode": "Source code",
+ "group:spreadsheet": "Spreadsheet files",
+ "group:video": "Video files",
+ "group:web_audio": "Audio files used on the web",
+ "group:web_file": "Web files",
+ "group:web_image": "Image files used on the web",
+ "group:web_video": "Video files used on the web",
+ "image": "Image ({{$a.MIMETYPE2}})",
+ "image/vnd.microsoft.icon": "Windows icon",
+ "text/css": "Cascading Style-Sheet",
+ "text/csv": "Comma-separated values",
+ "text/html": "HTML document",
+ "text/plain": "Text file",
+ "text/rtf": "RTF document",
+ "text/vtt": "Web Video Text Track",
+ "video": "Video file ({{$a.EXT}})"
+}
\ No newline at end of file
diff --git a/www/core/components/emulator/services/mediacapture.js b/www/core/components/emulator/services/mediacapture.js
index 98d5e34557d..014b68a0f01 100644
--- a/www/core/components/emulator/services/mediacapture.js
+++ b/www/core/components/emulator/services/mediacapture.js
@@ -59,7 +59,8 @@ angular.module('mm.core.emulator')
extension,
quality = 0.92, // Image only.
returnData = false, // Image only.
- isCaptureImage = false; // To identify if it's capturing an image using media capture plugin (instead of camera).
+ isCaptureImage = false, // To identify if it's capturing an image using media capture plugin (instead of camera).
+ mimeAndExt;
loadingModal = $mmUtil.showModalLoading();
@@ -72,13 +73,17 @@ angular.module('mm.core.emulator')
if (type == 'video') {
scope.isVideo = true;
title = 'mm.core.capturevideo';
- mimetype = videoMimeType;
- extension = possibleVideoMimeTypes[mimetype];
+
+ mimeAndExt = getMimeTypeAndExtension(type, options.mimetypes);
+ mimetype = mimeAndExt.mimetype;
+ extension = mimeAndExt.extension;
} else if (type == 'audio') {
scope.isAudio = true;
title = 'mm.core.captureaudio';
- mimetype = audioMimeType;
- extension = possibleAudioMimeTypes[mimetype];
+
+ mimeAndExt = getMimeTypeAndExtension(type, options.mimetypes);
+ mimetype = mimeAndExt.mimetype;
+ extension = mimeAndExt.extension;
} else if (type == 'image') {
scope.isImage = true;
title = 'mm.core.captureimage';
@@ -338,6 +343,45 @@ angular.module('mm.core.emulator')
}
}
+ /**
+ * Get the mimetype and extension to capture media.
+ *
+ * @param {String} type Type of media: image, audio, video.
+ * @param {String[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
+ * @return {Object} An object with mimetype and extension to use.
+ */
+ function getMimeTypeAndExtension(type, mimetypes) {
+ var result = {};
+
+ if (mimetypes && mimetypes.length) {
+ // Search for a supported mimetype.
+ for (var i = 0; i < mimetypes.length; i++) {
+ var mimetype = mimetypes[i],
+ matches = mimetype.match(new RegExp('^' + type + '/'));
+
+ if (matches && matches.length && MediaRecorder.isTypeSupported(mimetype)) {
+ result.mimetype = mimetype;
+ break;
+ }
+ }
+ }
+
+ if (result.mimetype) {
+ // Found a supported mimetype in the mimetypes array, get the extension.
+ result.extension = $mmFS.getExtension(result.mimetype);
+ } else if (type == 'video') {
+ // No mimetype found, use default extension.
+ result.mimetype = videoMimeType;
+ result.extension = possibleVideoMimeTypes[result.mimetype];
+ } else if (type == 'audio') {
+ // No mimetype found, use default extension.
+ result.mimetype = audioMimeType;
+ result.extension = possibleAudioMimeTypes[result.mimetype];
+ }
+
+ return result;
+ }
+
/**
* Initialize the audio drawer. This code has been extracted from MDN's example on MediaStream Recording:
* https://github.com/mdn/web-dictaphone
diff --git a/www/core/components/fileuploader/lang/en.json b/www/core/components/fileuploader/lang/en.json
index d42abc6a3c8..041d6469017 100644
--- a/www/core/components/fileuploader/lang/en.json
+++ b/www/core/components/fileuploader/lang/en.json
@@ -14,6 +14,7 @@
"errorwhileuploading": "An error occurred during the file upload.",
"file": "File",
"fileuploaded": "The file was successfully uploaded.",
+ "invalidfiletype": "{{$a}} filetype cannot be accepted.",
"maxbytesfile": "The file {{$a.file}} is too large. The maximum size you can upload is {{$a.size}}.",
"photoalbums": "Photo albums",
"readingfile": "Reading file",
diff --git a/www/core/components/fileuploader/services/delegate.js b/www/core/components/fileuploader/services/delegate.js
index 790210ef9aa..cbc488c14df 100644
--- a/www/core/components/fileuploader/services/delegate.js
+++ b/www/core/components/fileuploader/services/delegate.js
@@ -36,16 +36,18 @@ angular.module('mm.core.fileuploader')
* returning an object defining these functions. See {@link $mmUtil#resolveObject}.
* - isEnabled (Boolean|Promise) Whether or not the handler is enabled on a site level.
* When using a promise, it should return a boolean.
+ * - getSupportedMimeTypes(mimetypes) (String[]) Given a list of mimetypes, return the ones
+ * that are supported by the handler.
* - getData (Object) Returns an object with the data to display the handler. Accepted properties:
* * name Required. A name to identify the handler. Allows filtering it.
* * class Optional. Class to add to the handler's row.
* * title Required. Title to show in the handler's row.
* * icon Optional. Icon to show in the handler's row.
- * * afterRender(maxSize, upload, allowOffline) Optional. Called when the handler is
- * rendered.
- * * action(maxSize, upload, allowOffline) Required. A function called when the handler
- * is clicked. It must return an object - or a promise resolved with an object -
- * containing these properties:
+ * * afterRender(maxSize, upload, allowOffline, mimetypes) Optional. Called when the
+ * handler is rendered.
+ * * action(maxSize, upload, allowOffline, mimetypes) Required. A function called when
+ * the handler is clicked. It must return an object - or a promise resolved with an
+ * object - containing these properties:
* - uploaded Boolean. Whether the handler uploaded or treated the file.
* - path String. Ignored if uploaded=true. The path of the file to upload.
* - fileEntry Object. Ignored if uploaded=true. The fileEntry to upload.
@@ -93,14 +95,32 @@ angular.module('mm.core.fileuploader')
* @module mm.core.fileuploader
* @ngdoc method
* @name $mmFileUploaderDelegate#getHandlers
+ * @param {String[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
* @return {Promise} Resolved with an array of objects containing 'priority' and the handler data.
*/
- self.getHandlers = function() {
+ self.getHandlers = function(mimetypes) {
var handlers = [];
angular.forEach(enabledHandlers, function(handler) {
+ var supportedMimetypes;
+
+ if (mimetypes) {
+ if (!handler.instance.getSupportedMimeTypes) {
+ // Handler doesn't implement a required function, don't add it.
+ return;
+ }
+
+ supportedMimetypes = handler.instance.getSupportedMimeTypes(mimetypes);
+
+ if (!supportedMimetypes.length) {
+ // Handler doesn't support any mimetype, don't add it.
+ return;
+ }
+ }
+
var data = handler.instance.getData();
data.priority = handler.priority;
+ data.mimetypes = supportedMimetypes;
handlers.push(data);
});
diff --git a/www/core/components/fileuploader/services/handlers.js b/www/core/components/fileuploader/services/handlers.js
index 1cc6ea3e1d6..707310044d3 100644
--- a/www/core/components/fileuploader/services/handlers.js
+++ b/www/core/components/fileuploader/services/handlers.js
@@ -21,7 +21,7 @@ angular.module('mm.core.fileuploader')
* @ngdoc service
* @name $mmFileUploaderHandlers
*/
-.factory('$mmFileUploaderHandlers', function($mmFileUploaderHelper, $rootScope, $compile, $mmUtil, $mmApp) {
+.factory('$mmFileUploaderHandlers', function($mmFileUploaderHelper, $rootScope, $compile, $mmUtil, $mmApp, $translate, $mmFS) {
var self = {};
@@ -56,8 +56,8 @@ angular.module('mm.core.fileuploader')
title: 'mm.fileuploader.photoalbums',
class: 'mm-fileuploader-album-handler',
icon: 'ion-images',
- action: function(maxSize, upload, allowOffline) {
- return $mmFileUploaderHelper.uploadImage(true, maxSize, upload).then(function(result) {
+ action: function(maxSize, upload, allowOffline, mimetypes) {
+ return $mmFileUploaderHelper.uploadImage(true, maxSize, upload, mimetypes).then(function(result) {
return {
uploaded: true,
result: result
@@ -67,6 +67,17 @@ angular.module('mm.core.fileuploader')
};
};
+ /**
+ * Given a list of mimetypes, return the ones supported by this handler.
+ *
+ * @param {String[]} mimetypes List of mimetypes.
+ * @return {String[]} Supported mimetypes.
+ */
+ self.getSupportedMimeTypes = function(mimetypes) {
+ // Album allows picking images and videos.
+ return $mmUtil.filterByRegexp(mimetypes, /^(image|video)\//);
+ };
+
return self;
};
@@ -101,8 +112,8 @@ angular.module('mm.core.fileuploader')
title: 'mm.fileuploader.camera',
class: 'mm-fileuploader-camera-handler',
icon: 'ion-camera',
- action: function(maxSize, upload, allowOffline) {
- return $mmFileUploaderHelper.uploadImage(false, maxSize, upload).then(function(result) {
+ action: function(maxSize, upload, allowOffline, mimetypes) {
+ return $mmFileUploaderHelper.uploadImage(false, maxSize, upload, mimetypes).then(function(result) {
return {
uploaded: true,
result: result
@@ -112,6 +123,17 @@ angular.module('mm.core.fileuploader')
};
};
+ /**
+ * Given a list of mimetypes, return the ones supported by this handler.
+ *
+ * @param {String[]} mimetypes List of mimetypes.
+ * @return {String[]} Supported mimetypes.
+ */
+ self.getSupportedMimeTypes = function(mimetypes) {
+ // Camera only supports JPEG and PNG.
+ return $mmUtil.filterByRegexp(mimetypes, /^image\/(jpeg|png)$/);
+ };
+
return self;
};
@@ -146,8 +168,8 @@ angular.module('mm.core.fileuploader')
title: 'mm.fileuploader.audio',
class: 'mm-fileuploader-audio-handler',
icon: 'ion-mic-a',
- action: function(maxSize, upload, allowOffline) {
- return $mmFileUploaderHelper.uploadAudioOrVideo(true, maxSize, upload).then(function(result) {
+ action: function(maxSize, upload, allowOffline, mimetypes) {
+ return $mmFileUploaderHelper.uploadAudioOrVideo(true, maxSize, upload, mimetypes).then(function(result) {
return {
uploaded: true,
result: result
@@ -157,6 +179,32 @@ angular.module('mm.core.fileuploader')
};
};
+ /**
+ * Given a list of mimetypes, return the ones supported by this handler.
+ *
+ * @param {String[]} mimetypes List of mimetypes.
+ * @return {String[]} Supported mimetypes.
+ */
+ self.getSupportedMimeTypes = function(mimetypes) {
+ if (ionic.Platform.isIOS()) {
+ // iOS records as WAV.
+ return $mmUtil.filterByRegexp(mimetypes, /^audio\/wav$/);
+ } else if (ionic.Platform.isAndroid()) {
+ // In Android we don't know the format the audio will be recorded, so accept any audio mimetype.
+ return $mmUtil.filterByRegexp(mimetypes, /^audio\//);
+ } else {
+ // In desktop, support audio formats that are supported by MediaRecorder.
+ if (MediaRecorder) {
+ return mimetypes.filter(function(type) {
+ var matches = type.match(/^audio\//);
+ return matches && matches.length && MediaRecorder.isTypeSupported(type);
+ });
+ }
+ }
+
+ return [];
+ };
+
return self;
};
@@ -191,8 +239,8 @@ angular.module('mm.core.fileuploader')
title: 'mm.fileuploader.video',
class: 'mm-fileuploader-video-handler',
icon: 'ion-ios-videocam',
- action: function(maxSize, upload, allowOffline) {
- return $mmFileUploaderHelper.uploadAudioOrVideo(false, maxSize, upload).then(function(result) {
+ action: function(maxSize, upload, allowOffline, mimetypes) {
+ return $mmFileUploaderHelper.uploadAudioOrVideo(false, maxSize, upload, mimetypes).then(function(result) {
return {
uploaded: true,
result: result
@@ -202,6 +250,32 @@ angular.module('mm.core.fileuploader')
};
};
+ /**
+ * Given a list of mimetypes, return the ones supported by this handler.
+ *
+ * @param {String[]} mimetypes List of mimetypes.
+ * @return {String[]} Supported mimetypes.
+ */
+ self.getSupportedMimeTypes = function(mimetypes) {
+ if (ionic.Platform.isIOS()) {
+ // iOS records as MOV.
+ return $mmUtil.filterByRegexp(mimetypes, /^video\/quicktime$/);
+ } else if (ionic.Platform.isAndroid()) {
+ // In Android we don't know the format the video will be recorded, so accept any video mimetype.
+ return $mmUtil.filterByRegexp(mimetypes, /^video\//);
+ } else {
+ // In desktop, support video formats that are supported by MediaRecorder.
+ if (MediaRecorder) {
+ return mimetypes.filter(function(type) {
+ var matches = type.match(/^video\//);
+ return matches && matches.length && MediaRecorder.isTypeSupported(type);
+ });
+ }
+ }
+
+ return [];
+ };
+
return self;
};
@@ -237,12 +311,16 @@ angular.module('mm.core.fileuploader')
title: 'mm.fileuploader.file',
class: 'mm-fileuploader-file-handler',
icon: 'ion-folder',
- afterRender: function(maxSize, upload, allowOffline) {
+ afterRender: function(maxSize, upload, allowOffline, mimetypes) {
// Add an invisible file input in the file handler.
// It needs to be done like this because button text doesn't accept inputs.
var element = document.querySelector('.mm-fileuploader-file-handler');
if (element) {
var input = angular.element('');
+ if (mimetypes && mimetypes.length && (!ionic.Platform.isAndroid() || mimetypes.length === 1)) {
+ // Don't use accept attribute in Android with several mimetypes, it's not supported.
+ input.attr('accept', mimetypes.join(', '));
+ }
if (!uploadFileScope) {
// Create a scope for the on change directive.
@@ -257,6 +335,13 @@ angular.module('mm.core.fileuploader')
return;
}
+ // Verify that the mimetype of the file is supported, in case the accept attribute isn't supported.
+ var error = $mmFileUploaderHelper.isInvalidMimetype(mimetypes, file.name, file.type);
+ if (error) {
+ $mmUtil.showErrorModal(error);
+ return;
+ }
+
// Upload the picked file.
$mmFileUploaderHelper.uploadFileObject(file, maxSize, upload, allowOffline).then(function(result) {
$mmFileUploaderHelper.fileUploaded(result);
@@ -275,6 +360,16 @@ angular.module('mm.core.fileuploader')
};
};
+ /**
+ * Given a list of mimetypes, return the ones supported by this handler.
+ *
+ * @param {String[]} mimetypes List of mimetypes.
+ * @return {String[]} Supported mimetypes.
+ */
+ self.getSupportedMimeTypes = function(mimetypes) {
+ return mimetypes;
+ };
+
return self;
};
diff --git a/www/core/components/fileuploader/services/helper.js b/www/core/components/fileuploader/services/helper.js
index 8e3bdc60cd6..e799e7ba7e8 100644
--- a/www/core/components/fileuploader/services/helper.js
+++ b/www/core/components/fileuploader/services/helper.js
@@ -228,6 +228,38 @@ angular.module('mm.core.fileuploader')
});
};
+ /**
+ * Check if a file's mimetype is invalid based on the list of accepted mimetypes. This function needs either the file's
+ * mimetype or the file's path/name.
+ *
+ * @module mm.core.fileuploader
+ * @ngdoc method
+ * @name $mmFileUploaderHelper#isInvalidMimetype
+ * @param {String[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
+ * @param {String} [path] File's path or name.
+ * @param {String} [mimetype] File's mimetype.
+ * @return {Mixed} False if file is valid, error message if file is invalid.
+ */
+ self.isInvalidMimetype = function(mimetypes, path, mimetype) {
+ var extension;
+
+ if (mimetypes) {
+ // Verify that the mimetype of the file is supported.
+ if (mimetype) {
+ extension = $mmFS.getExtension(mimetype);
+ } else {
+ extension = $mmFS.getFileExtension(path);
+ mimetype = $mmFS.getMimeType(extension);
+ }
+
+ if (mimetype && mimetypes.indexOf(mimetype) == -1) {
+ return $translate.instant('mm.fileuploader.invalidfiletype', {$a: extension});
+ }
+ }
+
+ return false;
+ };
+
/**
* Mark files as offline.
*
@@ -252,14 +284,15 @@ angular.module('mm.core.fileuploader')
* @module mm.core.fileuploader
* @ngdoc method
* @name $mmFileUploaderHelper#selectAndUploadFile
- * @param {Number} [maxSize] Max size of the file to upload. If not defined or -1, no max size.
- * @param {String} [title] File picker page title
- * @param {Array} [filterMethods] File picker available methods
+ * @param {Number} [maxSize] Max size of the file to upload. If not defined or -1, no max size.
+ * @param {String} [title] File picker page title
+ * @param {Array} [filterMethods] File picker available methods
+ * @param {String[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
* @return {Promise} Promise resolved when a file is uploaded, rejected if file picker is closed without a file uploaded.
* The resolve value should be the response of the upload request.
*/
- self.selectAndUploadFile = function(maxSize, title, filterMethods) {
- return selectFile(maxSize, false, title, filterMethods, true);
+ self.selectAndUploadFile = function(maxSize, title, filterMethods, mimetypes) {
+ return selectFile(maxSize, false, title, filterMethods, true, mimetypes);
};
/**
@@ -268,36 +301,38 @@ angular.module('mm.core.fileuploader')
* @module mm.core.fileuploader
* @ngdoc method
* @name $mmFileUploaderHelper#selectFile
- * @param {Number} [maxSize] Max size of the file. If not defined or -1, no max size.
- * @param {Boolean} allowOffline True to allow selecting in offline, false to require connection.
- * @param {String} [title] File picker page title
- * @param {Array} [filterMethods] File picker available methods
+ * @param {Number} [maxSize] Max size of the file. If not defined or -1, no max size.
+ * @param {Boolean} [allowOffline] True to allow selecting in offline, false to require connection.
+ * @param {String} [title] File picker page title
+ * @param {Array} [filterMethods] File picker available methodss
+ * @param {String[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
* @return {Promise} Promise resolved when a file is selected, rejected if file picker is closed without selecting a file.
* The resolve value should be the FileEntry of a copy of the picked file, so it can be deleted afterwards.
*/
- self.selectFile = function(maxSize, allowOffline, title, filterMethods) {
- return selectFile(maxSize, allowOffline, title, filterMethods, false);
+ self.selectFile = function(maxSize, allowOffline, title, filterMethods, mimetypes) {
+ return selectFile(maxSize, allowOffline, title, filterMethods, false, mimetypes);
};
/**
* Open the view to select a file and maybe uploading it.
*
* @param {Number} [maxSize] Max size of the file. If not defined or -1, no max size.
- * @param {Boolean} allowOffline True to allow selecting in offline, false to require connection.
+ * @param {Boolean} [allowOffline] True to allow selecting in offline, false to require connection.
* @param {String} [title] File picker title.
* @param {Array} [filterMethods] File picker available methods.
- * @param {Boolean} upload True if the file should be uploaded, false if only picked.
+ * @param {Boolean} [upload] True if the file should be uploaded, false if only picked.
+ * @param {String[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
* @return {Promise} Promise resolved when a file is selected, rejected if file picker is closed without selecting a file.
* The resolve value should be the FileEntry of a copy of the picked file, so it can be deleted afterwards.
*/
- function selectFile(maxSize, allowOffline, title, filterMethods, upload) {
+ function selectFile(maxSize, allowOffline, title, filterMethods, upload, mimetypes) {
var buttons = [],
handlers;
filePickerDeferred = $q.defer();
// Add buttons for handlers.
- handlers = $mmFileUploaderDelegate.getHandlers();
+ handlers = $mmFileUploaderDelegate.getHandlers(mimetypes);
handlers.sort(function(a, b) {
return a.priority <= b.priority ? 1 : -1;
});
@@ -312,7 +347,8 @@ angular.module('mm.core.fileuploader')
text: (handler.icon ? '' : '') + $translate.instant(handler.title),
action: handler.action,
className: handler.class,
- afterRender: handler.afterRender
+ afterRender: handler.afterRender,
+ mimetypes: handler.mimetypes
});
});
@@ -329,7 +365,7 @@ angular.module('mm.core.fileuploader')
}
// Execute the action and close the action sheet.
- buttons[index].action(maxSize, upload, allowOffline).then(function(data) {
+ buttons[index].action(maxSize, upload, allowOffline, buttons[index].mimetypes).then(function(data) {
if (data.uploaded) {
// The handler already uploaded the file. Return the result.
return data.result;
@@ -377,7 +413,7 @@ angular.module('mm.core.fileuploader')
$timeout(function() {
angular.forEach(buttons, function(button) {
if (angular.isFunction(button.afterRender)) {
- button.afterRender(maxSize, upload, allowOffline);
+ button.afterRender(maxSize, upload, allowOffline, button.mimetypes);
}
});
}, 500);
@@ -420,19 +456,27 @@ angular.module('mm.core.fileuploader')
* @module mm.core.fileuploader
* @ngdoc method
* @name $mmFileUploaderHelper#uploadAudioOrVideo
- * @param {Boolean} isAudio True if uploading an audio, false if it's a video.
- * @param {Number} maxSize Max size of the upload. -1 for no max size.
- * @param {Boolean} upload True if the file should be uploaded, false to return the picked file.
- * @return {Promise} The reject contains the error message, if there is no error message
- * then we can consider that this is a silent fail.
+ * @param {Boolean} isAudio True if uploading an audio, false if it's a video.
+ * @param {Number} maxSize Max size of the upload. -1 for no max size.
+ * @param {Boolean} upload True if the file should be uploaded, false to return the picked file.
+ * @param {String[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
+ * @return {Promise} The reject contains the error message, if there is no error message
+ * then we can consider that this is a silent fail.
*/
- self.uploadAudioOrVideo = function(isAudio, maxSize, upload) {
+ self.uploadAudioOrVideo = function(isAudio, maxSize, upload, mimetypes) {
$log.debug('Trying to record a video file');
var fn = isAudio ? $cordovaCapture.captureAudio : $cordovaCapture.captureVideo;
- return fn({limit: 1}).then(function(medias) {
+
+ // The mimetypes param is only for desktop apps, the Cordova plugin doesn't support it.
+ return fn({limit: 1, mimetypes: mimetypes}).then(function(medias) {
// We used limit 1, we only want 1 media.
var media = medias[0],
- path = media.localURL || media.toURL();
+ path = media.localURL || media.toURL(),
+ error = self.isInvalidMimetype(mimetypes, path); // Verify that the mimetype of the file is supported.
+
+ if (error) {
+ return $q.reject(error);
+ }
if (upload) {
return uploadFile(true, path, maxSize, true, $mmFileUploader.uploadMedia, media);
@@ -472,13 +516,14 @@ angular.module('mm.core.fileuploader')
* @module mm.core.fileuploader
* @ngdoc method
* @name $mmFileUploaderHelper#uploadImage
- * @param {Boolean} fromAlbum True if the image should be selected from album, false if it should be taken with camera.
- * @param {Number} maxSize Max size of the upload. -1 for no max size.
- * @param {Boolean} upload True if the image should be uploaded, false to return the picked file.
- * @return {Promise} The reject contains the error message, if there is no error message
- * then we can consider that this is a silent fail.
+ * @param {Boolean} fromAlbum True if the image should be selected from album, false if it should be taken with camera.
+ * @param {Number} maxSize Max size of the upload. -1 for no max size.
+ * @param {Boolean} upload True if the image should be uploaded, false to return the picked file.
+ * @param {String[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
+ * @return {Promise} The reject contains the error message, if there is no error message
+ * then we can consider that this is a silent fail.
*/
- self.uploadImage = function(fromAlbum, maxSize, upload) {
+ self.uploadImage = function(fromAlbum, maxSize, upload, mimetypes) {
$log.debug('Trying to capture an image with camera');
var options = {
quality: 50,
@@ -487,16 +532,36 @@ angular.module('mm.core.fileuploader')
};
if (fromAlbum) {
+ var imageSupported = !mimetypes || $mmUtil.indexOfRegexp(mimetypes, /^image\//) > -1,
+ videoSupported = !mimetypes || $mmUtil.indexOfRegexp(mimetypes, /^video\//) > -1;
+
options.sourceType = navigator.camera.PictureSourceType.PHOTOLIBRARY;
options.popoverOptions = new CameraPopoverOptions(10, 10, $window.innerWidth - 200, $window.innerHeight - 200,
Camera.PopoverArrowDirection.ARROW_ANY);
- if (ionic.Platform.isIOS()) {
+
+ // Determine the mediaType based on the mimetypes.
+ if (imageSupported && !videoSupported) {
+ options.mediaType = Camera.MediaType.PICTURE;
+ } else if (!imageSupported && videoSupported) {
+ options.mediaType = Camera.MediaType.VIDEO;
+ } else if (ionic.Platform.isIOS()) {
// Only get all media in iOS because in Android using this option allows uploading any kind of file.
options.mediaType = Camera.MediaType.ALLMEDIA;
}
+ } else if (mimetypes) {
+ if (mimetypes.indexOf('image/jpeg') > -1) {
+ options.encodingType = Camera.EncodingType.JPEG;
+ } else if (mimetypes.indexOf('image/png') > -1) {
+ options.encodingType = Camera.EncodingType.PNG;
+ }
}
return $cordovaCamera.getPicture(options).then(function(path) {
+ var error = self.isInvalidMimetype(mimetypes, path); // Verify that the mimetype of the file is supported.
+ if (error) {
+ return $q.reject(error);
+ }
+
if (upload) {
return uploadFile(!fromAlbum, path, maxSize, true, $mmFileUploader.uploadImage, path, fromAlbum);
} else {
diff --git a/www/core/components/sharedfiles/services/handlers.js b/www/core/components/sharedfiles/services/handlers.js
index db9c700bb26..d183974ca05 100644
--- a/www/core/components/sharedfiles/services/handlers.js
+++ b/www/core/components/sharedfiles/services/handlers.js
@@ -57,14 +57,24 @@ angular.module('mm.core.sharedfiles')
title: 'mm.sharedfiles.sharedfiles',
class: 'mm-sharedfiles-filepicker-handler',
icon: 'ion-folder',
- action: function(maxSize, upload, allowOffline) {
+ action: function(maxSize, upload, allowOffline, mimetypes) {
// We don't use the params because we aren't uploading the file ourselves, we return
// the file to upload to the fileuploader.
- return $mmSharedFilesHelper.pickSharedFile();
+ return $mmSharedFilesHelper.pickSharedFile(mimetypes);
}
};
};
+ /**
+ * Given a list of mimetypes, return the ones supported by this handler.
+ *
+ * @param {String[]} mimetypes List of mimetypes.
+ * @return {String[]} Supported mimetypes.
+ */
+ self.getSupportedMimeTypes = function(mimetypes) {
+ return mimetypes;
+ };
+
return self;
};
diff --git a/www/core/components/sharedfiles/services/helper.js b/www/core/components/sharedfiles/services/helper.js
index 926a85f40ac..78bb7e47fa9 100644
--- a/www/core/components/sharedfiles/services/helper.js
+++ b/www/core/components/sharedfiles/services/helper.js
@@ -15,7 +15,7 @@
angular.module('mm.core.sharedfiles')
.factory('$mmSharedFilesHelper', function($mmSharedFiles, $mmUtil, $log, $mmApp, $mmSitesManager, $mmFS, $rootScope, $q,
- $ionicModal, $state, $translate, $mmSite) {
+ $ionicModal, $state, $translate, $mmSite, $mmFileUploaderHelper) {
$log = $log.getInstance('$mmSharedFilesHelper');
@@ -76,11 +76,12 @@ angular.module('mm.core.sharedfiles')
* @module mm.core.sharedfiles
* @ngdoc method
* @name $mmSharedFilesHelper#filePickerClosed
+ * @param {String} [error] The error message if any.
* @return {Void}
*/
- self.filePickerClosed = function() {
+ self.filePickerClosed = function(error) {
if (filePickerDeferred) {
- filePickerDeferred.reject();
+ filePickerDeferred.reject(error);
filePickerDeferred = undefined;
}
};
@@ -150,9 +151,10 @@ angular.module('mm.core.sharedfiles')
* @module mm.core.sharedfiles
* @ngdoc method
* @name $mmSharedFilesHelper#pickSharedFile
+ * @param {String[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
* @return {Promise} Promise resolved when a file is picked, rejected if file picker is closed without selecting a file.
*/
- self.pickSharedFile = function() {
+ self.pickSharedFile = function(mimetypes) {
var path = '',
siteId = $mmSite.getId();
@@ -160,6 +162,7 @@ angular.module('mm.core.sharedfiles')
self.initFileListModal().then(function() {
fileListScope.filesLoaded = false;
+ // fileListScope.mimetypes = mimetypes;
if (path) {
fileListScope.title = $mmFS.getFileAndDirectoryFromPath(path).name;
} else {
@@ -199,8 +202,15 @@ angular.module('mm.core.sharedfiles')
// File picked.
fileListScope.filePicked = function(file) {
- self.filePicked(file.fullPath);
fileListScope.modal.hide();
+
+ var error = $mmFileUploaderHelper.isInvalidMimetype(mimetypes, file.fullPath);
+ if (error) {
+ self.filePickerClosed(error);
+ return;
+ }
+
+ self.filePicked(file.fullPath);
};
});
@@ -212,7 +222,7 @@ angular.module('mm.core.sharedfiles')
return filePickerDeferred.promise;
function loadFiles() {
- return $mmSharedFiles.getSiteSharedFiles(siteId, path).then(function(files) {
+ return $mmSharedFiles.getSiteSharedFiles(siteId, path, mimetypes).then(function(files) {
fileListScope.files = files;
fileListScope.filesLoaded = true;
});
diff --git a/www/core/components/sharedfiles/services/sharedfiles.js b/www/core/components/sharedfiles/services/sharedfiles.js
index 1792ac28f24..09c4ecfaa91 100644
--- a/www/core/components/sharedfiles/services/sharedfiles.js
+++ b/www/core/components/sharedfiles/services/sharedfiles.js
@@ -133,11 +133,12 @@ angular.module('mm.core.sharedfiles')
* @module mm.core.sharedfiles
* @ngdoc method
* @name $mmSharedFiles#getSiteSharedFiles
- * @param {String} [siteId] Site ID. If not defined, current site.
- * @param {String} [path] Path to search inside the site shared folder.
- * @return {Promise} Promise resolved with the files.
+ * @param {String} [siteId] Site ID. If not defined, current site.
+ * @param {String} [path] Path to search inside the site shared folder.
+ * @param {String[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
+ * @return {Promise} Promise resolved with the files.
*/
- self.getSiteSharedFiles = function(siteId, path) {
+ self.getSiteSharedFiles = function(siteId, path, mimetypes) {
siteId = siteId || $mmSite.getId();
var pathToGet = self.getSiteSharedFilesDirPath(siteId);
@@ -145,7 +146,19 @@ angular.module('mm.core.sharedfiles')
pathToGet = $mmFS.concatenatePaths(pathToGet, path);
}
- return $mmFS.getDirectoryContents(pathToGet).catch(function() {
+ return $mmFS.getDirectoryContents(pathToGet).then(function(files) {
+ if (mimetypes) {
+ // Only show files with the right mimetype and the ones we cannot determine the mimetype.
+ files = files.filter(function(file) {
+ var extension = $mmFS.getFileExtension(file.name),
+ mimetype = $mmFS.getMimeType(extension);
+
+ return !mimetype || mimetypes.indexOf(mimetype) > -1;
+ });
+ }
+
+ return files;
+ }).catch(function() {
// Directory not found, return empty list.
return [];
});
diff --git a/www/core/directives/attachments.js b/www/core/directives/attachments.js
index 075c1f3af1a..3fd2b755a7c 100644
--- a/www/core/directives/attachments.js
+++ b/www/core/directives/attachments.js
@@ -33,7 +33,7 @@ angular.module('mm.core')
* Example usage:
*
*
+ * component="{{component}}" component-id="{{assign.id}}" mimetypes="mimetypes">
*
* Parameters accepted:
*
@@ -43,6 +43,7 @@ angular.module('mm.core')
* @param {String} [component] Component the downloaded files will be linked to.
* @param {Mixed} [componentId] Component ID the downloaded files will be linked to.
* @param {Boolean} [allowOffline] True to allow selecting files in offline.
+ * @param {String[]} [mimetypes] List of supported mimetypes. If undefined, all mimetypes supported.
*/
.directive('mmAttachments', function($mmText, $translate, $ionicScrollDelegate, $mmUtil, $mmApp, $mmFileUploaderHelper, $q) {
return {
@@ -55,7 +56,8 @@ angular.module('mm.core')
maxSubmissions: '@?',
component: '@?',
componentId: '@?',
- allowOffline: '@?'
+ allowOffline: '@?',
+ mimetypes: '=?'
},
link: function(scope) {
var allowOffline = scope.allowOffline && scope.allowOffline !== 'false';
@@ -77,7 +79,8 @@ angular.module('mm.core')
if (!allowOffline && !$mmApp.isOnline()) {
$mmUtil.showErrorModal('mm.fileuploader.errormustbeonlinetoupload', true);
} else {
- return $mmFileUploaderHelper.selectFile(maxSize, allowOffline).then(function(result) {
+ return $mmFileUploaderHelper.selectFile(maxSize, allowOffline, undefined, undefined, scope.mimetypes)
+ .then(function(result) {
scope.files.push(result);
});
}
diff --git a/www/core/lib/fs.js b/www/core/lib/fs.js
index 30f4435d8ba..1af868ab556 100644
--- a/www/core/lib/fs.js
+++ b/www/core/lib/fs.js
@@ -25,7 +25,7 @@ angular.module('mm.core')
* This service handles the interaction with the FileSystem.
*/
.factory('$mmFS', function($ionicPlatform, $cordovaFile, $log, $q, $http, $cordovaZip, $mmText, mmFsSitesFolder, mmFsTmpFolder,
- $mmApp) {
+ $mmApp, $translate) {
$log = $log.getInstance('$mmFS');
@@ -35,6 +35,7 @@ angular.module('mm.core')
isHTMLAPI = false,
extToMime = {},
mimeToExt = {},
+ groupsMimeInfo = {},
extensionRegex = new RegExp('^[a-z0-9]+$');
// Loading extensions to mimetypes file.
@@ -916,8 +917,7 @@ angular.module('mm.core')
if (dot > -1) {
ext = filename.substr(dot + 1).toLowerCase();
- // Remove hash in extension if there's any. @see $mmFilepool#_getFileIdByUrl
- ext = ext.replace(/_.{32}$/, '');
+ ext = self.cleanExtension(ext);
// Check extension corresponds to a mimetype to know if it's valid.
if (typeof self.getMimeType(ext) == 'undefined') {
@@ -939,13 +939,15 @@ angular.module('mm.core')
* @return {String} Mimetype.
*/
self.getMimeType = function(extension) {
+ extension = self.cleanExtension(extension);
+
if (extToMime[extension] && extToMime[extension].type) {
return extToMime[extension].type;
}
};
/**
- * Get the "type" of an extension, something like "image", "video" or "audio".
+ * Get the "type" (string) of an extension, something like "image", "video" or "audio".
*
* @module mm.core
* @ngdoc method
@@ -955,11 +957,39 @@ angular.module('mm.core')
* @since 3.3
*/
self.getExtensionType = function(extension) {
+ extension = self.cleanExtension(extension);
+
if (extToMime[extension] && extToMime[extension].string) {
return extToMime[extension].string;
}
};
+ /**
+ * Get the "type" (string) of a mimetype, something like "image", "video" or "audio".
+ *
+ * @module mm.core
+ * @ngdoc method
+ * @name $mmFS#getMimetypeType
+ * @param {String} mimetype Mimetype.
+ * @return {Mixed} Type of the mimetype.
+ * @since 3.3.2
+ */
+ self.getMimetypeType = function(mimetype) {
+ mimetype = mimetype.split(';')[0]; // Remove codecs from the mimetype if any.
+
+ var extensions = mimeToExt[mimetype];
+ if (!extensions) {
+ return;
+ }
+
+ for (var i = 0; i < extensions.length; i++) {
+ var extension = extensions[i];
+ if (extToMime[extension] && extToMime[extension].string) {
+ return extToMime[extension].string;
+ }
+ }
+ };
+
/**
* Guess the extension of a file from its URL.
*
@@ -1010,6 +1040,9 @@ angular.module('mm.core')
* @return {String} Extension.
*/
self.getExtension = function(mimetype, url) {
+ mimetype = mimetype || '';
+ mimetype = mimetype.split(';')[0]; // Remove codecs from the mimetype if any.
+
if (mimetype == 'application/x-forcedownload' || mimetype == 'application/forcedownload') {
// Couldn't get the right mimetype (old Moodle), try to guess it.
return self.guessExtensionFromUrl(url);
@@ -1029,6 +1062,22 @@ angular.module('mm.core')
return undefined;
};
+ /**
+ * Get all the possible extensions of a mimetype. Returns empty array if not found.
+ *
+ * @module mm.core
+ * @ngdoc method
+ * @name $mmFS#getExtensions
+ * @param {String} mimetype Mimetype.
+ * @return {String[]} Extensions.
+ * @since 3.3.2
+ */
+ self.getExtensions = function(mimetype) {
+ mimetype = mimetype || '';
+ mimetype = mimetype.split(';')[0]; // Remove codecs from the mimetype if any.
+ return mimeToExt[mimetype] || [];
+ };
+
/**
* Remove the extension from a path (if any).
*
@@ -1363,6 +1412,8 @@ angular.module('mm.core')
* @since 3.3
*/
self.isExtensionInGroup = function(extension, groups) {
+ extension = self.cleanExtension(extension);
+
if (groups && groups.length && extToMime[extension] && extToMime[extension].groups) {
for (var i = 0; i < extToMime[extension].groups.length; i++) {
var group = extToMime[extension].groups[i];
@@ -1388,5 +1439,182 @@ angular.module('mm.core')
return self.isExtensionInGroup(extension, ['web_image', 'web_video', 'web_audio']);
};
+ /**
+ * Get the mimetype/extension info belonging to a certain group.
+ *
+ * @module mm.core
+ * @ngdoc method
+ * @name $mmFS#getGroupMimetypes
+ * @param {String} group Group name.
+ * @param {String} [field] The field to get. If not supplied, all the info will be returned.
+ * @return {Mixed} List of mimetypes.
+ * @since 3.3.2
+ */
+ self.getGroupMimeInfo = function(group, field) {
+ if (typeof groupsMimeInfo[group] == 'undefined') {
+ fillGroupMimeInfo(group);
+ }
+
+ if (field) {
+ return groupsMimeInfo[group][field];
+ }
+ return groupsMimeInfo[group];
+ };
+
+ /**
+ * Fill the mimetypes and extensions info for a certain group.
+ *
+ * @param {String} group Group name.
+ * @return {Void}
+ * @since 3.3.2
+ */
+ function fillGroupMimeInfo(group) {
+ var mimetypes = {}, // Use an object to prevent duplicates.
+ extensions = []; // Extensions are unique.
+
+ angular.forEach(extToMime, function(data, extension) {
+ if (data.type && data.groups && data.groups.indexOf(group) != -1) {
+ // This extension has the group, add it to the list.
+ mimetypes[data.type] = true;
+ extensions.push(extension);
+ }
+ });
+
+ groupsMimeInfo[group] = {
+ mimetypes: Object.keys(mimetypes),
+ extensions: extensions
+ };
+ }
+
+ /**
+ * Obtains descriptions for file types (e.g. 'Microsoft Word document') from the language file.
+ * Based on Moodle's get_mimetype_description.
+ *
+ * @module mm.core
+ * @ngdoc method
+ * @name $mmFS#getMimetypeDescription
+ * @param {Mixed} obj Instance of FileEntry OR object with 'filename' and 'mimetype' OR string with mimetype.
+ * @param {Boolean} capitalise If true, capitalises first character of result.
+ * @return {String} Type description.
+ * @since 3.3.2
+ */
+ self.getMimetypeDescription = function(obj, capitalise) {
+ var filename = '',
+ mimetype = '',
+ extension = '',
+ langPrefix = 'mm.core.mimetype-';
+
+ if (typeof obj == 'object' && angular.isFunction(obj.file)) {
+ // It's a FileEntry. Don't use the file function because it's asynchronous and the type isn't reliable.
+ filename = obj.name;
+ } else if (typeof obj == 'object') {
+ filename = obj.filename || '';
+ mimetype = obj.mimetype || '';
+ } else {
+ mimetype = obj;
+ }
+
+ if (filename) {
+ extension = self.getFileExtension(filename);
+
+ if (!mimetype) {
+ // Try to calculate the mimetype using the extension.
+ mimetype = self.getMimeType(extension);
+ }
+ }
+
+ if (!mimetype) {
+ // Don't have the mimetype, stop.
+ return '';
+ }
+
+ if (!extension) {
+ extension = self.getExtension(mimetype);
+ }
+
+ var mimetypeStr = self.getMimetypeType(mimetype) || '',
+ chunks = mimetype.split('/'),
+ attr = {
+ mimetype: mimetype,
+ ext: extension || '',
+ mimetype1: chunks[0],
+ mimetype2: chunks[1] || '',
+ },
+ a = {};
+
+ for (var key in attr) {
+ var value = attr[key];
+ a[key] = value;
+ a[key.toUpperCase()] = value.toUpperCase();
+ a[$mmText.ucFirst(key)] = $mmText.ucFirst(value);
+ }
+
+ // MIME types may include + symbol but this is not permitted in string ids.
+ var safeMimetype = mimetype.replace(/\+/g, '_'),
+ safeMimetypeStr = mimetypeStr.replace(/\+/g, '_'),
+ safeMimetypeTrns = $translate.instant(langPrefix + safeMimetype, {$a: a}),
+ safeMimetypeStrTrns = $translate.instant(langPrefix + safeMimetypeStr, {$a: a}),
+ defaultTrns = $translate.instant(langPrefix + 'default', {$a: a}),
+ result = mimetype;
+
+ if (safeMimetypeTrns != langPrefix + safeMimetype) {
+ result = safeMimetypeTrns;
+ } else if (safeMimetypeStrTrns != langPrefix + safeMimetypeStr) {
+ result = safeMimetypeStrTrns;
+ } else if (defaultTrns != langPrefix + 'default') {
+ result = defaultTrns;
+ }
+
+ if (capitalise) {
+ result = $mmText.ucFirst(result);
+ }
+
+ return result;
+ };
+
+ /**
+ * Given a group name, return the translated name.
+ *
+ * @module mm.core
+ * @ngdoc method
+ * @name $mmFS#getTranslatedGroupName
+ * @param {String} name Group name.
+ * @return {String} Translated name.
+ * @since 3.3.2
+ */
+ self.getTranslatedGroupName = function(name) {
+ var key = 'mm.core.mimetype-group:' + name,
+ translated = $translate.instant(key);
+ return translated != key ? translated : name;
+ };
+
+ /**
+ * Clean a extension, removing the dot, hash, extra params...
+ *
+ * @module mm.core
+ * @ngdoc method
+ * @name $mmFS#cleanExtension
+ * @param {String} extension Extension to clean.
+ * @return {String} Clean extension.
+ * @since 3.3.2
+ */
+ self.cleanExtension = function(extension) {
+ // If the extension has parameters, remove them.
+ var position = extension.indexOf('?');
+ if (position > -1) {
+ extension = extension.substr(0, position);
+ }
+
+ // Remove hash in extension if there's any. @see $mmFilepool#_getFileIdByUrl
+ extension = extension.replace(/_.{32}$/, '');
+
+ // Remove dot from the extension if found.
+ if (extension && extension[0] == '.') {
+ extension = extension.substr(1);
+ }
+
+ return extension;
+ };
+
return self;
});
diff --git a/www/core/lib/text.js b/www/core/lib/text.js
index 27df20f5dbd..2466ec53cdf 100644
--- a/www/core/lib/text.js
+++ b/www/core/lib/text.js
@@ -725,5 +725,18 @@ angular.module('mm.core')
return self.escapeHTML(text).replace(/&#(\d+|x[0-9a-f]+);/i, '$1;');
};
+ /**
+ * Make a string's first character uppercase
+ *
+ * @module mm.core
+ * @ngdoc method
+ * @name $mmText#ucFirst
+ * @param {String} text Text to treat.
+ * @return {String} Treated text.
+ */
+ self.ucFirst = function(text) {
+ return text.charAt(0).toUpperCase() + text.slice(1);
+ };
+
return self;
});
diff --git a/www/core/lib/util.js b/www/core/lib/util.js
index f2d3df3e97e..89cf25d2d68 100644
--- a/www/core/lib/util.js
+++ b/www/core/lib/util.js
@@ -2573,6 +2573,54 @@ angular.module('mm.core')
return measure;
};
+ /**
+ * Gets the index of the first string that matches a regular expression.
+ *
+ * @module mm.core
+ * @ngdoc method
+ * @name $mmUtil#indexOfRegexp
+ * @param {String[]} array Array to search.
+ * @param {RegExp} regex RegExp to apply to each string.
+ * @return {Number} Index of the first string that matches the RegExp. -1 if not found.
+ */
+ self.indexOfRegexp = function(array, regex) {
+ if (!array || !array.length) {
+ return -1;
+ }
+
+ for (var i = 0; i < array.length; i++) {
+ var entry = array[i],
+ matches = entry.match(regex);
+
+ if (matches && matches.length) {
+ return i;
+ }
+ }
+
+ return -1;
+ };
+
+ /**
+ * Given an array of strings, return only the ones that match a regular expression.
+ *
+ * @module mm.core
+ * @ngdoc method
+ * @name $mmUtil#filterByRegexp
+ * @param {String[]} array Array to filter.
+ * @param {RegExp} regex RegExp to apply to each string.
+ * @return {String[]} Filtered array.
+ */
+ self.filterByRegexp = function(array, regex) {
+ if (!array || !array.length) {
+ return [];
+ }
+
+ return array.filter(function(entry) {
+ var matches = entry.match(regex);
+ return matches && matches.length;
+ });
+ };
+
return self;
};
});
diff --git a/www/core/scss/styles.scss b/www/core/scss/styles.scss
index 05b2f94e72f..b9ceba30e8e 100644
--- a/www/core/scss/styles.scss
+++ b/www/core/scss/styles.scss
@@ -1691,3 +1691,8 @@ h2.invert {
::-webkit-media-controls {
max-width: 100%;
}
+
+ol.list-with-style, ul.list-with-style {
+ list-style: initial;
+ padding-left: 40px;
+}