// Helper function. // $user: The user whose changes are being processed. // $recipients: The users who opted to receive online notifications of any contributors. void changes_process_identifiers (Webserver_Request * request, string user, vector <string> recipients, string bible, int book, int chapter, int oldId, int newId, string & email) { if (oldId != 0) { recipients = filter_string_array_diff (recipients, {user}); Database_Modifications database_modifications; string stylesheet = Database_Config_Bible::getExportStylesheet (bible); Database_Modifications_Text old_chapter_text = database_modifications.getUserChapter (user, bible, book, chapter, oldId); string old_chapter_usfm = old_chapter_text.oldtext; Database_Modifications_Text new_chapter_text = database_modifications.getUserChapter (user, bible, book, chapter, newId); string new_chapter_usfm = new_chapter_text.newtext; //int timestamp = database_modifications.getUserTimestamp (user, bible, book, chapter, newId); vector <int> old_verse_numbers = usfm_get_verse_numbers (old_chapter_usfm); vector <int> new_verse_numbers = usfm_get_verse_numbers (new_chapter_usfm); vector <int> verses = old_verse_numbers; verses.insert (verses.end (), new_verse_numbers.begin (), new_verse_numbers.end ()); verses = array_unique (verses); sort (verses.begin(), verses.end()); for (auto verse : verses) { string old_verse_usfm = usfm_get_verse_text (old_chapter_usfm, verse); string new_verse_usfm = usfm_get_verse_text (new_chapter_usfm, verse); if (old_verse_usfm != new_verse_usfm) { Filter_Text filter_text_old = Filter_Text (bible); Filter_Text filter_text_new = Filter_Text (bible); filter_text_old.html_text_standard = new Html_Text (translate("Bible")); filter_text_new.html_text_standard = new Html_Text (translate("Bible")); filter_text_old.text_text = new Text_Text (); filter_text_new.text_text = new Text_Text (); filter_text_old.addUsfmCode (old_verse_usfm); filter_text_new.addUsfmCode (new_verse_usfm); filter_text_old.run (stylesheet); filter_text_new.run (stylesheet); string old_html = filter_text_old.html_text_standard->getInnerHtml (); string new_html = filter_text_new.html_text_standard->getInnerHtml (); string old_text = filter_text_old.text_text->get (); string new_text = filter_text_new.text_text->get (); if (old_text != new_text) { string modification = filter_diff_diff (old_text, new_text); email += "<div>"; email += filter_passage_display (book, chapter, convert_to_string (verse)); email += " "; email += modification; email += "</div>"; if (request->database_config_user()->getUserUserChangesNotificationsOnline (user)) { database_modifications.recordNotification ({user}, changes_personal_category (), bible, book, chapter, verse, old_html, modification, new_html); } for (auto recipient : recipients) { if (recipient == user) continue; database_modifications.recordNotification ({recipient}, user, bible, book, chapter, verse, old_html, modification, new_html); } } } } } }
string xrefs_translate (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; string sourceBible = request->database_config_user()->getSourceXrefBible (); string targetBible = request->database_config_user()->getTargetXrefBible (); // Save book / abbreviation pair. if (request->post.count ("save")) { string fullname = request->post ["fullname"]; string abbreviation = request->post ["abbreviation"]; string abbreviations = Database_Config_Bible::getBookAbbreviations (targetBible); abbreviations = filter_abbreviations_add (abbreviations, fullname, abbreviation); Database_Config_Bible::setBookAbbreviations (targetBible, abbreviations); } string abbrevs = Database_Config_Bible::getBookAbbreviations (sourceBible); vector <pair <int, string> > sourceAbbreviations = filter_abbreviations_read (abbrevs); vector <int> sourceBooks; for (auto element : sourceAbbreviations) sourceBooks.push_back (element.first); abbrevs = Database_Config_Bible::getBookAbbreviations (targetBible); vector <pair <int, string> > targetAbbreviations = filter_abbreviations_read (abbrevs); vector <int> targetBooks; for (auto element : targetAbbreviations) targetBooks.push_back (element.first); vector <int> unknown_books = filter_string_array_diff (sourceBooks, targetBooks); unknown_books = array_unique (unknown_books); if (unknown_books.empty ()) { redirect_browser (request, xrefs_clear_url ()); return ""; } string page; Assets_Header header = Assets_Header (translate("Cross references"), webserver_request); page = header.run (); Assets_View view; view.set_variable ("remaining", convert_to_string (unknown_books.size () - 1)); string bookname = Database_Books::getEnglishFromId (unknown_books [0]); view.set_variable ("bookname", bookname); page += view.render ("xrefs", "translate"); page += Assets_Page::footer (); return page; }
// Maintains the database with note assignees. // $force: Force maintenance. void notes_logic_maintain_note_assignees (bool force) { Database_NoteAssignment database_noteassignment; Webserver_Request webserver_request; Database_Users database_users; vector <string> users = database_users.getUsers (); // If even one user's assignees are absent, force rebuilding them for all users. for (auto & user : users) { if (!database_noteassignment.exists (user)) force = true; } if (!force) return; Database_Bibles database_bibles; vector <string> bibles = database_bibles.getBibles (); // A user can assign notes to other users // who have access to the Bibles the user has access to. for (auto & user : users) { vector <string> assignees; for (auto & bible : bibles) { // Continue with this Bible if the user has access to it. if (access_bible_read (&webserver_request, bible, user)) { for (auto & assignee : users) { if (access_bible_read (&webserver_request, bible, assignee)) { assignees.push_back (assignee); } } } } assignees = array_unique (assignees); database_noteassignment.assignees (user, assignees); } }
string search_search2 (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; Database_Volatile database_volatile = Database_Volatile (); string siteUrl = config_logic_site_url (); string bible = request->database_config_user()->getBible (); if (request->query.count ("bible")) bible = request->query ["bible"]; bool hit_is_set = request->query.count ("h"); bool query_is_set = request->query.count ("q"); int identifier = convert_to_int (request->query ["i"]); string query = request->query ["q"]; string hit = request->query ["h"]; // Get one search hit. if (hit_is_set) { // Retrieve the search parameters from the volatile database. string query = database_volatile.getValue (identifier, "query"); //bool casesensitive = convert_to_bool (database_volatile.getValue (identifier, "casesensitive")); bool plaintext = convert_to_bool (database_volatile.getValue (identifier, "plaintext")); // Get the Bible and passage for this identifier. Passage details = Passage::from_text (hit); string bible = details.bible; int book = details.book; int chapter = details.chapter; string verse = details.verse; // Get the plain text or USFM. string text; if (plaintext) { text = search_logic_get_bible_verse_text (bible, book, chapter, convert_to_int (verse)); } else { text = search_logic_get_bible_verse_usfm (bible, book, chapter, convert_to_int (verse)); } // Format it. string link = filter_passage_link_for_opening_editor_at (book, chapter, verse); text = filter_string_markup_words ({query}, text); string output = "<div>" + link + " " + text + "</div>"; // Output to browser. return output; } // Perform the initial search. if (query_is_set) { // Get extra search parameters and store them all in the volatile database. bool casesensitive = (request->query ["c"] == "true"); bool plaintext = (request->query ["p"] == "true"); bool currentbook = (request->query ["b"] == "true"); string sharing = request->query ["s"]; database_volatile.setValue (identifier, "query", query); database_volatile.setValue (identifier, "casesensitive", convert_to_string (casesensitive)); database_volatile.setValue (identifier, "plaintext", convert_to_string (plaintext)); // Deal with case sensitivity. // Deal with whether to search the plain text, or the raw USFM. // Fetch the initial set of hits. vector <Passage> passages; if (plaintext) { if (casesensitive) { passages = search_logic_search_bible_text_case_sensitive (bible, query); } else { passages = search_logic_search_bible_text (bible, query); } } else { if (casesensitive) { passages = search_logic_search_bible_usfm_case_sensitive (bible, query); } else { passages = search_logic_search_bible_usfm (bible, query); } } // Deal with possible searching in the current book only. if (currentbook) { int book = Ipc_Focus::getBook (request); vector <Passage> bookpassages; for (auto & passage : passages) { if (book == passage.book) { bookpassages.push_back (passage); } } passages = bookpassages; } // Deal with how to share the results. vector <string> hits; for (auto & passage : passages) { hits.push_back (passage.to_text ()); } if (sharing != "load") { vector <string> loaded_hits = filter_string_explode (database_volatile.getValue (identifier, "hits"), '\n'); if (sharing == "add") { hits.insert (hits.end(), loaded_hits.begin(), loaded_hits.end()); } if (sharing == "remove") { hits = filter_string_array_diff (loaded_hits, hits); } if (sharing == "intersect") { hits = array_intersect (loaded_hits, hits); } hits = array_unique (hits); } // Generate one string from the hits. string output = filter_string_implode (hits, "\n"); // Store search hits in the volatile database. database_volatile.setValue (identifier, "hits", output); // Output results. return output; } // Build the advanced search page. string page; Assets_Header header = Assets_Header (translate("Search"), request); header.setNavigator (); header.addBreadCrumb (menu_logic_search_menu (), menu_logic_search_text ()); page = header.run (); Assets_View view; view.set_variable ("bible", bible); string script = "var searchBible = \"" + bible + "\";"; view.set_variable ("script", script); page += view.render ("search", "search2"); page += Assets_Page::footer (); return page; }
string search_strong (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; Database_Kjv database_kjv = Database_Kjv (); string bible = request->database_config_user()->getBible (); if (request->query.count ("b")) { bible = request->query ["b"]; } if (request->query.count ("load")) { int book = Ipc_Focus::getBook (request); int chapter = Ipc_Focus::getChapter (request); int verse = Ipc_Focus::getVerse (request); // Get Strong's numbers, plus English snippets. string html = "<table>\n"; vector <Database_Kjv_Item> details = database_kjv.getVerse (book, chapter, verse); for (auto & detail : details) { string strong = detail.strong; string english = detail.english; html += "<tr><td><a href=\"" + strong + "\">" + strong + "</a></td><td>" + english + "</td></tr>\n"; } html += "</table>\n"; return html; } if (request->query.count ("strong")) { string strong = request->query ["strong"]; strong = filter_string_trim (strong); vector <int> passages; vector <Passage> details = database_kjv.searchStrong (strong); for (auto & passage : details) { int i_passage = filter_passage_to_integer (passage); passages.push_back (i_passage); } passages = array_unique (passages); sort (passages.begin(), passages.end()); string output; for (auto & passage : passages) { if (!output.empty()) output.append ("\n"); output.append (convert_to_string (passage)); } return output; } if (request->query.count ("id")) { int id = convert_to_int (request->query ["id"]); // Get the and passage for this identifier. Passage passage = filter_integer_to_passage (id); int book = passage.book; int chapter = passage.chapter; string verse = passage.verse; // Get the plain text. string text = search_logic_get_bible_verse_text (bible, book, chapter, convert_to_int (verse)); // Format it. string link = filter_passage_link_for_opening_editor_at (book, chapter, verse); string output = "<div>" + link + " " + text + "</div>"; // Output to browser. return output; } string page; Assets_Header header = Assets_Header (translate("Search"), request); header.setNavigator (); header.addBreadCrumb (menu_logic_search_menu (), menu_logic_search_text ()); page = header.run (); Assets_View view; view.set_variable ("bible", bible); string script = "var searchBible = \"" + bible + "\";"; view.set_variable ("script", script); page += view.render ("search", "strong"); page += Assets_Page::footer (); return page; }
void changes_modifications () { Database_Logs::log ("Change notifications: Generating", Filter_Roles::translator ()); // Notifications are not available to clients for the duration of processing them. config_globals_change_notifications_available = false; // Data objects. Webserver_Request request; Database_Modifications database_modifications; // Check on the health of the modifications database and (re)create it if needed. if (!database_modifications.healthy ()) database_modifications.erase (); database_modifications.create (); // Create online change notifications for users who made changes in Bibles // through the web editor or through a client. // It runs before the team changes. // This produces the desired order of the notifications in the GUI. // At the same time, produce change statistics per user. // Get the users who will receive the changes entered by the contributors. vector <string> recipients; { vector <string> users = request.database_users ()->getUsers (); for (auto & user : users) { if (request.database_config_user ()->getContributorChangesNotificationsOnline (user)) { recipients.push_back (user); } } } // Storage for the statistics. map <string, int> user_change_statistics; float modification_time_total = 0; int modification_time_count = 0; vector <string> users = database_modifications.getUserUsernames (); if (!users.empty ()) Database_Logs::log ("Change notifications: Per user", Filter_Roles::translator ()); for (auto user : users) { // Total changes made by this user. int change_count = 0; // Go through the Bibles changed by the current user. vector <string> bibles = database_modifications.getUserBibles (user); for (auto bible : bibles) { // Body of the email to be sent. string email = "<p>" + translate("You have entered the changes below in a Bible editor.") + " " + translate ("You may check if it made its way into the Bible text.") + "</p>"; size_t empty_email_length = email.length (); // Go through the books in that Bible. vector <int> books = database_modifications.getUserBooks (user, bible); for (auto book : books) { // Go through the chapters in that book. vector <int> chapters = database_modifications.getUserChapters (user, bible, book); for (auto chapter : chapters) { // Get the sets of identifiers for that chapter, and set some variables. vector <Database_Modifications_Id> IdSets = database_modifications.getUserIdentifiers (user, bible, book, chapter); //int referenceOldId = 0; int referenceNewId = 0; int newId = 0; int lastNewId = 0; bool restart = true; // Go through the sets of identifiers. for (auto IdSet : IdSets) { int oldId = IdSet.oldid; newId = IdSet.newid; if (restart) { changes_process_identifiers (&request, user, recipients, bible, book, chapter, referenceNewId, newId, email, change_count, modification_time_total, modification_time_count); //referenceOldId = oldId; referenceNewId = newId; lastNewId = newId; restart = false; continue; } if (oldId == lastNewId) { lastNewId = newId; } else { restart = true; } } // Process the last set of identifiers. changes_process_identifiers (&request, user, recipients, bible, book, chapter, referenceNewId, newId, email, change_count, modification_time_total, modification_time_count); } } // Check whether there's any email to be sent. if (email.length () != empty_email_length) { // Send the user email with the user's personal changes if the user opted to receive it. if (request.database_config_user()->getUserUserChangesNotification (user)) { string subject = translate("Changes you entered in") + " " + bible; if (!client_logic_client_enabled ()) email_schedule (user, subject, email); } } } // Store change statistics for this user. user_change_statistics [user] = change_count; // Clear the user's changes in the database. database_modifications.clearUserUser (user); // Clear checksum cache. request.database_config_user ()->setUserChangeNotificationsChecksum (user, ""); } // Generate the notifications, online and by email, // for the changes in the Bibles entered by anyone since the previous notifications were generated. vector <string> bibles = database_modifications.getTeamDiffBibles (); for (auto bible : bibles) { string stylesheet = Database_Config_Bible::getExportStylesheet (bible); vector <string> changeNotificationUsers; vector <string> users = request.database_users ()->getUsers (); for (auto user : users) { if (access_bible_read (&request, bible, user)) { if (request.database_config_user()->getUserGenerateChangeNotifications (user)) { changeNotificationUsers.push_back (user); } } } users.clear (); // The number of changes processed so far for this Bible. int processedChangesCount = 0; // The files get stored at http://site.org:<port>/revisions/<Bible>/<date> int seconds = filter_date_seconds_since_epoch (); string timepath; timepath.append (convert_to_string (filter_date_numerical_year (seconds))); timepath.append ("-"); timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_month (seconds)), 2, '0')); timepath.append ("-"); timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_month_day (seconds)), 2, '0')); timepath.append (" "); timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_hour (seconds)), 2, '0')); timepath.append (":"); timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_minute (seconds)), 2, '0')); timepath.append (":"); timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_second (seconds)), 2, '0')); string directory = filter_url_create_root_path ("revisions", bible, timepath); filter_url_mkdir (directory); // Produce the USFM and html files. filter_diff_produce_verse_level (bible, directory); // Create online page with changed verses. string versesoutputfile = filter_url_create_path (directory, "changed_verses.html"); filter_diff_run_file (filter_url_create_path (directory, "verses_old.txt"), filter_url_create_path (directory, "verses_new.txt"), versesoutputfile); // Storage for body of the email with the changes. vector <string> email_changes; // Generate the online change notifications. vector <int> books = database_modifications.getTeamDiffBooks (bible); for (auto book : books) { vector <int> chapters = database_modifications.getTeamDiffChapters (bible, book); for (auto chapter : chapters) { Database_Logs::log ("Change notifications: " + bible + " " + filter_passage_display (book, chapter, ""), Filter_Roles::translator ()); string old_chapter_usfm = database_modifications.getTeamDiff (bible, book, chapter); string new_chapter_usfm = request.database_bibles()->getChapter (bible, book, chapter); vector <int> old_verse_numbers = usfm_get_verse_numbers (old_chapter_usfm); vector <int> new_verse_numbers = usfm_get_verse_numbers (new_chapter_usfm); vector <int> verses = old_verse_numbers; verses.insert (verses.end (), new_verse_numbers.begin (), new_verse_numbers.end ()); verses = array_unique (verses); sort (verses.begin (), verses.end()); for (auto verse : verses) { string old_verse_usfm = usfm_get_verse_text (old_chapter_usfm, verse); string new_verse_usfm = usfm_get_verse_text (new_chapter_usfm, verse); if (old_verse_usfm != new_verse_usfm) { processedChangesCount++; // In case of too many change notifications, processing them would take too much time, so take a few shortcuts. string old_html = "<p>" + old_verse_usfm + "</p>"; string new_html = "<p>" + new_verse_usfm + "</p>"; string old_text = old_verse_usfm; string new_text = new_verse_usfm; if (processedChangesCount < 800) { Filter_Text filter_text_old = Filter_Text (bible); Filter_Text filter_text_new = Filter_Text (bible); filter_text_old.html_text_standard = new Html_Text (""); filter_text_new.html_text_standard = new Html_Text (""); filter_text_old.text_text = new Text_Text (); filter_text_new.text_text = new Text_Text (); filter_text_old.addUsfmCode (old_verse_usfm); filter_text_new.addUsfmCode (new_verse_usfm); filter_text_old.run (stylesheet); filter_text_new.run (stylesheet); old_html = filter_text_old.html_text_standard->getInnerHtml (); new_html = filter_text_new.html_text_standard->getInnerHtml (); old_text = filter_text_old.text_text->get (); new_text = filter_text_new.text_text->get (); } string modification = filter_diff_diff (old_text, new_text); database_modifications.recordNotification (changeNotificationUsers, changes_bible_category (), bible, book, chapter, verse, old_html, modification, new_html); string passage = filter_passage_display (book, chapter, convert_to_string (verse)) + ": "; if (old_text != new_text) { email_changes.push_back (passage + modification); } else { email_changes.push_back (translate ("The following passage has no change in the text.") + " " + translate ("The change is in the formatting only.") + " " + translate ("The USFM code is given for reference.")); email_changes.push_back (passage); email_changes.push_back (translate ("Old code:") + " " + old_verse_usfm); email_changes.push_back (translate ("New code:") + " " + new_verse_usfm); } } } // Delete the diff data for this chapter, for two reasons: // 1. New diffs for this chapter can be stored straightaway. // 2. In case of large amounts of diff data, and this function gets killed, // then the next time it runs again, it will continue from where it was killed. database_modifications.deleteTeamDiffChapter (bible, book, chapter); } } // Email the changes to the subscribed users. if (!email_changes.empty ()) { string body; for (auto & line : email_changes) { body.append ("<div>"); body.append (line); body.append ("</div>\n"); } string subject = translate("Recent changes:") + " " + bible; vector <string> users = request.database_users ()->getUsers (); for (auto & user : users) { if (request.database_config_user()->getUserBibleChangesNotification (user)) { if (access_bible_read (&request, bible, user)) { if (!client_logic_client_enabled ()) { email_schedule (user, subject, body); } } } } } } // Index the data and remove expired notifications. Database_Logs::log ("Change notifications: Indexing", Filter_Roles::translator ()); database_modifications.indexTrimAllNotifications (); // Remove expired downloadable revisions. string directory = filter_url_create_root_path ("revisions"); int now = filter_date_seconds_since_epoch (); bibles = filter_url_scandir (directory); for (auto &bible : bibles) { string folder = filter_url_create_path (directory, bible); int time = filter_url_file_modification_time (folder); int days = (now - time) / 86400; if (days > 31) { filter_url_rmdir (folder); } else { vector <string> revisions = filter_url_scandir (folder); for (auto & revision : revisions) { string path = filter_url_create_path (folder, revision); int time = filter_url_file_modification_time (path); int days = (now - time) / 86400; if (days > 31) { filter_url_rmdir (path); Database_Logs::log ("Removing expired downloadable revision notification: " + bible + " " + revision, Filter_Roles::translator ()); } } } } // Clear checksum caches. users = request.database_users ()->getUsers (); for (auto user : users) { request.database_config_user ()->setUserChangeNotificationsChecksum (user, ""); } // Vacuum the modifications index, as it might have been updated. database_modifications.vacuum (); // Make the notifications available again to clients. config_globals_change_notifications_available = true; // Store the statistics in the database. if (modification_time_count) { // Take average timestamp of all timestamps. int timestamp = round (modification_time_total / modification_time_count); for (auto & element : user_change_statistics) { // Store dated change statistics per user. string user = element.first; int count = element.second; Database_Statistics::store_changes (timestamp, user, count); } } Database_Logs::log ("Change notifications: Ready", Filter_Roles::translator ()); }