From f02d3dc683d9547d751e2871e811811f376b99cc Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Mon, 8 Apr 2024 12:00:46 -0400 Subject: [PATCH] Implement CodeQL recommendations --- Backend.Tests/Mocks/PermissionServiceMock.cs | 1 - Backend.Tests/Mocks/UserRepositoryMock.cs | 1 - Backend/Controllers/InviteController.cs | 2 +- Backend/Controllers/ProjectController.cs | 2 +- Backend/Controllers/UserRoleController.cs | 11 +- Backend/Helper/FileOperations.cs | 1 - Backend/Helper/FileStorage.cs | 1 - Backend/Helper/Language.cs | 2 +- Backend/Helper/LiftHelper.cs | 1 - Backend/Helper/Sanitization.cs | 1 - Backend/Services/InviteService.cs | 8 +- Backend/Services/LiftService.cs | 36 ++--- Backend/Services/MergeService.cs | 1 - Backend/Services/PermissionService.cs | 1 - Backend/Services/StatisticsService.cs | 131 +++++++++---------- README.md | 4 + 16 files changed, 84 insertions(+), 120 deletions(-) diff --git a/Backend.Tests/Mocks/PermissionServiceMock.cs b/Backend.Tests/Mocks/PermissionServiceMock.cs index 8da10040af..30fdc953a2 100644 --- a/Backend.Tests/Mocks/PermissionServiceMock.cs +++ b/Backend.Tests/Mocks/PermissionServiceMock.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.Serialization; using System.Threading.Tasks; using BackendFramework.Interfaces; using BackendFramework.Models; diff --git a/Backend.Tests/Mocks/UserRepositoryMock.cs b/Backend.Tests/Mocks/UserRepositoryMock.cs index b5c367d333..72f99ac91d 100644 --- a/Backend.Tests/Mocks/UserRepositoryMock.cs +++ b/Backend.Tests/Mocks/UserRepositoryMock.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.Serialization; using System.Threading.Tasks; using BackendFramework.Helper; using BackendFramework.Interfaces; diff --git a/Backend/Controllers/InviteController.cs b/Backend/Controllers/InviteController.cs index 05a12b8b24..aa50249af2 100644 --- a/Backend/Controllers/InviteController.cs +++ b/Backend/Controllers/InviteController.cs @@ -92,7 +92,7 @@ public async Task ValidateToken(string projectId, string token) if (user.Email == tokenObj.Email) { currentUser = user; - if (!user.ProjectRoles.ContainsKey(projectId)) + if (!user.ProjectRoles.TryGetValue(projectId, out var _roleId)) { isUserRegisteredAndNotInProject = true; } diff --git a/Backend/Controllers/ProjectController.cs b/Backend/Controllers/ProjectController.cs index 4c67980c9f..0e8ddf0f0a 100644 --- a/Backend/Controllers/ProjectController.cs +++ b/Backend/Controllers/ProjectController.cs @@ -54,7 +54,7 @@ public async Task GetAllProjectUsers(string projectId) } var allUsers = await _userRepo.GetAllUsers(); - var projectUsers = allUsers.FindAll(user => user.ProjectRoles.ContainsKey(projectId)); + var projectUsers = allUsers.FindAll(user => user.ProjectRoles.TryGetValue(projectId, out var _roleId)); return Ok(projectUsers); } diff --git a/Backend/Controllers/UserRoleController.cs b/Backend/Controllers/UserRoleController.cs index 0de76cd2ff..b87331dee3 100644 --- a/Backend/Controllers/UserRoleController.cs +++ b/Backend/Controllers/UserRoleController.cs @@ -108,11 +108,11 @@ public async Task GetCurrentPermissions(string projectId) return NotFound($"user: {userId}"); } - if (!user.ProjectRoles.ContainsKey(projectId)) + if (!user.ProjectRoles.TryGetValue(projectId, out var roleId)) { return Ok(new List()); } - var userRole = await _userRoleRepo.GetUserRole(projectId, user.ProjectRoles[projectId]); + var userRole = await _userRoleRepo.GetUserRole(projectId, roleId); if (userRole is null) { return Ok(new List()); @@ -231,12 +231,7 @@ public async Task UpdateUserRole( return NotFound(userId); } - string userRoleId; - if (changeUser.ProjectRoles.ContainsKey(projectId)) - { - userRoleId = changeUser.ProjectRoles[projectId]; - } - else + if (!changeUser.ProjectRoles.TryGetValue(projectId, out var userRoleId)) { // Generate the userRole var usersRole = new UserRole { ProjectId = projectId }; diff --git a/Backend/Helper/FileOperations.cs b/Backend/Helper/FileOperations.cs index 74b87ec359..eedaba9723 100644 --- a/Backend/Helper/FileOperations.cs +++ b/Backend/Helper/FileOperations.cs @@ -3,7 +3,6 @@ using System.IO; using System.IO.Compression; using System.Linq; -using System.Runtime.Serialization; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; diff --git a/Backend/Helper/FileStorage.cs b/Backend/Helper/FileStorage.cs index a4ec0f6119..7388b756be 100644 --- a/Backend/Helper/FileStorage.cs +++ b/Backend/Helper/FileStorage.cs @@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Runtime.Serialization; namespace BackendFramework.Helper { diff --git a/Backend/Helper/Language.cs b/Backend/Helper/Language.cs index 21e221a18b..cb5d9bd4d6 100644 --- a/Backend/Helper/Language.cs +++ b/Backend/Helper/Language.cs @@ -43,7 +43,7 @@ public static List ConvertLangTagsToWritingSystems(IEnumerable public static List GetWritingSystems(string dirPath) { - if (!Directory.GetFiles(dirPath, "*.ldml").Any()) + if (Directory.GetFiles(dirPath, "*.ldml").Length == 0) { dirPath = FileStorage.GenerateWritingsSystemsSubdirPath(dirPath); } diff --git a/Backend/Helper/LiftHelper.cs b/Backend/Helper/LiftHelper.cs index 7d48389b20..0fb7b2ce83 100644 --- a/Backend/Helper/LiftHelper.cs +++ b/Backend/Helper/LiftHelper.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.Serialization; using BackendFramework.Models; using SIL.Lift.Parsing; diff --git a/Backend/Helper/Sanitization.cs b/Backend/Helper/Sanitization.cs index b89a66f43e..e52cf33aea 100644 --- a/Backend/Helper/Sanitization.cs +++ b/Backend/Helper/Sanitization.cs @@ -3,7 +3,6 @@ using System.Collections.Immutable; using System.Globalization; using System.Linq; -using System.Runtime.Serialization; using System.Text; namespace BackendFramework.Helper diff --git a/Backend/Services/InviteService.cs b/Backend/Services/InviteService.cs index 6b25032771..00c65869dd 100644 --- a/Backend/Services/InviteService.cs +++ b/Backend/Services/InviteService.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.Serialization; using System.Threading.Tasks; using BackendFramework.Interfaces; using BackendFramework.Models; @@ -68,11 +67,8 @@ public async Task RemoveTokenAndCreateUserRole(Project project, User user, user.ProjectRoles.Add(project.Id, userRole.Id); await _userRepo.Update(user.Id, user); // Generate the JWT based on those new userRoles - var updatedUser = await _permissionService.MakeJwt(user); - if (updatedUser is null) - { - throw new PermissionService.InvalidJwtTokenException("Unable to generate JWT."); - } + var updatedUser = await _permissionService.MakeJwt(user) + ?? throw new PermissionService.InvalidJwtTokenException("Unable to generate JWT."); await _userRepo.Update(updatedUser.Id, updatedUser); diff --git a/Backend/Services/LiftService.cs b/Backend/Services/LiftService.cs index c82e17efc8..1d2fb08496 100644 --- a/Backend/Services/LiftService.cs +++ b/Backend/Services/LiftService.cs @@ -4,7 +4,6 @@ using System.IO; using System.IO.Compression; using System.Linq; -using System.Runtime.Serialization; using System.Security; using System.Threading.Tasks; using System.Xml; @@ -33,7 +32,7 @@ public CombineLiftWriter(string path, ByteOrderStyle byteOrderStyle) : base(path protected override void InsertPronunciationIfNeeded( LexEntry entry, List propertiesAlreadyOutput) { - if (entry.Pronunciations.FirstOrDefault() is not null && entry.Pronunciations.First().Forms.Any()) + if (entry.Pronunciations.Count != 0 && entry.Pronunciations.First().Forms.Length != 0) { foreach (var phonetic in entry.Pronunciations) { @@ -141,11 +140,8 @@ public void SetExportInProgress(string userId, bool isInProgress) /// Query whether user has an in-progress export. public bool IsExportInProgress(string userId) { - if (!_liftExports.ContainsKey(userId)) - { - return false; - } - return _liftExports[userId] == InProgress; + _liftExports.TryGetValue(userId, out var exportPath); + return exportPath == InProgress; } /// Store filePath for a user's Lift export. @@ -159,12 +155,8 @@ public void StoreExport(string userId, string filePath) /// Path to the Lift file on disk. public string? RetrieveExport(string userId) { - if (!_liftExports.ContainsKey(userId) || _liftExports[userId] == InProgress) - { - return null; - } - - return _liftExports[userId]; + _liftExports.TryGetValue(userId, out var exportPath); + return exportPath == InProgress ? null : exportPath; } /// Delete a stored Lift export path and its file on disk. @@ -190,12 +182,8 @@ public void StoreImport(string userId, string filePath) /// Path to the Lift file on disk. public string? RetrieveImport(string userId) { - if (!_liftImports.ContainsKey(userId)) - { - return null; - } - - return _liftImports[userId]; + _liftImports.TryGetValue(userId, out var importPath); + return importPath; } /// Delete a stored Lift import path and its file on disk. @@ -214,7 +202,7 @@ public bool DeleteImport(string userId) /// A bool indicating whether a character set was added to the project. public async Task LdmlImport(string dirPath, IProjectRepository projRepo, Project project) { - if (!Directory.GetFiles(dirPath, "*.ldml").Any()) + if (Directory.GetFiles(dirPath, "*.ldml").Length == 0) { dirPath = FileStorage.GenerateWritingsSystemsSubdirPath(dirPath); } @@ -478,11 +466,11 @@ private static void AddSenses(LexEntry entry, Word wordEntry, Dictionary(); foreach (var def in currentSense.Definitions) { - if (defDict.ContainsKey(def.Language)) + if (defDict.TryGetValue(def.Language, out var defText)) { // This is an unexpected situation but rather than crashing or losing data we // will just append extra definitions for the language with a separator. - defDict[def.Language] = $"{defDict[def.Language]}{sep}{def.Text}"; + defDict[def.Language] = $"{defText}{sep}{def.Text}"; } else { @@ -492,11 +480,11 @@ private static void AddSenses(LexEntry entry, Word wordEntry, Dictionary(); foreach (var gloss in currentSense.Glosses) { - if (glossDict.ContainsKey(gloss.Language)) + if (glossDict.TryGetValue(gloss.Language, out var glossDef)) { // This is an unexpected situation but rather than crashing or losing data we // will just append extra definitions for the language with a separator. - glossDict[gloss.Language] = $"{glossDict[gloss.Language]}{sep}{gloss.Def}"; + glossDict[gloss.Language] = $"{glossDef}{sep}{gloss.Def}"; } else { diff --git a/Backend/Services/MergeService.cs b/Backend/Services/MergeService.cs index 919eb61716..d88f9f00b3 100644 --- a/Backend/Services/MergeService.cs +++ b/Backend/Services/MergeService.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using System.Runtime.Serialization; using BackendFramework.Helper; using BackendFramework.Interfaces; using BackendFramework.Models; diff --git a/Backend/Services/PermissionService.cs b/Backend/Services/PermissionService.cs index e25d2e1bed..881f348407 100644 --- a/Backend/Services/PermissionService.cs +++ b/Backend/Services/PermissionService.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; -using System.Runtime.Serialization; using System.Security.Claims; using System.Text; using System.Text.Json; diff --git a/Backend/Services/StatisticsService.cs b/Backend/Services/StatisticsService.cs index 60cccf1789..1979891f1a 100644 --- a/Backend/Services/StatisticsService.cs +++ b/Backend/Services/StatisticsService.cs @@ -28,28 +28,28 @@ public StatisticsService( /// public async Task> GetSemanticDomainCounts(string projectId, string lang) { - Dictionary hashMap = new Dictionary(); - List? domainTreeNodeList = await _domainRepo.GetAllSemanticDomainTreeNodes(lang); - List wordList = await _wordRepo.GetFrontier(projectId); + var hashMap = new Dictionary(); + var domainTreeNodeList = await _domainRepo.GetAllSemanticDomainTreeNodes(lang); + var wordList = await _wordRepo.GetFrontier(projectId); - if (domainTreeNodeList is null || !domainTreeNodeList.Any() || !wordList.Any()) + if (domainTreeNodeList is null || domainTreeNodeList.Count == 0 || wordList.Count == 0) { return new List(); } - foreach (Word word in wordList) + foreach (var word in wordList) { - foreach (Sense sense in word.Senses) + foreach (var sense in word.Senses) { - foreach (SemanticDomain sd in sense.SemanticDomains) + foreach (var sd in sense.SemanticDomains) { hashMap[sd.Id] = hashMap.GetValueOrDefault(sd.Id, 0) + 1; } } } - List resList = new List(); - foreach (SemanticDomainTreeNode domainTreeNode in domainTreeNodeList) + var resList = new List(); + foreach (var domainTreeNode in domainTreeNodeList) { resList.Add(new SemanticDomainCount(domainTreeNode, hashMap.GetValueOrDefault(domainTreeNode.Id, 0))); } @@ -61,67 +61,60 @@ public async Task> GetSemanticDomainCounts(string proj /// public async Task> GetWordsPerDayPerUserCounts(string projectId) { - List wordList = await _wordRepo.GetFrontier(projectId); - Dictionary shortTimeDictionary = - new Dictionary(); - Dictionary userNameIdDictionary = new Dictionary(); + var wordList = await _wordRepo.GetFrontier(projectId); + var shortTimeDictionary = new Dictionary(); + var userNameIdDictionary = new Dictionary(); - if (!wordList.Any()) + if (wordList.Count == 0) { return new List(); } var allUsers = await _userRepo.GetAllUsers(); - var projectUsers = allUsers.FindAll(user => user.ProjectRoles.ContainsKey(projectId)); + var projectUsers = allUsers.FindAll(user => user.ProjectRoles.TryGetValue(projectId, out var _roleId)); // only count for current valid users of the project - foreach (User u in projectUsers) + foreach (var u in projectUsers) { userNameIdDictionary.Add(u.Id, u.Username); } - foreach (Word word in wordList) + foreach (var word in wordList) { - foreach (Sense sense in word.Senses) + foreach (var sense in word.Senses) { - foreach (SemanticDomain sd in sense.SemanticDomains) + foreach (var sd in sense.SemanticDomains) { // The created timestamp may not exist for some model if (!string.IsNullOrEmpty(sd.Created)) { var dateKey = ParseDateTimePermissivelyWithException(sd.Created) .ToISO8601TimeFormatDateOnlyString(); - if (!shortTimeDictionary.ContainsKey(dateKey)) + if (!shortTimeDictionary.TryGetValue(dateKey, out var chartNode)) { - var tempBarChartNode = new WordsPerDayPerUserCount(sd.Created); - foreach (User u in projectUsers) + chartNode = new WordsPerDayPerUserCount(sd.Created); + foreach (var u in projectUsers) { - tempBarChartNode.UserNameCountDictionary.Add(u.Username, 0); + chartNode.UserNameCountDictionary.Add(u.Username, 0); } - shortTimeDictionary.Add(dateKey, tempBarChartNode); + shortTimeDictionary.Add(dateKey, chartNode); } - var chartNode = shortTimeDictionary[dateKey]; var username = userNameIdDictionary.GetValueOrDefault(sd.UserId, "?"); // A semantic domain shouldn't usually have `.Created` without a valid `.UserId`; // this case is a safe-guard to allow a project owner to see statistics even if there's an // error in the user reckoning (e.g., if a user is removed from the project mid-workshop). - if (!chartNode.UserNameCountDictionary.ContainsKey(username)) + if (!chartNode.UserNameCountDictionary.TryAdd(username, 1)) { - chartNode.UserNameCountDictionary.Add(username, 1); - } - else - { - chartNode.UserNameCountDictionary[username] += 1; + chartNode.UserNameCountDictionary[username]++; } } } } } + var resList = shortTimeDictionary.Values.ToList(); // sort by date order - var resList = shortTimeDictionary.Values.ToList() - .OrderBy(t => t.DateTime.ToISO8601TimeFormatDateOnlyString()).ToList(); - return resList; + return resList.OrderBy(t => t.DateTime.ToISO8601TimeFormatDateOnlyString()).ToList(); } /// @@ -130,54 +123,50 @@ public async Task> GetWordsPerDayPerUserCounts(str /// public async Task GetProgressEstimationLineChartRoot(string projectId, List schedule) { - ChartRootData LineChartData = new ChartRootData(); - List wordList = await _wordRepo.GetFrontier(projectId); - List workshopSchedule = new List(); - Dictionary totalCountDictionary = new Dictionary(); + var LineChartData = new ChartRootData(); + var wordList = await _wordRepo.GetFrontier(projectId); + var workshopSchedule = new List(); + var totalCountDictionary = new Dictionary(); // If no schedule yet or wordList is empty, return empty ChartRootData - if (!schedule.Any() || !wordList.Any()) + if (schedule.Count == 0 || wordList.Count == 0) { return LineChartData; } // Build workshop schedule hashSet - foreach (DateTime dt in schedule) + foreach (var dt in schedule) { workshopSchedule.Add(dt.ToISO8601TimeFormatDateOnlyString()); } // Build daily count Dictionary - foreach (Word word in wordList) + foreach (var word in wordList) { - foreach (Sense sense in word.Senses) + foreach (var sense in word.Senses) { - foreach (SemanticDomain sd in sense.SemanticDomains) + foreach (var sd in sense.SemanticDomains) { if (!string.IsNullOrEmpty(sd.Created)) { - string dateString = ParseDateTimePermissivelyWithException(sd.Created) + var dateString = ParseDateTimePermissivelyWithException(sd.Created) .ToISO8601TimeFormatDateOnlyString(); if (!workshopSchedule.Contains(dateString)) { continue; } - else if (totalCountDictionary.ContainsKey(dateString)) + else if (!totalCountDictionary.TryAdd(dateString, 1)) { totalCountDictionary[dateString]++; } - else - { - totalCountDictionary.Add(dateString, 1); - } } } } } // If no semantic domain has Created, return empty ChartRootData - if (!totalCountDictionary.Any()) + if (totalCountDictionary.Count == 0) { return LineChartData; } @@ -197,7 +186,7 @@ public async Task GetProgressEstimationLineChartRoot(string proje // If pastDays is two or more and at least one of those days had no word added else if (pastDays > 1) { - averageValue = (totalCountList.Sum()) / (pastDays - 1); + averageValue = totalCountList.Sum() / (pastDays - 1); } // no need to remove the lowest data if there's only one past day else @@ -207,7 +196,7 @@ public async Task GetProgressEstimationLineChartRoot(string proje int burst = 0, burstProjection = 0, projection = min, runningTotal = 0, today = 0, yesterday = 0; // generate ChartRootData for frontend - for (int i = 0; i < workshopSchedule.Count; i++) + for (var i = 0; i < workshopSchedule.Count; i++) { LineChartData.Dates.Add(workshopSchedule[i]); var day = workshopSchedule[i]; @@ -216,7 +205,7 @@ public async Task GetProgressEstimationLineChartRoot(string proje runningTotal = totalCountDictionary.ContainsKey(day) ? totalCountDictionary[day] : 0; today = yesterday = runningTotal; LineChartData.Datasets.Add(new Dataset( - "Daily Total", (totalCountDictionary.ContainsKey(day) ? totalCountDictionary[day] : 0))); + "Daily Total", totalCountDictionary.ContainsKey(day) ? totalCountDictionary[day] : 0)); LineChartData.Datasets.Add(new Dataset("Average", averageValue)); LineChartData.Datasets.Add(new Dataset("Running Total", runningTotal)); LineChartData.Datasets.Add(new Dataset("Projection", projection)); @@ -242,7 +231,7 @@ public async Task GetProgressEstimationLineChartRoot(string proje else { LineChartData.Datasets.Find( - element => element.UserName == "Burst Projection")?.Data.Add((burstProjection)); + element => element.UserName == "Burst Projection")?.Data.Add(burstProjection); burstProjection += burst; } LineChartData.Datasets.Find(element => element.UserName == "Projection")?.Data.Add(projection); @@ -258,16 +247,16 @@ public async Task GetProgressEstimationLineChartRoot(string proje /// public async Task GetLineChartRootData(string projectId) { - ChartRootData LineChartData = new ChartRootData(); - List list = await GetWordsPerDayPerUserCounts(projectId); + var LineChartData = new ChartRootData(); + var list = await GetWordsPerDayPerUserCounts(projectId); // if the list is null or empty return new ChartRootData to generate a empty Chart - if (list is null || !list.Any()) + if (list is null || list.Count == 0) { return LineChartData; } // update the ChartRootData based on the order of the WordsPerDayPerUserCount from the list - foreach (WordsPerDayPerUserCount temp in list) + foreach (var temp in list) { LineChartData.Dates.Add(temp.DateTime.ToISO8601TimeFormatDateOnlyString()); // first traversal, generate a new Dataset @@ -306,15 +295,15 @@ public async Task GetLineChartRootData(string projectId) /// A List of SemanticDomainUserCount public async Task> GetSemanticDomainUserCounts(string projectId) { - List wordList = await _wordRepo.GetFrontier(projectId); - Dictionary resUserMap = new Dictionary(); + var wordList = await _wordRepo.GetFrontier(projectId); + var resUserMap = new Dictionary(); // Get all users of the project var allUsers = await _userRepo.GetAllUsers(); - var projectUsers = allUsers.FindAll(user => user.ProjectRoles.ContainsKey(projectId)); + var projectUsers = allUsers.FindAll(user => user.ProjectRoles.TryGetValue(projectId, out var _roleId)); // build a SemanticDomainUserCount object hashMap with userId as the key - foreach (User u in projectUsers) + foreach (var u in projectUsers) { resUserMap.Add(u.Id, new SemanticDomainUserCount(u.Id, u.Username)); } @@ -324,21 +313,21 @@ public async Task> GetSemanticDomainUserCounts(str var unknownName = "unknownUser"; resUserMap.Add(unknownId, new SemanticDomainUserCount(unknownId, unknownName)); - foreach (Word word in wordList) + foreach (var word in wordList) { - foreach (Sense sense in word.Senses) + foreach (var sense in word.Senses) { - foreach (SemanticDomain sd in sense.SemanticDomains) + foreach (var sd in sense.SemanticDomains) { var userId = sd.UserId; var domainName = sd.Name; - var domainUserValue = new SemanticDomainUserCount(); + // if the SemanticDomain have a userId and exist in HashMap - domainUserValue = (userId is not null && resUserMap.ContainsKey(userId) - // if true, new SemanticDomain model - ? domainUserValue = resUserMap[userId] - // if false, assign to unknownUser - : domainUserValue = resUserMap[unknownId]); + SemanticDomainUserCount? domainUserValue; + if (userId is null || !resUserMap.TryGetValue(userId, out domainUserValue)) + { + domainUserValue = resUserMap[unknownId]; + } // update DomainCount if (!domainUserValue.DomainSet.Contains(domainName)) diff --git a/README.md b/README.md index c6c5089d47..c95f2a8e18 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,10 @@ A rapid word collection tool. See the [User Guide](https://sillsdev.github.io/Th [this guide](https://github.com/nodesource/distributions/blob/master/README.md#installation-instructions) using the appropriate Node.js version. 4. [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) + + - On Ubuntu 20.02 or earlier, follow + [these instructions](https://docs.microsoft.com/en-us/dotnet/core/install/linux-ubuntu). + 5. [MongoDB](https://mongodb.com/docs/manual/administration/install-community/) provides instructions on how to install the current release of MongoDB.