// This provides a html link to Bibledit Cloud / $path, and displays $linktext. string client_logic_link_to_cloud (string path, string linktext) { string url; if (client_logic_client_enabled ()) { string address = Database_Config_General::getServerAddress (); int port = Database_Config_General::getServerPort (); url = address + ":" + convert_to_string (port); if (!path.empty ()) { url.append ("/"); url.append (path); } } else { // Client disconnected: Provide the link and the text to connect to the Cloud. url = "/" + client_index_url (); linktext.append (" "); linktext.append (translate("You are not yet connected to Bibledit Cloud.")); linktext.append (" "); linktext.append (translate("Connect.")); } if (linktext.empty ()) { // Empty link text: Select the link itself as the text to display. linktext = url; } string link = "<a href=\"" + url + "\">" + linktext + "</a>"; return link; }
// If $minute is negative, it syncs. // If $minute >=0, it determines from the settings whether to sync. void sendreceive_queue_sync (int minute) { // Deal with a numerical minute to find out whether it's time to automatically sync. if (minute >= 0) { int repeat = Database_Config_General::getRepeatSendReceive (); // Sync every hour. if (repeat == 1) { minute = minute % 60; if (minute == 0) minute = -1; } // Sync every five minutes. if (repeat == 2) { minute = minute % 5; if (minute == 0) minute = -1; } } // Send and receive: It is time now, or it is manual. // Only queue a sync task if it is not running at the moment. if (minute < 0) { // Send / receive only works in Client mode. if (client_logic_client_enabled ()) { if (tasks_logic_queued (SYNCBIBLES)) { Database_Logs::log ("Still synchronizing Bibles"); } else { tasks_logic_queue (SYNCBIBLES); } if (tasks_logic_queued (SYNCNOTES)) { Database_Logs::log ("Still synchronizing Notes"); } else { tasks_logic_queue (SYNCNOTES); } if (tasks_logic_queued (SYNCSETTINGS)) { Database_Logs::log ("Still synchronizing Settings"); } else { tasks_logic_queue (SYNCSETTINGS); } if (tasks_logic_queued (SYNCCHANGES)) { Database_Logs::log ("Still synchronizing Changes"); } else { tasks_logic_queue (SYNCCHANGES); } if (tasks_logic_queued (SYNCFILES)) { Database_Logs::log ("Still synchronizing Files"); } else { tasks_logic_queue (SYNCFILES); } // Sync resources always, because it checks on its own whether to do something. tasks_logic_queue (SYNCRESOURCES); } // Store the most recent time that the sync action ran. Database_Config_General::setLastSendReceive (filter_date_seconds_since_epoch ()); } }
bool journal_index_acl (void * webserver_request) { // In Client mode, anyone can view the journal. if (client_logic_client_enabled ()) { return true; } // The role of Consultant or higher can view the journal. if (Filter_Roles::access_control (webserver_request, Filter_Roles::consultant ())) { return true; } // No access. return false; }
// Sets the bible for note identifier. void Notes_Logic::setBible (int identifier, const string& bible) { Database_Notes database_notes (webserver_request); database_notes.setBible (identifier, bible); if (client_logic_client_enabled ()) { // Client: record the action in the database. string user = ((Webserver_Request *) webserver_request)->session_logic ()->currentUser (); Database_NoteActions database_noteactions = Database_NoteActions (); database_noteactions.record (user, identifier, Sync_Logic::notes_put_bible, ""); } else { // Server: do nothing extra. } }
void Notes_Logic::unmarkForDeletion (int identifier) { Database_Notes database_notes (webserver_request); database_notes.unmarkForDeletion (identifier); if (client_logic_client_enabled ()) { // Client: record the action in the database. string user = ((Webserver_Request *) webserver_request)->session_logic ()->currentUser (); Database_NoteActions database_noteactions = Database_NoteActions (); database_noteactions.record (user, identifier, Sync_Logic::notes_put_unmark_delete, ""); } else { // Server: do nothing extra. } }
string basic_index (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; Assets_Header header = Assets_Header ("Settings", webserver_request); string page = header.run (); Assets_View view; if (request->query.count ("changebible")) { string changebible = request->query ["changebible"]; if (changebible == "") { Dialog_List dialog_list = Dialog_List ("index", translate("Select which Bible to make the active one for editing"), "", ""); vector <string> bibles = access_bible_bibles (request); for (auto & bible : bibles) { dialog_list.add_row (bible, "changebible", bible); } page += dialog_list.run(); return page; } else { request->database_config_user()->setBible (changebible); // Going to another Bible, ensure that the focused book exists there. int book = Ipc_Focus::getBook (request); vector <int> books = request->database_bibles()->getBooks (changebible); if (find (books.begin(), books.end(), book) == books.end()) { if (!books.empty ()) book = books [0]; else book = 0; Ipc_Focus::set (request, book, 1, 1); } } } string bible = access_bible_clamp (request, request->database_config_user()->getBible ()); view.set_variable ("bible", bible); #ifdef CLIENT_PREPARED view.enable_zone ("client"); if (client_logic_client_enabled ()) { view.enable_zone ("connected"); } #else view.enable_zone ("cloud"); #endif page += view.render ("basic", "index"); page += Assets_Page::footer (); return page; }
void Notes_Logic::assignUser (int identifier, const string& user) { Database_Notes database_notes (webserver_request); if (client_logic_client_enabled ()) { // Client: record the action in the database. string myuser = ((Webserver_Request *) webserver_request)->session_logic ()->currentUser (); Database_NoteActions database_noteactions; database_noteactions.record (myuser, identifier, Sync_Logic::notes_put_assign, user); } else { // Server: do the notifications. // Assign logic comes before the database action in this particular case. handlerAssignNote (identifier, user); } database_notes.assignUser (identifier, user); }
void Notes_Logic::erase (int identifier) { Database_Notes database_notes (webserver_request); if (client_logic_client_enabled ()) { // Client: record the action in the database. string user = ((Webserver_Request *) webserver_request)->session_logic ()->currentUser (); Database_NoteActions database_noteactions = Database_NoteActions (); database_noteactions.record (user, identifier, Sync_Logic::notes_put_delete, ""); } else { // Server: notification. handlerDeleteNote (identifier); } trash_consultation_note (webserver_request, identifier); database_notes.erase (identifier); }
void Notes_Logic::handlerAssignNote (int identifier, const string& user) { // Take no action in client mode. if (client_logic_client_enabled ()) return; Database_Config_User database_config_user = Database_Config_User (webserver_request); if (database_config_user.getUserAssignedConsultationNoteNotification (user)) { // Only email the user if the user was not yet assigned this note. Database_Notes database_notes (webserver_request); vector <string> assignees = database_notes.getAssignees (identifier); if (find (assignees.begin(), assignees.end(), user) == assignees.end()) { emailUsers (identifier, translate("Assigned"), {user}, false); } } }
// Add a comment to an exiting note identified by identifier. void Notes_Logic::addComment (int identifier, const string& comment) { // Do nothing if there was no content. if (comment == "") return; Database_Notes database_notes (webserver_request); database_notes.addComment (identifier, comment); if (client_logic_client_enabled ()) { // Client: record the action in the database. string user = ((Webserver_Request *) webserver_request)->session_logic ()->currentUser (); Database_NoteActions database_noteactions = Database_NoteActions (); database_noteactions.record (user, identifier, Sync_Logic::notes_put_comment, comment); } else { // Server: do the notifications. handlerAddComment (identifier); } }
// Returns a warning in case the client is connected to the open demo server. string demo_client_warning () { string warning; if (client_logic_client_enabled ()) { string address = Database_Config_General::getServerAddress (); if (address == demo_address ()) { int port = Database_Config_General::getServerPort (); if (port == demo_port ()) { warning.append (translate("You are connected to a public demo of Bibledit Cloud.")); warning.append (" "); warning.append (translate("Everybody can modify the data on that server.")); warning.append (" "); warning.append (translate("After send and receive your data will reflect the data on the server.")); } } } return warning; }
// The client runs this function to fetch a general resource $name from the Cloud, // or from its local cache, // and to update the local cache with the fetched content, if needed, // and to return the requested content. string resource_logic_client_fetch_cache_from_cloud (string resource, int book, int chapter, int verse) { // Ensure that the cache for this resource exists on the client. if (!Database_Cache::exists (resource, book)) { Database_Cache::create (resource, book); } // If the content exists in the cache, return that content. if (Database_Cache::exists (resource, book, chapter, verse)) { return Database_Cache::retrieve (resource, book, chapter, verse); } // Fetch this resource from Bibledit Cloud or from the cache. string address = Database_Config_General::getServerAddress (); int port = Database_Config_General::getServerPort (); if (!client_logic_client_enabled ()) { // If the client has not been connected to a cloud instance, // fetch the resource from the Bibledit Cloud demo. address = demo_address (); port = demo_port (); } string url = client_logic_url (address, port, sync_resources_url ()); url = filter_url_build_http_query (url, "r", filter_url_urlencode (resource)); url = filter_url_build_http_query (url, "b", convert_to_string (book)); url = filter_url_build_http_query (url, "c", convert_to_string (chapter)); url = filter_url_build_http_query (url, "v", convert_to_string (verse)); string error; string content = filter_url_http_get (url, error); if (error.empty ()) { // No error: Cache content. Database_Cache::cache (resource, book, chapter, verse, content); } else { // Error: Log it, and return it. Database_Logs::log (resource + ": " + error); content.append (error); } // Done. return content; }
// Deal with AJAX call for a possible new journal entry. string journal_index_ajax (Webserver_Request * request, string filename) { int userLevel = request->session_logic()->currentLevel (); // Sample filetime: "141708283400041520". // It is the number of seconds past the Unix epoch, plus the microseconds within the current second. Database_Logs database_logs = Database_Logs (); string result = database_logs.getNext (filename); if (!result.empty()) { int entryLevel = convert_to_int (result); // Cloud: Pay attention to only rendering journal entries of sufficient user level. // Client: Render any journal entry. if ((entryLevel <= userLevel) || client_logic_client_enabled ()) { result = render_journal_entry (result); } else { result.clear (); } result.insert (0, filename + "\n"); } return result; }
// Create a consultation note. // $bible: The notes's Bible. // $book, $chapter, $verse: The note's passage. // $summary: The note's summary. // $contents: The note's contents. // $raw: Import $contents as it is. // It returns the $identifier of this new note. int Notes_Logic::createNote (string bible, int book, int chapter, int verse, string summary, string contents, bool raw) { summary = filter_string_str_replace ("\n", "", summary); Database_Notes database_notes (webserver_request); int note_id = database_notes.storeNewNote (bible, book, chapter, verse, summary, contents, raw); if (client_logic_client_enabled ()) { // Client: record the action in the database. Database_NoteActions database_noteactions; Webserver_Request * request = (Webserver_Request *) webserver_request; database_noteactions.record (request->session_logic()->currentUser (), note_id, Sync_Logic::notes_put_create_initiate, ""); database_noteactions.record (request->session_logic()->currentUser (), note_id, Sync_Logic::notes_put_summary, ""); // The contents to submit to the server, take it from the database, as it was updated in the logic above. database_noteactions.record (request->session_logic()->currentUser (), note_id, Sync_Logic::notes_put_contents, database_notes.getContents (note_id)); database_noteactions.record (request->session_logic()->currentUser (), note_id, Sync_Logic::notes_put_bible, ""); database_noteactions.record (request->session_logic()->currentUser (), note_id, Sync_Logic::notes_put_passages, ""); database_noteactions.record (request->session_logic()->currentUser (), note_id, Sync_Logic::notes_put_create_complete, ""); } else { // Server: do the notifications. handlerNewNote (note_id); } return note_id; }
// This handles email to users. // identifier: the note that is being handled. // label: prefix to the subject line of the email. // users: array of users to be mailed. // postpone: whether to postpone sending the email till the evening. void Notes_Logic::emailUsers (int identifier, const string& label, const vector <string> & users, bool postpone) { // Databases. Database_Notes database_notes (webserver_request); Database_Mail database_mail = Database_Mail(webserver_request); // Send mail to all users. string summary = database_notes.getSummary (identifier); string passages = filter_passage_display_inline (database_notes.getPassages (identifier)); string contents = database_notes.getContents (identifier); // Include links to the Cloud: One to the note, and one to the active desktop. contents.append ("<br>\n"); contents.append ("<p>"); contents.append ("<a href=\""); string notelink = Database_Config_General::getSiteURL () + notes_note_url () + "?id=" + convert_to_string (identifier); contents.append (notelink); contents.append ("\">"); contents.append (translate ("View or respond online")); contents.append ("</a>"); contents.append (" " + translate ("or") + " "); contents.append ("<a href=\""); string desktoplink = Database_Config_General::getSiteURL () + workbench_index_url () + "?note=" + convert_to_string (identifier); contents.append (desktoplink); contents.append ("\">"); contents.append (translate ("open the desktop online")); contents.append ("</a>"); contents.append ("</p>\n"); string mailto = "mailto:" + Database_Config_General::getSiteMailAddress () + "?subject=(CNID" + convert_to_string (identifier) + ")"; contents.append ("<p><a href=\""); contents.append (mailto); contents.append ("\">Respond by email</a></p>\n"); // Deal with possible postponing email till 9 PM. int timestamp = filter_date_seconds_since_epoch (); if (postpone) { int localseconds = filter_date_local_seconds (timestamp); float localhour = filter_date_numerical_hour (localseconds) + (float) filter_date_numerical_minute (localseconds) / 60; if (localhour < 21) { float difference = 21 - localhour; timestamp += (3600 * difference) - 10; } } // Send (but not in client mode). for (auto & user : users) { if (!client_logic_client_enabled ()) { string subject = label; subject.append (" | "); subject.append (passages); subject.append (" | "); subject.append (summary); subject.append (" | (CNID"); subject.append (convert_to_string (identifier)); subject.append (")"); database_mail.send (user, subject, contents, timestamp); } } }
string sword_logic_get_text (string source, string module, int book, int chapter, int verse) { #ifdef HAVE_CLIENT // Client checks for and optionally creates the cache for this SWORD module. if (!Database_Cache::exists (module, book)) { Database_Cache::create (module, book); } // If this module/passage exists in the cache, return it (it updates the access days in the cache). if (Database_Cache::exists (module, book, chapter, verse)) { return Database_Cache::retrieve (module, book, chapter, verse); } // Fetch this SWORD resource from the server. string address = Database_Config_General::getServerAddress (); int port = Database_Config_General::getServerPort (); if (!client_logic_client_enabled ()) { // If the client has not been connected to a cloud instance, // fetch the SWORD content from the Bibledit Cloud demo. address = demo_address (); port = demo_port (); } string url = client_logic_url (address, port, sync_resources_url ()); string resource = "[" + source + "][" + module + "]"; url = filter_url_build_http_query (url, "r", resource); url = filter_url_build_http_query (url, "b", convert_to_string (book)); url = filter_url_build_http_query (url, "c", convert_to_string (chapter)); url = filter_url_build_http_query (url, "v", convert_to_string (verse)); string error; string html = filter_url_http_get (url, error, true); // In case of an error, don't cache that error, but let the user see it. if (!error.empty ()) return error; // Client caches this info for later. // Except in case of predefined responses from the Cloud. if (html != sword_logic_installing_module_text ()) { if (html != sword_logic_fetch_failure_text ()) { Database_Cache::cache (module, book, chapter, verse, html); } } return html; #else string module_text; bool module_available = false; string osis = Database_Books::getOsisFromId (book); string chapter_verse = convert_to_string (chapter) + ":" + convert_to_string (verse); // See notes on function sword_logic_diatheke // for why it is not currently fetching content via a SWORD library call. // module_text = sword_logic_diatheke (module, osis, chapter, verse, module_available); // Running diatheke only works when it runs in the SWORD installation directory. string sword_path = sword_logic_get_path (); // Running several instances of diatheke simultaneously fails. sword_logic_diatheke_run_mutex.lock (); // The server fetches the module text as follows: // diatheke -b KJV -k Jn 3:16 int result = filter_shell_vfork (module_text, sword_path, "diatheke", "-b", module.c_str(), "-k", osis.c_str(), chapter_verse.c_str()); sword_logic_diatheke_run_mutex.unlock (); if (result != 0) return sword_logic_fetch_failure_text (); // Touch the cache so the server knows that the module has been accessed just now. string url = sword_logic_virtual_url (module, 0, 0, 0); database_filebased_cache_get (url); // If the module has not been installed, the output of "diatheke" will be empty. // If the module was installed, but the requested passage is out of range, // the output of "diatheke" contains the module name, so it won't be empty. module_available = !module_text.empty (); if (!module_available) { // Check whether the SWORD module exists. vector <string> modules = sword_logic_get_available (); string smodules = filter_string_implode (modules, ""); if (smodules.find ("[" + module + "]") != string::npos) { // Schedule SWORD module installation. // (It used to be the case that this function, to get the text, // would wait till the SWORD module was installed, and then after installation, // return the text from that module. // But due to long waiting on Bibledit demo, while it would install multiple modules, // the Bibledit demo would become unresponsive. // So, it's better to return immediately with an informative text.) sword_logic_install_module_schedule (source, module); // Return standard 'installing' information. Client knows not to cache this. return sword_logic_installing_module_text (); } else { return "Cannot find SWORD module " + module; } } // Remove any OSIS elements. filter_string_replace_between (module_text, "<", ">", ""); // Remove the passage name that diatheke adds. // A reliable signature for this is the chapter and verse plus subsequent colon. size_t pos = module_text.find (" " + chapter_verse + ":"); if (pos != string::npos) { pos += 2; pos += chapter_verse.size (); module_text.erase (0, pos); } // Remove the module name that diatheke adds. module_text = filter_string_str_replace ("(" + module + ")", "", module_text); // Clean whitespace away. module_text = filter_string_trim (module_text); return module_text; #endif return ""; }
string bible_book (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; string page; Assets_Header header = Assets_Header (translate("Book"), webserver_request); header.addBreadCrumb (menu_logic_settings_menu (), menu_logic_settings_text ()); header.addBreadCrumb (bible_manage_url (), menu_logic_bible_manage_text ()); page = header.run (); Assets_View view; string success_message; string error_message; // The name of the Bible. string bible = access_bible_clamp (request, request->query["bible"]); view.set_variable ("bible", filter_string_sanitize_html (bible)); // The book. int book = convert_to_int (request->query ["book"]); view.set_variable ("book", convert_to_string (book)); string book_name = Database_Books::getEnglishFromId (book); view.set_variable ("book_name", filter_string_sanitize_html (book_name)); // Whether the user has write access to this Bible book. bool write_access = access_bible_book_write (request, "", bible, book); if (write_access) view.enable_zone ("write_access"); // Delete chapter. string deletechapter = request->query ["deletechapter"]; if (deletechapter != "") { string confirm = request->query ["confirm"]; if (confirm == "") { Dialog_Yes dialog_yes = Dialog_Yes ("book", translate("Would you like to delete this chapter?")); dialog_yes.add_query ("bible", bible); dialog_yes.add_query ("book", convert_to_string (book)); dialog_yes.add_query ("deletechapter", deletechapter); page += dialog_yes.run (); return page; } if (confirm == "yes") { if (write_access) Bible_Logic::deleteChapter (bible, book, convert_to_int (deletechapter)); } } // Add chapter. if (request->query.count ("createchapter")) { Dialog_Entry dialog_entry = Dialog_Entry ("book", translate("Please enter the number for the new chapter"), "", "createchapter", ""); dialog_entry.add_query ("bible", bible); dialog_entry.add_query ("book", convert_to_string (book)); page += dialog_entry.run (); return page; } if (request->post.count ("createchapter")) { int createchapter = convert_to_int (request->post ["entry"]); vector <int> chapters = request->database_bibles ()->getChapters (bible, book); // Only create the chapters if it does not yet exist. if (find (chapters.begin(), chapters.end(), createchapter) == chapters.end()) { vector <string> feedback; bool result = true; if (write_access) result = book_create (bible, book, createchapter, feedback); string message = filter_string_implode (feedback, " "); if (result) success_message = message; else error_message = message; } else { error_message = translate ("This chapter already exists"); } } // Available chapters. vector <int> chapters = request->database_bibles ()->getChapters (bible, book); string chapterblock; for (auto & chapter : chapters) { chapterblock.append ("<a href=\"chapter?bible="); chapterblock.append (bible); chapterblock.append ("&book="); chapterblock.append (convert_to_string (book)); chapterblock.append ("&chapter="); chapterblock.append (convert_to_string (chapter)); chapterblock.append ("\">"); chapterblock.append (convert_to_string (chapter)); chapterblock.append ("</a>\n"); } view.set_variable ("chapterblock", chapterblock); view.set_variable ("success_message", success_message); view.set_variable ("error_message", error_message); if (!client_logic_client_enabled ()) view.enable_zone ("server"); page += view.render ("bible", "book"); page += Assets_Page::footer (); return page; }
// This handles notifications for the users // identifier: the note that is being handled. // notification: the type of action on the consultation note. void Notes_Logic::notifyUsers (int identifier, int notification) { // Take no action in client mode. if (client_logic_client_enabled ()) return; // Data objects. Webserver_Request * request = (Webserver_Request *) webserver_request; Database_Notes database_notes (webserver_request); // This note's Bible. string bible = database_notes.getBible (identifier); // Subscription and assignment not to be used for notes marked for deletion, // because marking notes for deletion is nearly the same as deleting them straightaway. if (notification != notifyMarkNoteForDeletion) { // Whether current user gets subscribed to the note. if (request->database_config_user ()->getSubscribeToConsultationNotesEditedByMe ()) { database_notes.subscribe (identifier); } // Users to get subscribed to the note, or to whom the note is to be assigned. vector <string> users = request->database_users ()->getUsers (); for (const string & user : users) { if (access_bible_read (webserver_request, bible, user)) { if (request->database_config_user ()->getNotifyUserOfAnyConsultationNotesEdits (user)) { database_notes.subscribeUser (identifier, user); } if (request->database_config_user ()->getUserAssignedToConsultationNotesChanges (user)) { database_notes.assignUser (identifier, user); } } } } // The recipients who receive a notification by email. vector <string> recipients; // Subscribers who receive email. vector <string> subscribers = database_notes.getSubscribers (identifier); for (const string & subscriber : subscribers) { if (request->database_config_user ()->getUserSubscribedConsultationNoteNotification (subscriber)) { recipients.push_back (subscriber); } } // Assignees who receive email. vector <string> assignees = database_notes.getAssignees (identifier); for (const string & assignee : assignees) { if (request->database_config_user ()->getUserAssignedConsultationNoteNotification (assignee)) { recipients.push_back (assignee); } } // In case the consultation note is deleted or marked for deletion, // notify only the users with this specific notification set. if ((notification == notifyNoteDelete) || (notification == notifyMarkNoteForDeletion)) { recipients.clear (); vector <string> users = request->database_users ()->getUsers (); for (const auto & user : users) { if (request->database_config_user ()->getUserDeletedConsultationNoteNotification (user)) { if (access_bible_read (webserver_request, bible, user)) { recipients.push_back (user); } } } } // Remove duplicates from the recipients. set <string> unique (recipients.begin (), recipients.end ()); recipients.assign (unique.begin (), unique.end()); // Deal with suppressing mail to the user when he made the update himself. string username = request->session_logic ()->currentUser (); if (request->database_config_user ()->getUserSuppressMailFromYourUpdatesNotes (username)) { recipients.erase (remove (recipients.begin(), recipients.end(), username), recipients.end()); } // Generate the label prefixed to the subject line of the email. string label = translate("General"); switch (notification) { case notifyNoteNew : label = translate("New"); break; case notifyNoteComment : label = translate("Comment"); break; case notifyNoteDelete : label = translate("Deleted"); break; case notifyMarkNoteForDeletion : label = translate("Marked for deletion"); break; } // Optional postponing sending email. bool postpone = false; if (notification == notifyNoteNew) { if (request->database_config_user ()->getPostponeNewNotesMails ()) { postpone = true; } } // Send mail to all recipients. emailUsers (identifier, label, recipients, postpone); }
string bible_manage (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; string page; Assets_Header header = Assets_Header (translate("Bibles"), webserver_request); header.addBreadCrumb (menu_logic_settings_menu (), menu_logic_settings_text ()); page = header.run (); Assets_View view; string success_message; string error_message; // New Bible handler. if (request->query.count ("new")) { Dialog_Entry dialog_entry = Dialog_Entry ("manage", translate("Please enter a name for the new empty Bible"), "", "new", ""); page += dialog_entry.run (); return page; } if (request->post.count ("new")) { string bible = request->post ["entry"]; vector <string> bibles = request->database_bibles ()->getBibles (); if (find (bibles.begin(), bibles.end(), bible) != bibles.end()) { error_message = translate("This Bible already exists"); } else { request->database_bibles ()->createBible (bible); // Check / grant access. if (!access_bible_write (request, bible)) { string me = request->session_logic ()->currentUser (); Database_Privileges::setBible (me, bible, true); } success_message = translate("The Bible was created"); // Creating a Bible removes any Sample Bible that might have been there. if (!config_logic_demo_enabled ()) { request->database_bibles ()->deleteBible (demo_sample_bible_name ()); search_logic_delete_bible (demo_sample_bible_name ()); } } } // Copy Bible handler. if (request->query.count ("copy")) { string copy = request->query["copy"]; Dialog_Entry dialog_entry = Dialog_Entry ("manage", translate("Please enter a name for where to copy the Bible to"), "", "", "A new Bible will be created with the given name, and the current Bible copied to it"); dialog_entry.add_query ("origin", copy); page += dialog_entry.run (); return page; } if (request->query.count ("origin")) { string origin = request->query["origin"]; if (request->post.count ("entry")) { string destination = request->post["entry"]; vector <string> bibles = request->database_bibles ()->getBibles (); if (find (bibles.begin(), bibles.end(), destination) != bibles.end()) { error_message = translate("Cannot copy the Bible because the destination Bible already exists."); } else { // User needs read access to the original. if (access_bible_read (request, origin)) { request->database_bibles ()->createBible (destination); vector <int> books = request->database_bibles ()->getBooks (origin); for (auto & book : books) { vector <int> chapters = request->database_bibles ()->getChapters (origin, book); for (auto & chapter : chapters) { string data = request->database_bibles ()->getChapter (origin, book, chapter); Bible_Logic::storeChapter (destination, book, chapter, data); } } success_message = translate("The Bible was copied."); // Check / grant access to destination Bible. if (!access_bible_write (request, destination)) { string me = request->session_logic ()->currentUser (); Database_Privileges::setBible (me, destination, true); } // Creating a Bible removes any Sample Bible that might have been there. if (!config_logic_demo_enabled ()) { request->database_bibles ()->deleteBible (demo_sample_bible_name ()); search_logic_delete_bible (demo_sample_bible_name ()); } } } } } // Delete Bible handler. if (request->query.count ("delete")) { string bible = request->query ["delete"]; string confirm = request->query ["confirm"]; if (confirm == "yes") { // User needs write access for delete operation. if (access_bible_write (request, bible)) { Bible_Logic::deleteBible (bible); string gitdirectory = filter_git_directory (bible); if (file_exists (gitdirectory)) { filter_url_rmdir (gitdirectory); } // Remove associated settings and privileges. Database_Privileges::removeBible (bible); Database_Config_Bible::remove (bible); } else { page += Assets_Page::error ("Insufficient privileges to complete action"); } } if (confirm == "") { Dialog_Yes dialog_yes = Dialog_Yes ("manage", translate("Would you like to delete this Bible?") + " (" + bible + ")"); dialog_yes.add_query ("delete", bible); page += dialog_yes.run (); return page; } } view.set_variable ("success_message", success_message); view.set_variable ("error_message", error_message); vector <string> bibles = access_bible_bibles (request); string bibleblock; for (auto & bible : bibles) { bibleblock.append ("<li><a href=\"settings?bible=" + bible + "\">" + bible + "</a></li>\n"); } view.set_variable ("bibleblock", bibleblock); if (!client_logic_client_enabled ()) view.enable_zone ("server"); page += view.render ("bible", "manage"); page += Assets_Page::footer (); return page; }
void sendreceive_resources () { if (sendreceive_resources_watchdog) { int time = filter_date_seconds_since_epoch (); if (time < (sendreceive_resources_watchdog + 900)) { return; } Database_Logs::log ("Resources: " + translate("Watchdog timeout"), Filter_Roles::translator ()); sendreceive_resources_done (); } // If any of the prioritized synchronization tasks run, postpone the current task and do not start it. if (sendreceive_logic_prioritized_task_is_active ()) { sendreceive_resources_done (); this_thread::sleep_for (chrono::seconds (5)); tasks_logic_queue (SYNCRESOURCES); return; } sendreceive_resources_interrupt = false; // If there's nothing to cache, bail out. vector <string> resources = Database_Config_General::getResourcesToCache (); if (resources.empty ()) return; sendreceive_resources_kick_watchdog (); // Error counter. int error_count = 0; // Resource to cache. string resource = resources [0]; // Erase the two older storage locations that were used to cache resources in earlier versions of Bibledit. { Database_OfflineResources database_offlineresources; Database_UsfmResources database_usfmresources; database_offlineresources.erase (resource); database_usfmresources.deleteResource (resource); } Database_Logs::log ("Starting to install resource:" " " + resource, Filter_Roles::consultant ()); Database_Versifications database_versifications; vector <int> books = database_versifications.getMaximumBooks (); for (auto & book : books) { sendreceive_resources_delay_during_prioritized_tasks (); if (sendreceive_resources_interrupt) continue; // Database layout is per book: Create a database for this book. Database_Cache::create (resource, book); // Last downloaded passage in a previous download operation. int last_downloaded_passage = 0; { pair <int, int> progress = Database_Cache::progress (resource, book); int chapter = progress.first; int verse = progress.second; Passage passage ("", book, chapter, convert_to_string (verse)); last_downloaded_passage = filter_passage_to_integer (passage); } // List of passages recorded in the database that had errors on a previous download operation. vector <int> previous_errors; { vector <pair <int, int> > errors = Database_Cache::errors (resource, book); for (auto & element : errors) { int chapter = element.first; int verse = element.second; Passage passage ("", book, chapter, convert_to_string (verse)); int numeric_error = filter_passage_to_integer (passage); previous_errors.push_back (numeric_error); } } string bookName = Database_Books::getEnglishFromId (book); vector <int> chapters = database_versifications.getMaximumChapters (book); for (auto & chapter : chapters) { sendreceive_resources_delay_during_prioritized_tasks (); if (sendreceive_resources_interrupt) continue; bool downloaded = false; string message = resource + ": " + bookName + " chapter " + convert_to_string (chapter); vector <int> verses = database_versifications.getMaximumVerses (book, chapter); for (auto & verse : verses) { sendreceive_resources_delay_during_prioritized_tasks (); if (sendreceive_resources_interrupt) continue; // Numeric representation of passage to deal with. Passage passage ("", book, chapter, convert_to_string (verse)); int numeric_passage = filter_passage_to_integer (passage); // Conditions to download this verse: // 1. The passage is past the last downloaded passage. bool download_verse_past = numeric_passage > last_downloaded_passage; // 2. The passage was recorded as an error in a previous download operation. bool download_verse_error = in_array (numeric_passage, previous_errors); // Whether to download the verse. if (download_verse_past || download_verse_error) { // Fetch the text for the passage. bool server_is_installing_module = false; int wait_iterations = 0; string html, error; do { // Fetch this resource from the server. string address = Database_Config_General::getServerAddress (); int port = Database_Config_General::getServerPort (); // If the client has not been connected to a cloud instance, // fetch the resource from the Bibledit Cloud demo. if (!client_logic_client_enabled ()) { address = demo_address (); port = demo_port (); } string url = client_logic_url (address, port, sync_resources_url ()); url = filter_url_build_http_query (url, "r", filter_url_urlencode (resource)); url = filter_url_build_http_query (url, "b", convert_to_string (book)); url = filter_url_build_http_query (url, "c", convert_to_string (chapter)); url = filter_url_build_http_query (url, "v", convert_to_string (verse)); error.clear (); html = filter_url_http_get (url, error); server_is_installing_module = (html == sword_logic_installing_module_text ()); if (server_is_installing_module) { Database_Logs::log ("Waiting while Bibledit Cloud installs the requested SWORD module"); this_thread::sleep_for (chrono::seconds (60)); wait_iterations++; } } while (server_is_installing_module && (wait_iterations < 5)); // Record the passage as having been done in case it was a regular download, // rather than one to retry a previous download error. if (download_verse_past) Database_Cache::progress (resource, book, chapter, verse); // Clear the registered error in case the verse download corrects it. if (download_verse_error) Database_Cache::error (resource, book, chapter, verse, false); if (error.empty ()) { // Cache the verse data. if (!Database_Cache::exists (resource, book)) Database_Cache::create (resource, book); Database_Cache::cache (resource, book, chapter, verse, html); } else { // Record an error. Database_Cache::error (resource, book, chapter, verse, true); if (message.find (error) == string::npos) { message.append ("; " + error); } error_count++; this_thread::sleep_for (chrono::seconds (1)); } downloaded = true; } sendreceive_resources_kick_watchdog (); } message += "; done"; if (downloaded) Database_Logs::log (message, Filter_Roles::manager ()); } } // Done. if (error_count) { string msg = "Error count while downloading resource: " + convert_to_string (error_count); Database_Logs::log (msg, Filter_Roles::consultant ()); } Database_Logs::log ("Completed installing resource:" " " + resource, Filter_Roles::consultant ()); // In case of too many errors, schedule the resource download again. bool re_schedule_download = false; if (error_count) { if (!sendreceive_resources_interrupt) { re_schedule_download = true; Database_Logs::log ("Errors: Re-scheduling resource installation", Filter_Roles::consultant ()); } } // Store new download schedule. resources = Database_Config_General::getResourcesToCache (); resources = filter_string_array_diff (resources, {resource}); if (re_schedule_download) { resources.push_back (resource); } Database_Config_General::setResourcesToCache (resources); sendreceive_resources_done (); sendreceive_resources_interrupt = false; // If there's another resource waiting to be cached, schedule it for caching. if (!resources.empty ()) tasks_logic_queue (SYNCRESOURCES); }
string journal_index (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; int userLevel = request->session_logic()->currentLevel (); string filename = request->query ["filename"]; if (!filename.empty ()) { return journal_index_ajax (request, filename); } Database_Logs database_logs = Database_Logs (); Assets_Header header = Assets_Header (translate ("Journal"), webserver_request); header.addBreadCrumb (menu_logic_tools_menu (), menu_logic_tools_text ()); string page = header.run (); Assets_View view; if (request->query.count ("clear")) { database_logs.clear (); // If the logbook has been cleared on a mobile device, and the screen shuts off, // and then the user activates the screen on the mobile device, // the logbook will then again be cleared, because that was the last opened URL. // Redirecting the browser to a clean URL fixes this behaviour. redirect_browser (request, journal_index_url ()); return ""; } string lastfilename; vector <string> entries = database_logs.get (lastfilename); string lines; for (auto entry : entries) { int entryLevel = convert_to_int (entry); // Cloud: Pay attention to only rendering journal entries of sufficient user level. // Client: Render any journal entry. if (!client_logic_client_enabled ()) { if (entryLevel > userLevel) continue; } entry = render_journal_entry (entry); lines.append ("<p>"); lines.append (entry); lines.append ("</p>\n"); } view.set_variable ("lines", lines); // Pass the filename of the most recent entry to javascript // for use by the AJAX calls for getting subsequent journal entries. // It should be passed as a String object in JavaScript. // Because when it were passed as an Int, JavaScript would round the value off. // And rounding it off often led to double journal entries. string script = "var filename = \"" + lastfilename + "\";"; view.set_variable ("script", script); page += view.render ("journal", "index"); page += Assets_Page::footer (); return page; }
string client_index (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; Assets_View view; if (request->query.count ("disable")) { client_logic_enable_client (false); client_index_remove_all_users (request); Database_Config_General::setRepeatSendReceive (0); Database_Config_General::setUnsentBibleDataTime (0); Database_Config_General::setUnreceivedBibleDataTime (0); } bool connect = request->post.count ("connect"); bool demo = request->query.count ("demo"); if (connect || demo) { string address; if (connect) address = request->post ["address"]; if (demo) address = demo_address (); if (address.find ("http") == string::npos) address = filter_url_set_scheme (address, false); Database_Config_General::setServerAddress (address); int port = convert_to_int (config_logic_http_network_port ()); if (connect) port = convert_to_int (request->post ["port"]); if (demo) port = demo_port (); Database_Config_General::setServerPort (port); string user; if (connect) user = request->post ["user"]; if (demo) user = session_admin_credentials (); string pass; if (connect) pass = request->post ["pass"]; if (demo) pass = session_admin_credentials (); string response = client_logic_connection_setup (user, md5 (pass)); int iresponse = convert_to_int (response); if ((iresponse >= Filter_Roles::guest ()) && (iresponse <= Filter_Roles::admin ())) { // Enable client mode upon a successful connection. client_index_enable_client (request, user, pass, iresponse); // Feedback. view.set_variable ("success", translate("Connection is okay.")); } else { view.set_variable ("error", translate ("Could not create a connection with Bibledit Cloud") + ": " + response); } } if (client_logic_client_enabled ()) view.enable_zone ("clienton"); else view.enable_zone ("clientoff"); string address = Database_Config_General::getServerAddress (); view.set_variable ("address", address); int port = Database_Config_General::getServerPort (); view.set_variable ("port", convert_to_string (port)); view.set_variable ("url", client_logic_link_to_cloud ("", "")); vector <string> users = request->database_users ()->getUsers (); for (auto & user : users) { int level = request->database_users()->get_level (user); view.set_variable ("role", Filter_Roles::text (level)); } view.set_variable ("demo", demo_client_warning ()); string page; // Since the role of the user may change after a successful connection to the server, // the menu generation in the header should be postponed till when the actual role is known. page = Assets_Page::header (translate ("Server"), webserver_request); page += view.render ("client", "index"); page += Assets_Page::footer (); return page; }
// This function runs the sprint burndown history logger for $bible. // If no $bible is passed, it will do all Bibles. // If $mail is true, it will mail the burndown chart to the subscribed users. // If $mail is false, it decides on its own whether to mail the chart to the users. void sprint_burndown (string bible, bool email) { int localseconds = filter_date_local_seconds (filter_date_seconds_since_epoch ()); int year = filter_date_numerical_year (localseconds); int month = filter_date_numerical_month (localseconds); int monthday = filter_date_numerical_month_day (localseconds); // 1 to 31. int weekday = filter_date_numerical_week_day (localseconds); // 0 (for Sunday) through 6 (for Saturday). int hour = filter_date_numerical_hour (localseconds); bool sprintstart = false; bool sprintfinish = false; // Every Friday at 2 PM (14:00h) it sends email about the sprint progress. if ((weekday == 5) && (hour == 14)) email = true; // On the first business day of the month, at 10 AM, send email about the start of the sprint. if (filter_date_is_first_business_day_of_month (monthday, weekday) && (hour == 10)) { email = true; sprintstart = true; } // On the last business day of the month, at 2 PM (14:00h), send email about the end of the sprint. if ((monthday == filter_date_get_last_business_day_of_month (year, month)) && (hour == 14)) { email = true; sprintfinish = true; } // Determine what to do, or to quit. if (!email && !sprintstart && !sprintfinish) { if (hour != 1) return; } Database_Logs::log ("Updating Sprint information", Filter_Roles::manager ()); Webserver_Request request; Database_Mail database_mail = Database_Mail (&request); Database_Sprint database_sprint = Database_Sprint (); // Determine year / month / day of the current sprint. // If this script runs from midnight till early morning, // it applies to the day before. // If the script runs during the day, it applies to today. if (hour <= 6) { localseconds -= (3600 * 6); } year = filter_date_numerical_year (localseconds); month = filter_date_numerical_month (localseconds); monthday = filter_date_numerical_month_day (localseconds); // 1 to 31. vector <string> bibles = {bible}; if (bible == "") { bibles = request.database_bibles()->getBibles (); } for (auto bible : bibles) { // Get the total number of tasks for this sprint, // and the average percentage of completion of them, // and store this information in the sprint history table. vector <int> ids = database_sprint.getTasks (bible, year, month); vector <int> percentages; for (auto id : ids) { percentages.push_back (database_sprint.getComplete (id)); } int tasks = ids.size (); int complete = 0; if (tasks != 0) { for (auto percentage : percentages) complete += percentage; complete = round ((float) complete / (float) tasks); } database_sprint.logHistory (bible, year, month, monthday, tasks, complete); // Send email if requested. if (email) { if (tasks) { // Only mail if the current sprint contains tasks. string scategories = Database_Config_Bible::getSprintTaskCompletionCategories (bible); vector <string> categories = filter_string_explode (scategories, '\n'); int category_count = categories.size(); int category_percentage = round (100 / category_count); vector <string> users = request.database_users ()->getUsers (); for (auto user : users) { if (request.database_config_user()->getUserSprintProgressNotification (user)) { string subject = translate("Team's progress in Sprint"); if (sprintstart) subject = translate("Sprint has started"); if (sprintfinish) subject = translate("Sprint has finished"); subject += " | " + bible; vector <string> body; body.push_back ("<h3>" + translate("Sprint Planning and Team's Progress") + " | " + bible + "</h3>"); body.push_back ("<table class='burndown'>"); vector <int> tasks = database_sprint.getTasks (bible, year, month); for (auto id : tasks) { body.push_back ("<tr>"); string title = database_sprint.getTitle (id); body.push_back ("<td>" + title + "</td>"); int complete = database_sprint.getComplete (id); string text; for (int i = 0; i < round (complete / category_percentage); i++) text.append ("▓"); for (int i = 0; i < category_count - round (complete / category_percentage); i++) text.append ("▁"); body.push_back ("<td>" + text + "</td>"); body.push_back ("</tr>"); } body.push_back ("</table>"); body.push_back ("<h3>" + translate("Sprint Burndown Chart - Remaining Tasks") + "</h3>"); string burndownchart = sprint_create_burndown_chart (bible, year, month); body.push_back ("<p>" + burndownchart + "</p>"); if (!body.empty ()) { string mailbody = filter_string_implode (body, "\n"); if (!client_logic_client_enabled ()) database_mail.send (user, subject, mailbody); } } } } else { // Since there are no tasks, no mail will be sent: Make a logbook entry. Database_Logs::log ("No tasks in this Sprint: No email was sent"); } } } }
void timer_index () { bool client = client_logic_client_enabled (); int previous_second = -1; int previous_minute = -1; int previous_fraction = -1; while (config_globals_running) { try { // Wait shortly. this_thread::sleep_for (chrono::milliseconds (100)); // Wait tilll the data structures have been initialized. if (!config_globals_data_initialized) continue; // The current time, localized. int seconds_since_epoch = filter_date_seconds_since_epoch (); int local_seconds = filter_date_local_seconds (seconds_since_epoch); int second = filter_date_numerical_second (local_seconds); int minute = filter_date_numerical_minute (local_seconds); int hour = filter_date_numerical_hour (local_seconds); int weekday = filter_date_numerical_week_day (local_seconds); // Run once per second. if (second == previous_second) continue; previous_second = second; // Every second: Deal with queued and/or active tasks. tasks_run_check (); // Run the part below every so many seconds. int fraction = second / 5; if (fraction != previous_fraction) { previous_fraction = fraction; } // Run the part below once per minute. if (minute == previous_minute) continue; previous_minute = minute; // Every minute send out queued email, except in client mode. if (!client) tasks_logic_queue (SENDEMAIL); // Check for new mail every five minutes. // Do not check more often with gmail else the account may be shut down. if ((!client) && ((minute % 5) == 0)) { tasks_logic_queue (RECEIVEEMAIL); } // At the nineth minute after every full hour rotate the journal. // The nine is chosen, because the journal rotation will summarize the send/receive messages. // In case send/receive happens every five minutes, it is expected that under normal circumstances // the whole process of sending/receivning will be over, so summarization can then be done. if (minute == 9) tasks_logic_queue (ROTATEJOURNAL); // Client sends/receives Bibles and Consultation. sendreceive_queue_sync (minute); // Sending and receiving Bibles to and from the git repository. // On a production website running on an inexpensive virtual private server // with 512 Mbyte of memory and a fast network connection, // sending and receiving two Bibles takes more than 15 minutes when there are many changes. bool sendreceive = ((hour == 0) && (minute == 5)); bool repeat = ((minute % 5) == 0); if (sendreceive || repeat) { sendreceive_queue_all (sendreceive); } // Deal with the changes in the Bible made per user. // Deal with notifications for the daily changes in the Bibles. // This takes a few minutes on a production machine with two Bibles and changes in several chapters. // It runs in a server configuration, not on a client. if (!client) { if ((hour == 0) && (minute == 20)) { changes_logic_start (); } } // Run the checks on the Bibles. // This takes 15 minutes on a production machine with two Bibles. if (!client) { if ((hour == 0) && (minute == 30)) { checks_logic_start_all (); } } // Database maintenance and trimming. // It takes a few minutes on a production machine. if ((hour == 0) && (minute == 50)) { tasks_logic_queue (MAINTAINDATABASE); } // Export the Bibles to the various output formats. // This may take an hour on a production machine. // This hour was in PHP. In C++ it is much faster. if (!client) { if ((hour == 1) && (minute == 10)) { Export_Logic::scheduleAll (); } } // Delete temporal files older than a few days. if ((hour == 2) && (minute == 0)) { tasks_logic_queue (CLEANTMPFILES); } // Re-index Bibles and notes. // Only update missing indexes. if ((hour == 2) && (minute == 0)) { Database_State::create (); Database_Config_General::setIndexBibles (true); tasks_logic_queue (REINDEXBIBLES); Database_Config_General::setIndexNotes (true); tasks_logic_queue (REINDEXNOTES); } // Actions for a demo installation. if (minute == 10) { if (config_logic_demo_enabled ()) { tasks_logic_queue (CLEANDEMO); } } // Sprint burndown. // It runs every hour in the Cloud. // The script itself determines what to do at which hour of the day or day of the week or day of the month. if (!client) { if (minute == 5) { tasks_logic_queue (SPRINTBURNDOWN); } } // Quit at midnight if flag is set. if (config_globals_quit_at_midnight) { if (hour == 0) { if (minute == 1) { if (!Database_Config_General::getJustStarted ()) { if (tasks_run_active_count ()) { Database_Logs::log ("Server is due to restart itself but does not because of active jobs"); } else { Database_Logs::log ("Server restarts itself"); exit (0); } } } // Clear flag in preparation of restart next minute. // This flag also has the purpose of ensuring the server restarts once during that minute, // rather than restarting repeatedly many times during that minute. if (minute == 0) { Database_Config_General::setJustStarted (false); } } } // Email notes statistics to the users. if (!client) { if ((hour == 3) && (minute == 0)) { tasks_logic_queue (NOTESSTATISTICS); } } // Update SWORD stuff once a week. if (weekday == 1) { // Refresh module list. if ((!client) && (hour == 3) && (minute == 5)) { tasks_logic_queue (REFRESHSWORDMODULES); } // Update installed SWORD modules, shortly after the module list has been refreshed. if ((!client) && (hour == 3) && (minute == 15)) { tasks_logic_queue (UPDATESWORDMODULES); } } // The Cloud updates the list of USFM resources once a week. if (weekday == 1) { if ((!client) && (hour == 3) && (minute == 10)) { tasks_logic_queue (LISTUSFMRESOURCES); } } } catch (exception & e) { Database_Logs::log (e.what ()); } catch (exception * e) { Database_Logs::log (e->what ()); } catch (...) { Database_Logs::log ("A general internal error occurred in the timers"); } } }
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 ()); }