/// <summary> /// Clone Paratext project data into the SF projects sync folder. Then synchronize existing books /// and notes in project. /// </summary> public async Task CloneAndSyncFromParatext(SFProject proj, string userId, string syncDir, bool silent) { Log($"Cloning {proj.Name} ({proj.Id}) as SF user {userId}"); string existingCloneDir = Path.Combine(syncDir, proj.ParatextId); // If the project directory already exists, no need to sync the project if (_fileSystemService.DirectoryExists(existingCloneDir)) { Log("The project has already been cloned. Skipping..."); return; } try { await CloneProject(proj.Id, userId, silent); Log($"{proj.Name} - Succeeded"); if (silent) { Log($"Deleting cloned repository for {proj.Name}"); _fileSystemService.DeleteDirectory(existingCloneDir); } } catch (Exception e) { Log($"There was a problem cloning the project.{Environment.NewLine}Exception is: {e}"); if (_fileSystemService.DirectoryExists(existingCloneDir)) { _fileSystemService.DeleteDirectory(existingCloneDir); } throw; } }
public async Task <IEnumerable <SFProject> > GetSFProjects(string[] sfProjectIds) { List <SFProject> projectsResults = new List <SFProject>(); foreach (string id in sfProjectIds) { SFProject proj = await RealtimeService.GetSnapshotAsync <SFProject>(id); projectsResults.Add(proj); } return(projectsResults); }
public async Task InviteAsync_LinkSharingEnabled_UserInvited() { var env = new TestEnvironment(); SFProject project = env.GetProject(Project02); Assert.That(project.CheckingConfig.ShareEnabled, Is.True, "setup"); Assert.That(project.CheckingConfig.ShareLevel, Is.EqualTo(CheckingShareLevel.Anyone), "setup: link sharing should be enabled"); const string email = "*****@*****.**"; // SUT await env.Service.InviteAsync(User01, Project02, email); await env.EmailService.Received(1).SendEmailAsync(email, Arg.Any <string>(), Arg.Is <string>(body => body.Contains($"http://localhost/projects/{Project02}?sharing=true&shareKey=1234abc"))); }
private IReadOnlyList <ParatextProject> GetProjects(UserSecret userSecret, IEnumerable <SharedRepository> remotePtProjects, IEnumerable <ProjectMetadata> projectsMetadata) { if (userSecret == null) { throw new ArgumentNullException(); } List <ParatextProject> paratextProjects = new List <ParatextProject>(); IQueryable <SFProject> existingSfProjects = _realtimeService.QuerySnapshots <SFProject>(); foreach (SharedRepository remotePtProject in remotePtProjects) { SFProject correspondingSfProject = existingSfProjects.FirstOrDefault(sfProj => sfProj.ParatextId == remotePtProject.SendReceiveId.Id); bool sfProjectExists = correspondingSfProject != null; bool sfUserIsOnSfProject = correspondingSfProject?.UserRoles.ContainsKey(userSecret.Id) ?? false; bool adminOnPtProject = remotePtProject.SourceUsers.GetRole( GetParatextUsername(userSecret)) == UserRoles.Administrator; bool ptProjectIsConnectable = (sfProjectExists && !sfUserIsOnSfProject) || (!sfProjectExists && adminOnPtProject); // On SF Live server, many users have projects without corresponding project metadata. // If this happens, default to using the project's short name var projectMD = projectsMetadata .SingleOrDefault(pmd => pmd.ProjectGuid == remotePtProject.SendReceiveId); string fullOrShortName = projectMD == null ? remotePtProject.ScrTextName : projectMD.FullName; paratextProjects.Add(new ParatextProject { ParatextId = remotePtProject.SendReceiveId.Id, Name = fullOrShortName, ShortName = remotePtProject.ScrTextName, LanguageTag = correspondingSfProject?.WritingSystem.Tag, ProjectId = correspondingSfProject?.Id, IsConnectable = ptProjectIsConnectable, IsConnected = sfProjectExists && sfUserIsOnSfProject }); } return(paratextProjects.OrderBy(project => project.Name, StringComparer.InvariantCulture).ToArray()); }
/// <summary> /// Creates an internal test project. /// </summary> /// <param name="sourceId">The source project/resource identifier.</param> /// <param name="targetId">The target project identifier. /// This is the project that will reference this project/resource a source.</param> /// <returns>THe task</returns> /// <exception cref="DataNotFoundException"> /// The target project does not exist /// or /// The user does not exist. /// </exception> /// <remarks> /// This is only to be used on test runs! /// </remarks> internal async Task CreateInternalTestProjectAsync(string sourceId, string targetId) { if (!this._testProjectCollection.Any(p => p.ParatextId == sourceId) && !string.IsNullOrWhiteSpace(sourceId)) { // Create the test project SFProject testProject = new SFProject { Id = ObjectId.GenerateNewId().ToString(), ParatextId = sourceId, }; this._testProjectCollection.Add(testProject); // Load the source project from the target project's source directory (it is not moved in test mode) ScrText?scrText = SourceScrTextCollection.FindById("admin", targetId); if (scrText != null) { // Create the test text objects for all of the books foreach (int bookNum in scrText.Settings.BooksPresentSet.SelectedBookNumbers) { string usfm = scrText.GetText(bookNum); string bookText = UsfmToUsx.ConvertToXmlString(scrText, bookNum, usfm, false); var usxDoc = XDocument.Parse(bookText); Dictionary <int, ChapterDelta> deltas = this._deltaUsxMapper.ToChapterDeltas(usxDoc).ToDictionary(cd => cd.Number); var chapters = new List <Chapter>(); foreach (KeyValuePair <int, ChapterDelta> kvp in deltas) { this._testTextDataIdCollection.Add(TextData.GetTextDocId(testProject.Id, bookNum, kvp.Key)); } } } else { Program.Log($"Test Mode Error Migrating TextData For {sourceId} - Could Not Load From Filesystem!"); } // See that at least one user in the target project has permission to create the source project var targetProject = this._realtimeService.QuerySnapshots <SFProject>().FirstOrDefault(p => p.ParatextId == targetId); if (targetProject == null) { throw new DataNotFoundException("The target project does not exist"); } // Get the highest ranked users for this project, that probably have source access string[] userIds = targetProject.UserRoles .Where(ur => ur.Value == SFProjectRole.Administrator || ur.Value == SFProjectRole.Translator) .OrderBy(ur => ur.Value) .Select(ur => ur.Key) .ToArray(); bool someoneCanAccessSourceProject = false; foreach (string userId in userIds) { Attempt <UserSecret> userSecretAttempt = await _userSecrets.TryGetAsync(userId); if (!userSecretAttempt.TryResult(out UserSecret userSecret)) { throw new DataNotFoundException("The user does not exist."); } // We check projects first, in case it is a project IReadOnlyList <ParatextProject> ptProjects = await _paratextService.GetProjectsAsync(userSecret); if (ptProjects.Any(p => p.ParatextId == sourceId)) { someoneCanAccessSourceProject = true; break; } } if (!someoneCanAccessSourceProject) { Program.Log($"Test Mode Error Creating {sourceId} - Nobody In The Target Project Has Access!"); } } }
/// <summary> /// Migrates the objects. /// </summary> /// <param name="doWrite">If set to <c>true</c>, do write changes to the database.</param> /// <returns> /// The task. /// </returns> public async Task MigrateObjectsAsync(bool doWrite) { // Get the existing projects from MongoDB List <SFProject> existingProjects = this._realtimeService.QuerySnapshots <SFProject>().ToList(); // If we are testing, add the test projects if (!doWrite) { existingProjects.AddRange(this._testProjectCollection); } // This is the mapping for update the TextData identifiers // The key is the target project id, the value is the new source project id Dictionary <string, string> sourceMapping = new Dictionary <string, string>(); // Connect to the realtime service using IConnection connection = await this._realtimeService.ConnectAsync(); // Iterate over every project in database foreach (var project in existingProjects) { // Update ProjectRef value string?sourceParatextId = project.TranslateConfig?.Source?.ParatextId; if (!string.IsNullOrWhiteSpace(sourceParatextId)) { // Get the source project SFProject sourceProject = existingProjects.FirstOrDefault(p => p.ParatextId == sourceParatextId); if (sourceProject != null) { // Update the database Program.Log($"Project {project.Id} $.TranslateConfig.Source.ProjectRef = '{sourceProject.Id}'"); if (doWrite) { // Set the ProjectRef var projectDoc = await connection.FetchAsync <SFProject>(project.Id); await projectDoc.SubmitJson0OpAsync(op => { op.Set(p => p.TranslateConfig.Source.ProjectRef, sourceProject.Id); }); } // Record the source mapping sourceMapping.Add(project.Id, sourceProject.Id); } else { Program.Log($"Error Migrating {project.Id} - Source {sourceParatextId} is missing from MongoDB!"); } } } // Get the Textdata collection string collectionName = this._realtimeService.GetCollectionName <TextData>(); IMongoCollection <TextData> collection = this._database.GetCollection <TextData>(collectionName); // Get the existing textdata object ids from MongoDB List <string> textIds = collection.AsQueryable().Select(t => t.Id).ToList(); if (!doWrite) { // Add the test text ids, if we are testing textIds = textIds.Concat(this._testTextDataIdCollection).Distinct().ToList(); } List <string> sourceTextIds = textIds.Where(t => t.EndsWith(":source", StringComparison.Ordinal)).ToList(); // Iterate over every text id in database foreach (string textId in sourceTextIds) { // Get the TextData from the database TextData?text = await collection .Find(Builders <TextData> .Filter.Eq("_id", textId)) .SingleOrDefaultAsync(); // If we are testing, see if we have an id in the our test collection if (!doWrite && text == null && this._testTextDataIdCollection.Contains(textId)) { // Build a hollow text data object for test purposes text = new TextData() { Id = textId }; } // If we have the specified id in the database if (text != null) { // Create with new _id for source text, deleting the old one // You cannot rename _id's, and the client will download a new object with the new id anyway string oldId = text.Id; string[] textIdParts = oldId.Split(':', StringSplitOptions.RemoveEmptyEntries); if (textIdParts.Length == 4 && textIdParts.Last() == "source") { string targetId = textIdParts.First(); if (sourceMapping.ContainsKey(targetId)) { text.Id = $"{sourceMapping[targetId]}:{textIdParts[1]}:{textIdParts[2]}:target"; if (!textIds.Contains(text.Id)) { textIds.Add(text.Id); Program.Log($"Rename TextData {oldId} to {text.Id}"); if (doWrite) { // NOTE: You cannot rename _id in MongoDB! // Delete from ShareDB IDocument <TextData> oldTextDoc = connection.Get <TextData>(oldId); await oldTextDoc.FetchAsync(); if (oldTextDoc.IsLoaded) { await oldTextDoc.DeleteAsync(); } // Remove from MongoDB await this.DeleteDocsAsync("texts", oldId); // Add the new text document via the real time service await connection.CreateAsync(text.Id, text); } } else { // Remove the source, as the it already exists as a target Program.Log($"TextData {text.Id} already exists, deleting {oldId}"); if (doWrite) { // Delete from ShareDB IDocument <TextData> oldTextDoc = connection.Get <TextData>(oldId); await oldTextDoc.FetchAsync(); if (oldTextDoc.IsLoaded) { await oldTextDoc.DeleteAsync(); } // Remove from MongoDB await this.DeleteDocsAsync("texts", oldId); } } } else { Program.Log($"Error Migrating TextData {text.Id} - Missing Source Mapping"); } } else { Program.Log($"Error Migrating TextData {text.Id} - Incorrect Identifier Format"); } } else { Program.Log($"Error Migrating TextData {textId} - Could Not Load From MongoDB"); } } }
/// <summary> /// Gets the permissions for a project or resource. /// </summary> /// <param name="userSecret">The user secret.</param> /// <param name="project">The project - the UserRoles and ParatextId are used.</param> /// <param name="ptUsernameMapping">A mapping of user ID to Paratext username.</param> /// <param name="book">The book number. Set to zero to check for all books.</param> /// <param name="chapter">The chapter number. Set to zero to check for all books.</param> /// <returns> /// A dictionary of permissions where the key is the user ID and the value is the permission. /// </returns> /// <remarks> /// See <see cref="TextInfoPermission" /> for permission values. /// A dictionary is returned, as permissions can be updated. /// </remarks> public async Task <Dictionary <string, string> > GetPermissionsAsync(UserSecret userSecret, SFProject project, IReadOnlyDictionary <string, string> ptUsernameMapping, int book = 0, int chapter = 0) { var permissions = new Dictionary <string, string>(); // See if the source is a resource if (project.ParatextId.Length == SFInstallableDblResource.ResourceIdentifierLength) { foreach (string uid in project.UserRoles.Keys) { permissions.Add(uid, await this.GetResourcePermissionAsync(project.ParatextId, uid)); } } else { // Get the scripture text so we can retrieve the permissions from the XML ScrText scrText = ScrTextCollection.FindById(GetParatextUsername(userSecret), project.ParatextId); // Calculate the project and resource permissions foreach (string uid in project.UserRoles.Keys) { // See if the user is in the project members list if (!ptUsernameMapping.TryGetValue(uid, out string userName) || string.IsNullOrWhiteSpace(userName) || scrText.Permissions.GetRole(userName) == Paratext.Data.Users.UserRoles.None) { permissions.Add(uid, TextInfoPermission.None); } else { string textInfoPermission = TextInfoPermission.Read; if (book == 0) { // Project level if (scrText.Permissions.CanEditAllBooks(userName)) { textInfoPermission = TextInfoPermission.Write; } } else if (chapter == 0) { // Book level IEnumerable <int> editable = scrText.Permissions.GetEditableBooks( Paratext.Data.Users.PermissionSet.Merged, userName); if (editable == null || !editable.Any()) { // If there are no editable book permissions, check if they can edit all books if (scrText.Permissions.CanEditAllBooks(userName)) { textInfoPermission = TextInfoPermission.Write; } } else if (editable.Contains(book)) { textInfoPermission = TextInfoPermission.Write; } } else { // Chapter level IEnumerable <int> editable = scrText.Permissions.GetEditableChapters(book, scrText.Settings.Versification, userName, Paratext.Data.Users.PermissionSet.Merged); if (editable?.Contains(chapter) ?? false) { textInfoPermission = TextInfoPermission.Write; } } permissions.Add(uid, textInfoPermission); } } } return(permissions); }