diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index cc99e9653..fad7af826 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -26,6 +26,15 @@ */ const CSV_MIME_TYPE = 'text/csv'; +/** + * A fallback regular expression for CSV filenames, if no MIME type is provided + * by the browser. Any file that matches this regex will be considered to be a + * CSV file. + * + * @type RegExp + */ +const CSV_FILENAME_REGEX = /\.csv$/i; + /** * The allowed MIME type for JSON files. * @@ -33,6 +42,15 @@ const CSV_MIME_TYPE = 'text/csv'; */ const JSON_MIME_TYPE = 'application/json'; +/** + * A fallback regular expression for JSON filenames, if no MIME type is provided + * by the browser. Any file that matches this regex will be considered to be a + * JSON file. + * + * @type RegExp + */ +const JSON_FILENAME_REGEX = /\.json$/i; + /** * The allowed MIME types for YAML files. * NOTE: There is no registered MIME type for YAML files. This may result in a @@ -50,6 +68,15 @@ const YAML_MIME_TYPES = [ 'application/yml' ]; +/** + * A fallback regular expression for YAML filenames, if no MIME type is provided + * by the browser. Any file that matches this regex will be considered to be a + * YAML file. + * + * @type RegExp + */ +const YAML_FILENAME_REGEX = /\.ya?ml$/i; + /** * Possible signatures for zip files (which include most modern Microsoft office * documents - most notable excel). If any file, regardless of extension, has @@ -564,12 +591,46 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // There should only ever be a single file in the array const file = files[0]; - // The MIME type of the provided file - const mimeType = file.type; + // The name and MIME type of the file as provided by the browser + let fileName = file.name; + let mimeType = file.type; + + // If no MIME type was provided by the browser at all, use REGEXes as a + // fallback to try to determine the file type. NOTE: Windows 10/11 are + // known to do this with YAML files. + if (!_.trim(mimeType).length) { + + // If the file name matches what we'd expect for a CSV file, set the + // CSV MIME type and move on + if (CSV_FILENAME_REGEX.test(fileName)) + mimeType = CSV_MIME_TYPE; + + // If the file name matches what we'd expect for a JSON file, set + // the JSON MIME type and move on + else if (JSON_FILENAME_REGEX.test(fileName)) + mimeType = JSON_MIME_TYPE; + + // If the file name matches what we'd expect for a JSON file, set + // one of the allowed YAML MIME types and move on + else if (YAML_FILENAME_REGEX.test(fileName)) + mimeType = YAML_MIME_TYPES[0]; + + else { + + // If none of the REGEXes pass, there's nothing more to be tried + handleError(new ParseError({ + message: "Unknown type for file: " + fileName, + key: 'IMPORT.ERROR_DETECTED_INVALID_TYPE' + })); + return; + + } + + } // Check if the mimetype is one of the supported types, // e.g. "application/json" or "text/csv" - if (LEGAL_MIME_TYPES.indexOf(mimeType) < 0) { + else if (LEGAL_MIME_TYPES.indexOf(mimeType) < 0) { // If the provided file is not one of the supported types, // display an error and abort processing @@ -582,7 +643,9 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ } - $scope.fileName = file.name; + // Save the name and type to the scope + $scope.fileName = fileName; + $scope.mimeType = mimeType; // Initialize upload state $scope.aborted = false; @@ -590,9 +653,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ $scope.processing = false; $scope.uploadStarted = true; - // Save the MIME type to the scope - $scope.mimeType = file.type; - // Save the file to the scope when ready $scope.fileReader = new FileReader(); $scope.fileReader.onloadend = (e => { diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index b7c2923ec..b7135ec22 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -477,9 +477,17 @@ angular.module('import').factory('connectionParseService', _.forEach(connection.parameters, (value, name) => { + // An explicit null value for a parameter is valid - do not + // process it further + if (value === null) + return; + + // All non-null connection parameters must be strings. + const stringValue = String(value); + // Convert the provided value to the format that would match // the lookup object format - const comparisonValue = value.toLowerCase().trim(); + const comparisonValue = stringValue.toLowerCase().trim(); // The validated / corrected option value for this connection // parameter, if any @@ -491,6 +499,22 @@ angular.module('import').factory('connectionParseService', if (validOptionValue) connection.parameters[name] = validOptionValue; + // Even if no option is found, the value must be a string + else + connection.parameters[name] = stringValue; + + }); + + _.forEach(connection.attributes, (value, name) => { + + // An explicit null value for an attribute is valid - do not + // process it further + if (value === null) + return; + + // All non-null connection attributes must be strings + connection.attributes[name] = String(value); + }); return connection; diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 1429360d6..0826f3354 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -244,7 +244,7 @@ "HELP_UPLOAD_DROP_TITLE" : "Drop a File Here", "HELP_UPLOAD_FILE_TYPES" : "CSV, JSON, or YAML", "HELP_YAML_DESCRIPTION" : "A connection import YAML file is a list of connection objects with exactly the same structure as the JSON format.", - "HELP_YAML_EXAMPLE" : "---\n - name: conn1\n protocol: vnc\n parameters:\n username: alice\n password: pass1\n hostname: conn1.web.com\n group: ROOT\n users:\n - guac user 1\n - guac user 2\n groups:\n - Connection 1 Users\n attributes:\n guacd-encryption: none\n - name: conn2\n protocol: rdp\n parameters:\n username: bob\n password: pass2\n hostname: conn2.web.com\n group: ROOT/Parent Group\n users:\n - guac user 1\n attributes:\n guacd-encryption: none\n - name: conn3\n protocol: ssh\n parameters:\n username: carol\n password: pass3\n hostname: conn3.web.com\n group: ROOT/Parent Group/Child Group\n users:\n - guac user 2\n - guac user 3\n - name: conn4\n protocol: kubernetes", + "HELP_YAML_EXAMPLE" : "---\n - name: conn1\n protocol: vnc\n parameters:\n username: alice\n password: pass1\n hostname: conn1.web.com\n group: ROOT\n users:\n - guac user 1\n - guac user 2\n groups:\n - Connection 1 Users\n attributes:\n guacd-encryption: none\n - name: conn2\n protocol: rdp\n parameters:\n username: bob\n password: pass2\n hostname: conn2.web.com\n group: ROOT/Parent Group\n users:\n - guac user 1\n attributes:\n guacd-encryption: none\n - name: conn3\n protocol: ssh\n parameters:\n username: carol\n password: pass3\n hostname: conn3.web.com\n group: ROOT/Parent Group/Child Group\n users:\n - guac user 2\n - guac user 3\n - name: conn4\n protocol: kubernetes", "INFO_CONNECTIONS_IMPORTED_SUCCESS" : "{NUMBER} {NUMBER, plural, one{connection} other{connections}} imported successfully.",