// 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 public_create (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; Database_Notes database_notes (webserver_request); Notes_Logic notes_logic = Notes_Logic (webserver_request); string page; Assets_Header header = Assets_Header (translate("Create note"), request); page += header.run (); Assets_View view; string bible = request->database_config_user()->getBible (); int book = Ipc_Focus::getBook (webserver_request); int chapter = Ipc_Focus::getChapter (webserver_request); int verse = Ipc_Focus::getVerse (webserver_request); string chapter_usfm = request->database_bibles()->getChapter (bible, book, chapter); string verse_usfm = usfm_get_verse_text (chapter_usfm, verse); string stylesheet = Database_Config_Bible::getExportStylesheet (bible); Filter_Text filter_text = Filter_Text (bible); filter_text.html_text_standard = new Html_Text (bible); filter_text.addUsfmCode (verse_usfm); filter_text.run (stylesheet); string versetext = filter_text.html_text_standard->getInnerHtml (); view.set_variable ("versetext", versetext); if (request->post.count ("submit")) { string summary = filter_string_trim (request->post["summary"]); if (summary.empty ()) summary = translate ("Feedback"); string contents = "<p>" + versetext + "</p>" + filter_string_trim (request->post["contents"]); int identifier = notes_logic.createNote (bible, book, chapter, verse, summary, contents, false); // A note created by a public user is made public to all. database_notes.setPublic (identifier, true); // Subscribe the user to the note. // Then the user receives email about any updates made on this note. database_notes.subscribe (identifier); // Go to the main public notes page. redirect_browser (request, public_index_url ()); return ""; } if (request->post.count ("cancel")) { redirect_browser (request, public_index_url ()); return ""; } string passage = filter_passage_display (book, chapter, convert_to_string (verse)); view.set_variable ("passage", passage); page += view.render ("public", "create"); page += Assets_Page::footer (); return page; }
// Returns the USFM text for a range of verses for the input $usfm code. // It handles combined verses. // It ensures that the $exclude_usfm does not make it to the output of the function. string usfm_get_verse_range_text (string usfm, int verse_from, int verse_to, const string& exclude_usfm) { vector <string> bits; string previous_usfm; for (int vs = verse_from; vs <= verse_to; vs++) { string verse_usfm = usfm_get_verse_text (usfm, vs); // Do not include repeating USFM in the case of combined verse numbers in the input USFM code. if (verse_usfm == previous_usfm) continue; previous_usfm = verse_usfm; // In case of combined verses, the excluded USFM should not be included in the result. if (verse_usfm != exclude_usfm) { bits.push_back (verse_usfm); } } usfm = filter_string_implode (bits, "\n"); return usfm; }
// This runs on the server. // It gets the html or text contents for a $resource for serving it to a client. string resource_logic_get_contents_for_client (string resource, int book, int chapter, int verse) { // Lists of the various types of resources. Database_UsfmResources database_usfmresources; vector <string> externals = resource_external_names (); vector <string> usfms = database_usfmresources.getResources (); // Possible SWORD details in case the client requests a SWORD resource. string sword_module = sword_logic_get_remote_module (resource); string sword_source = sword_logic_get_source (resource); // Determine the type of the current resource. bool isExternal = in_array (resource, externals); bool isUsfm = in_array (resource, usfms); bool isSword = (!sword_source.empty () && !sword_module.empty ()); if (isExternal) { // The server fetches it from the web. return resource_external_cloud_fetch_cache_extract (resource, book, chapter, verse); } if (isUsfm) { // Fetch from database and convert to html. string chapter_usfm = database_usfmresources.getUsfm (resource, book, chapter); string verse_usfm = usfm_get_verse_text (chapter_usfm, verse); string stylesheet = styles_logic_standard_sheet (); Filter_Text filter_text = Filter_Text (resource); filter_text.html_text_standard = new Html_Text (""); filter_text.addUsfmCode (verse_usfm); filter_text.run (stylesheet); return filter_text.html_text_standard->getInnerHtml (); } if (isSword) { // Fetch it from a SWORD module. return sword_logic_get_text (sword_source, sword_module, book, chapter, verse); } // Nothing found. return "Bibledit Cloud could not localize this resource"; }
string editverse_load (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; string bible = request->query ["bible"]; int book = convert_to_int (request->query ["book"]); int chapter = convert_to_int (request->query ["chapter"]); int verse = convert_to_int (request->query ["verse"]); string usfm = request->database_bibles()->getChapter (bible, book, chapter); usfm = usfm_get_verse_text (usfm, verse); usfm = filter_string_str_replace ("\n", "<br>", usfm); string chapter_verse_text; string needle = "\\c"; if (verse) needle = "\\v"; size_t pos = usfm.find (needle); if (pos != string::npos) { if (pos < 2) { usfm.erase (0, pos + 2); usfm = filter_string_trim (usfm); chapter_verse_text = usfm_peek_verse_number (usfm); usfm.erase (0, chapter_verse_text.length ()); usfm = filter_string_trim (usfm); } } usfm.insert (0, "<span contenteditable=\"true\">"); usfm.append ("</span>"); if (!chapter_verse_text.empty ()) { string spacer; if (verse) spacer = " "; usfm.insert (0, "<span " + filter_css_grey_background () + ">" + needle + " " + chapter_verse_text + spacer + "</span>"); } string user = request->session_logic ()->currentUser (); bool write = access_bible_book_write (webserver_request, user, bible, book); return Checksum_Logic::send (usfm, write); }
// This is the most basic version that fetches the text of a $resource. // It works on server and on client. // It uses the cache. string resource_logic_get_verse (void * webserver_request, string resource, int book, int chapter, int verse) { Webserver_Request * request = (Webserver_Request *) webserver_request; string data; Database_UsfmResources database_usfmresources; Database_ImageResources database_imageresources; // Lists of the various types of resources. // As from February 2016 a client no longer automatically downloads USFM resources from the Cloud. // But a client takes in account existing USFM resources it has downloaded before. vector <string> bibles = request->database_bibles()->getBibles (); vector <string> local_usfms = database_usfmresources.getResources (); vector <string> remote_usfms; if (config_logic_client_prepared ()) { remote_usfms = client_logic_usfm_resources_get (); } vector <string> externals = resource_external_names (); vector <string> images = database_imageresources.names (); vector <string> lexicons = lexicon_logic_resource_names (); // Possible SWORD details. string sword_module = sword_logic_get_remote_module (resource); string sword_source = sword_logic_get_source (resource); // Determine the type of the current resource. bool isBible = in_array (resource, bibles); bool isLocalUsfm = in_array (resource, local_usfms); bool isRemoteUsfm = in_array (resource, remote_usfms); bool isExternal = in_array (resource, externals); bool isImage = in_array (resource, images); bool isLexicon = in_array (resource, lexicons); bool isSword = (!sword_source.empty () && !sword_module.empty ()); if (isBible || isLocalUsfm) { string chapter_usfm; if (isBible) chapter_usfm = request->database_bibles()->getChapter (resource, book, chapter); if (isLocalUsfm) chapter_usfm = database_usfmresources.getUsfm (resource, book, chapter); string verse_usfm = usfm_get_verse_text (chapter_usfm, verse); string stylesheet = styles_logic_standard_sheet (); Filter_Text filter_text = Filter_Text (resource); filter_text.html_text_standard = new Html_Text (""); filter_text.addUsfmCode (verse_usfm); filter_text.run (stylesheet); data = filter_text.html_text_standard->getInnerHtml (); } else if (isRemoteUsfm) { data = resource_logic_client_fetch_cache_from_cloud (resource, book, chapter, verse); } else if (isExternal) { if (config_logic_client_prepared ()) { // A client fetches it from the cache or from the Cloud, // or, for older versions, from the offline resources database. // As of 12 December 2015, the offline resources database is not needed anymore. // It can be removed after a year or so. Database_OfflineResources database_offlineresources; if (database_offlineresources.exists (resource, book, chapter, verse)) { data = database_offlineresources.get (resource, book, chapter, verse); } else { data = resource_logic_client_fetch_cache_from_cloud (resource, book, chapter, verse); } } else { // The server fetches it from the web, via the http cache. data.append (resource_external_cloud_fetch_cache_extract (resource, book, chapter, verse)); } } else if (isImage) { vector <string> images = database_imageresources.get (resource, book, chapter, verse); for (auto & image : images) { data.append ("<div><img src=\"/resource/imagefetch?name=" + resource + "&image=" + image + "\" alt=\"Image resource\" style=\"width:100%\"></div>"); } } else if (isLexicon) { data = lexicon_logic_get_html (request, resource, book, chapter, verse); } else if (isSword) { data = sword_logic_get_text (sword_source, sword_module, book, chapter, verse); } else { // Nothing found. } // Any font size given in a paragraph style may interfere with the font size setting for the resources // as given in Bibledit. For that reason remove the class name from a paragraph style. for (unsigned int i = 0; i < 5; i++) { string fragment = "p class=\""; size_t pos = data.find (fragment); if (pos != string::npos) { size_t pos2 = data.find ("\"", pos + fragment.length () + 1); if (pos2 != string::npos) { data.erase (pos + 1, pos2 - pos + 1); } } } // NET Bible updates. data = filter_string_str_replace ("<span class=\"s ", "<span class=\"", data); return data; }
// Function to safely store a verse. // It saves the verse if the new USFM does not differ too much from the existing USFM. // On success it returns an empty message. // On failure it returns a message that specifies the reason why it could not be saved. // This function proves useful in cases that the text in the Bible editor gets corrupted // due to human error. // It also is useful in cases where the session is deleted from the server, // where the text in the editors would get corrupted. // It also is useful in view of an unstable connection between browser and server, to prevent data corruption. // It handles combined verses. string usfm_safely_store_verse (void * webserver_request, string bible, int book, int chapter, int verse, string usfm) { Webserver_Request * request = (Webserver_Request *) webserver_request; usfm = filter_string_trim (usfm); // Check that the USFM to be saved is for the correct verse. vector <int> save_verses = usfm_get_verse_numbers (usfm); if ((verse != 0) && !save_verses.empty ()) { save_verses.erase (save_verses.begin()); } if (save_verses.empty ()) { Database_Logs::log ("The USFM contains no verse information: " + usfm); return translate ("Missing verse number"); } if (!in_array (verse, save_verses)) { vector <string> vss; for (auto vs : save_verses) vss.push_back (convert_to_string (vs)); Database_Logs::log ("The USFM contains verse(s) " + filter_string_implode (vss, " ") + " while it wants to save to verse " + convert_to_string (verse) + ": " + usfm); return translate ("Verse mismatch"); } // Get the existing chapter USFM. string chapter_usfm = request->database_bibles()->getChapter (bible, book, chapter); // Get the existing USFM fragment for the verse to save. string existing_verse_usfm = usfm_get_verse_text (chapter_usfm, verse); existing_verse_usfm = filter_string_trim (existing_verse_usfm); // Check that there is a match between the existing verse numbers and the verse numbers to save. vector <int> existing_verses = usfm_get_verse_numbers (existing_verse_usfm); save_verses = usfm_get_verse_numbers (usfm); bool verses_match = true; if (save_verses.size () == existing_verses.size ()) { for (unsigned int i = 0; i < save_verses.size (); i++) { if (save_verses [i] != existing_verses [i]) verses_match = false; } } else { verses_match = false; } if (!verses_match) { vector <string> existing, save; for (auto vs : existing_verses) existing.push_back (convert_to_string (vs)); for (auto vs : save_verses) save.push_back (convert_to_string (vs)); Database_Logs::log ("The USFM contains verse(s) " + filter_string_implode (save, " ") + " which would overwrite a fragment that contains verse(s) " + filter_string_implode (existing, " ") + ": " + usfm); return translate ("Cannot overwrite another verse"); } // Bail out if the new USFM is the same as the existing. if (usfm == existing_verse_usfm) { return ""; } // Check maximum difference between new and existing USFM. string message = usfm_save_is_safe (webserver_request, existing_verse_usfm, usfm, false); if (!message.empty ()) return message; // Store the new verse USFM in the existing chapter USFM. size_t pos = chapter_usfm.find (existing_verse_usfm); size_t length = existing_verse_usfm.length (); chapter_usfm.erase (pos, length); chapter_usfm.insert (pos, usfm); // Safety checks have passed: Save chapter. Bible_Logic::storeChapter (bible, book, chapter, chapter_usfm); // Done: OK. return ""; }
string notes_notes (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; Database_Notes database_notes (webserver_request); string bible = access_bible_clamp (webserver_request, request->database_config_user()->getBible()); int book = Ipc_Focus::getBook (webserver_request); int chapter = Ipc_Focus::getChapter (webserver_request); int verse = Ipc_Focus::getVerse (webserver_request); int passage_selector = request->database_config_user()->getConsultationNotesPassageSelector(); int edit_selector = request->database_config_user()->getConsultationNotesEditSelector(); int non_edit_selector = request->database_config_user()->getConsultationNotesNonEditSelector(); string status_selector = request->database_config_user()->getConsultationNotesStatusSelector(); string bible_selector = request->database_config_user()->getConsultationNotesBibleSelector(); string assignment_selector = request->database_config_user()->getConsultationNotesAssignmentSelector(); bool subscription_selector = request->database_config_user()->getConsultationNotesSubscriptionSelector(); int severity_selector = request->database_config_user()->getConsultationNotesSeveritySelector(); int text_selector = request->database_config_user()->getConsultationNotesTextSelector(); string search_text = request->database_config_user()->getConsultationNotesSearchText(); int passage_inclusion_selector = request->database_config_user()->getConsultationNotesPassageInclusionSelector(); int text_inclusion_selector = request->database_config_user()->getConsultationNotesTextInclusionSelector(); // The Bibles the current user has access to. vector <string> bibles = access_bible_bibles (webserver_request, request->session_logic()->currentUser ()); // The admin disables notes selection on Bibles, // so the admin sees all notes, including notes referring to non-existing Bibles. if (request->session_logic ()->currentLevel () == Filter_Roles::admin ()) bibles.clear (); vector <int> identifiers = database_notes.selectNotes (bibles, book, chapter, verse, passage_selector, edit_selector, non_edit_selector, status_selector, bible_selector, assignment_selector, subscription_selector, severity_selector, text_selector, search_text, -1); // In case there aren't too many notes, there's enough time to sort them in passage order. if (identifiers.size () <= 200) { vector <int> passage_sort_keys; for (auto & identifier : identifiers) { int passage_sort_key = 0; vector <float> numeric_passages; vector <Passage> passages = database_notes.getPassages (identifier); for (auto & passage : passages) { numeric_passages.push_back (filter_passage_to_integer (passage)); } if (!numeric_passages.empty ()) { float average = accumulate (numeric_passages.begin (), numeric_passages.end (), 0) / numeric_passages.size (); passage_sort_key = round (average); } passage_sort_keys.push_back (passage_sort_key); } quick_sort (passage_sort_keys, identifiers, 0, identifiers.size ()); } string notesblock; for (auto & identifier : identifiers) { string summary = database_notes.getSummary (identifier); vector <Passage> passages = database_notes.getPassages (identifier); string verses = filter_passage_display_inline (passages); // A simple way to make it easier to see the individual notes in the list, // when the summaries of some notes are long, is to display the passage first. summary.insert (0, verses + " | "); string verse_text; if (passage_inclusion_selector) { vector <Passage> passages = database_notes.getPassages (identifier); for (auto & passage : passages) { string usfm = request->database_bibles()->getChapter (bible, passage.book, passage.chapter); string text = usfm_get_verse_text (usfm, convert_to_int (passage.verse)); if (!verse_text.empty ()) verse_text.append ("<br>"); verse_text.append (text); } } string content; if (text_inclusion_selector) { content = database_notes.getContents (identifier); } notesblock.append ("<a name=\"note" + convert_to_string (identifier) + "\"></a>\n"); notesblock.append ("<p><a href=\"note?id=" + convert_to_string (identifier) + "\">" + summary + "</a></p>\n"); if (!verse_text.empty ()) notesblock.append ("<p>" + verse_text + "</p>\n"); if (!content.empty ()) notesblock.append ("<p>" + content + "</p>\n"); } if (identifiers.empty ()) { return translate("This selection does not display any notes."); } return notesblock; }
// Indexes a $bible $book $chapter for searching. void search_logic_index_chapter (string bible, int book, int chapter) { Database_Bibles database_bibles; string usfm = database_bibles.getChapter (bible, book, chapter); string stylesheet = Database_Config_Bible::getExportStylesheet (bible); vector <string> index; set <string> already_processed; vector <int> verses = usfm_get_verse_numbers (usfm); for (auto verse : verses) { string raw_usfm = filter_string_trim (usfm_get_verse_text (usfm, verse)); // In case of combined verses, the bit of USFM may have been indexed already. // Skip it in that case. if (already_processed.find (raw_usfm) != already_processed.end ()) continue; already_processed.insert (raw_usfm); index.push_back (search_logic_verse_separator ()); index.push_back (convert_to_string (verse)); index.push_back (search_logic_index_separator ()); index.push_back (raw_usfm); string usfm_lower = unicode_string_casefold (raw_usfm); index.push_back (search_logic_index_separator ()); index.push_back (usfm_lower); // Text filter for getting the plain text. Filter_Text filter_text = Filter_Text (bible); filter_text.text_text = new Text_Text (); filter_text.initializeHeadingsAndTextPerVerse (true); filter_text.addUsfmCode (raw_usfm); filter_text.run (stylesheet); string raw_plain; // Add the clean verse texts. map <int, string> texts = filter_text.getVersesText (); for (auto & element : texts) { raw_plain.append (element.second + "\n"); } // Add any clean headings. map <int, string> headings = filter_text.verses_headings; for (auto & element : headings) { raw_plain.append (element.second + "\n"); } // Add any footnotes. raw_plain.append (filter_text.text_text->getnote ()); // Clean up. raw_plain = filter_string_trim (raw_plain); index.push_back (search_logic_index_separator ()); index.push_back (raw_plain); string plain_lower = unicode_string_casefold (raw_plain); index.push_back (search_logic_index_separator ()); index.push_back (plain_lower); } index.push_back (search_logic_index_separator ()); // Store everything. string path = search_logic_chapter_file (bible, book, chapter); filter_url_file_put_contents (path, filter_string_implode (index, "\n")); }
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 ()); }