// 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_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>"); 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"); email_schedule (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 sendreceive_changes () { if (sendreceive_changes_watchdog) { int time = filter_date_seconds_since_epoch (); if (time < (sendreceive_changes_watchdog + 900)) { Database_Logs::log (sendreceive_changes_text () + translate("Still busy"), Filter_Roles::translator ()); return; } Database_Logs::log (sendreceive_changes_text () + translate("Watchdog timeout"), Filter_Roles::translator ()); } sendreceive_changes_kick_watchdog (); config_globals_syncing_changes = true; Database_Logs::log (sendreceive_changes_sendreceive_text (), Filter_Roles::translator ()); Webserver_Request request; Sync_Logic sync_logic = Sync_Logic (&request); Database_Modifications database_modifications; if (!database_modifications.healthy ()) { Database_Logs::log (sendreceive_changes_text () + translate("Recreate damaged modifications database"), Filter_Roles::translator ()); database_modifications.erase (); database_modifications.create (); } string response = client_logic_connection_setup (); int iresponse = convert_to_int (response); if (iresponse < Filter_Roles::guest () || iresponse > Filter_Roles::admin ()) { Database_Logs::log (sendreceive_changes_text () + translate("Failure to initiate connection"), Filter_Roles::translator ()); send_receive_changes_done (); return; } // Set the correct user in the session: The sole user on the Client. vector <string> users = request.database_users ()->getUsers (); if (users.empty ()) { Database_Logs::log (translate("No user found"), Filter_Roles::translator ()); send_receive_changes_done (); return; } string user = users [0]; request.session_logic ()->setUsername (user); string password = request.database_users ()->getmd5 (user); // The basic request to be POSTed to the server. // It contains the user's credentials. map <string, string> post; post ["u"] = bin2hex (user); post ["p"] = password; post ["l"] = convert_to_string (request.database_users ()->getUserLevel (user)); // Error variables. string error; bool communication_errors = false; // Server URL to call. string address = Database_Config_General::getServerAddress (); int port = Database_Config_General::getServerPort (); string url = client_logic_url (address, port, sync_changes_url ()); // Send the removed change notifications to the server. vector <int> ids = request.database_config_user ()->getRemovedChanges (); if (!ids.empty ()) Database_Logs::log (sendreceive_changes_text () + "Sending removed notifications: " + convert_to_string (ids.size()), Filter_Roles::translator ()); for (auto & id : ids) { post ["a"] = convert_to_string (Sync_Logic::changes_delete_modification); post ["i"] = convert_to_string (id); response = sync_logic.post (post, url, error); if (!error.empty ()) { communication_errors = true; Database_Logs::log (sendreceive_changes_text () + "Failure sending removed notification: " + error, Filter_Roles::translator ()); } else { request.database_config_user ()->removeRemovedChange (id); } } if (communication_errors) { Database_Logs::log (sendreceive_changes_text () + translate("Not downloading change notifications due to communication error"), Filter_Roles::translator ()); send_receive_changes_done (); return; } // Compare the total checksum for the change notifications for the active user on client and server. // Checksum is cached for future re-use. // Take actions based on that. string client_checksum = request.database_config_user ()->getChangeNotificationsChecksum (); if (client_checksum.empty ()) { client_checksum = Sync_Logic::changes_checksum (user); request.database_config_user ()->setChangeNotificationsChecksum (client_checksum); } string server_checksum; post ["a"] = convert_to_string (Sync_Logic::changes_get_checksum); response = sync_logic.post (post, url, error); if (!error.empty ()) { Database_Logs::log (sendreceive_changes_text () + "Failure receiving checksum: " + error, Filter_Roles::translator ()); send_receive_changes_done (); return; } server_checksum = response; if (client_checksum == server_checksum) { Database_Logs::log (sendreceive_changes_up_to_date_text (), Filter_Roles::translator ()); send_receive_changes_done (); return; } // Get all identifiers for the notifications on the server for the user. // Get the identifiers on the client. vector <int> client_identifiers = database_modifications.getNotificationIdentifiers (user, false); vector <int> server_identifiers; post ["a"] = convert_to_string (Sync_Logic::changes_get_identifiers); response = sync_logic.post (post, url, error); if (!error.empty ()) { Database_Logs::log (sendreceive_changes_text () + "Failure receiving identifiers: " + error, Filter_Roles::translator ()); send_receive_changes_done (); return; } { vector <string> ids = filter_string_explode (response, '\n'); for (auto & id : ids) server_identifiers.push_back (convert_to_int (id)); } // Any identifiers on the client, but not on the server, remove them from the client. vector <int> remove_identifiers = filter_string_array_diff (client_identifiers, server_identifiers); for (auto & id : remove_identifiers) { database_modifications.deleteNotification (id); request.database_config_user ()->setChangeNotificationsChecksum (""); Database_Logs::log (sendreceive_changes_text () + "Removing notification: " + convert_to_string (id), Filter_Roles::translator ()); } // Any identifiers on the server, but not on the client, download them from the server. vector <int> download_identifiers = filter_string_array_diff (server_identifiers, client_identifiers); for (auto & id : download_identifiers) { sendreceive_changes_kick_watchdog (); Database_Logs::log (sendreceive_changes_text () + "Downloading notification: " + convert_to_string (id), Filter_Roles::translator ()); post ["a"] = convert_to_string (Sync_Logic::changes_get_modification); post ["i"] = convert_to_string (id); response = sync_logic.post (post, url, error); if (!error.empty ()) { Database_Logs::log (sendreceive_changes_text () + "Failure downloading notification: " + error, Filter_Roles::translator ()); } else { // The server has put all bits together, one bit per line. vector <string> lines = filter_string_explode (response, '\n'); string category; if (!lines.empty ()) { category = lines [0]; lines.erase (lines.begin ()); } string bible; if (!lines.empty ()) { bible = lines [0]; lines.erase (lines.begin ()); } int book = 0; if (!lines.empty ()) { book = convert_to_int (lines [0]); lines.erase (lines.begin ()); } int chapter = 0; if (!lines.empty ()) { chapter = convert_to_int (lines [0]); lines.erase (lines.begin ()); } int verse = 0; if (!lines.empty ()) { verse = convert_to_int (lines [0]); lines.erase (lines.begin ()); } string oldtext; if (!lines.empty ()) { oldtext = lines [0]; lines.erase (lines.begin ()); } string modification; if (!lines.empty ()) { modification = lines [0]; lines.erase (lines.begin ()); } string newtext; if (!lines.empty ()) { newtext = lines [0]; lines.erase (lines.begin ()); } database_modifications.storeClientNotification (id, user, category, bible, book, chapter, verse, oldtext, modification, newtext); request.database_config_user ()->setChangeNotificationsChecksum (""); } } // Done. Database_Logs::log (sendreceive_changes_text () + "Ready", Filter_Roles::translator ()); send_receive_changes_done (); }
string manage_users (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; bool user_updated = false; bool privileges_updated = false; string page; Assets_Header header = Assets_Header (translate("Users"), webserver_request); header.addBreadCrumb (menu_logic_settings_menu (), menu_logic_settings_text ()); page = header.run (); Assets_View view; int myLevel = request->session_logic ()->currentLevel (); // New user creation. if (request->query.count ("new")) { Dialog_Entry dialog_entry = Dialog_Entry ("users", translate("Please enter a name for the new user"), "", "new", ""); page += dialog_entry.run (); return page; } if (request->post.count ("new")) { string user = request->post["entry"]; if (request->database_users ()->usernameExists (user)) { page += Assets_Page::error (translate("User already exists")); } else { request->database_users ()->addNewUser(user, user, Filter_Roles::member (), ""); user_updated = true; page += Assets_Page::success (translate("User created")); } } // The user to act on. string objectUsername = request->query["user"]; int objectUserLevel = request->database_users ()->getUserLevel (objectUsername); // Delete a user. if (request->query.count ("delete")) { string role = Filter_Roles::text (objectUserLevel); string email = request->database_users ()->getUserToEmail (objectUsername); string message = "Deleted user " + objectUsername + " with role " + role + " and email " + email; Database_Logs::log (message, Filter_Roles::admin ()); request->database_users ()->removeUser (objectUsername); user_updated = true; database_privileges_client_remove (objectUsername); page += Assets_Page::success (message); // Also remove any privileges for this user. // In particular for the Bible privileges this is necessary, // beause if old users remain in the privileges storage, // then a situation where no user has any privileges to any Bible, // and thus all relevant users have all privileges, // can never be achieved again. Database_Privileges::removeUser (objectUsername); // Remove any login tokens the user might have had: Just to clean things up. Database_Login::removeTokens (objectUsername); // Remove any settings for the user. // The advantage of this is that when a user is removed, all settings are gone, // so when the same user would be created again, all settings will go back to their defaults. request->database_config_user ()->remove (objectUsername); } // The user's role. if (request->query.count ("level")) { string level = request->query ["level"]; if (level == "") { Dialog_List dialog_list = Dialog_List ("users", translate("Select a role for") + " " + objectUsername, "", ""); dialog_list.add_query ("user", objectUsername); for (int i = Filter_Roles::lowest (); i <= Filter_Roles::highest (); i++) { if (i <= myLevel) { dialog_list.add_row (Filter_Roles::text (i), "level", convert_to_string (i)); } } page += dialog_list.run (); return page; } else { request->database_users ()->updateUserLevel (objectUsername, convert_to_int (level)); user_updated = true; } } // User's email address. if (request->query.count ("email")) { string email = request->query ["email"]; if (email == "") { string question = translate("Please enter an email address for") + " " + objectUsername; string value = request->database_users ()->getUserToEmail (objectUsername); Dialog_Entry dialog_entry = Dialog_Entry ("users", question, value, "email", ""); dialog_entry.add_query ("user", objectUsername); page += dialog_entry.run (); return page; } } if (request->post.count ("email")) { string email = request->post["entry"]; if (filter_url_email_is_valid (email)) { page += Assets_Page::success (translate("Email address was updated")); request->database_users ()->updateUserEmail (objectUsername, email); user_updated = true; } else { page += Assets_Page::error (translate("The email address is not valid")); } } // Fetch all available Bibles. vector <string> allbibles = request->database_bibles ()->getBibles (); // Add Bible to user account. if (request->query.count ("addbible")) { string addbible = request->query["addbible"]; if (addbible == "") { Dialog_List dialog_list = Dialog_List ("users", translate("Would you like to grant the user access to a Bible?"), "", ""); dialog_list.add_query ("user", objectUsername); for (auto bible : allbibles) { dialog_list.add_row (bible, "addbible", bible); } page += dialog_list.run (); return page; } else { Assets_Page::success (translate("The user has been granted access to this Bible")); // Write access depends on whether it's a translator role or higher. bool write = (objectUserLevel >= Filter_Roles::translator ()); Database_Privileges::setBible (objectUsername, addbible, write); user_updated = true; privileges_updated = true; } } // Remove Bible from user. if (request->query.count ("removebible")) { string removebible = request->query ["removebible"]; Database_Privileges::removeBibleBook (objectUsername, removebible, 0); user_updated = true; privileges_updated = true; Assets_Page::success (translate("The user no longer has access to this Bible")); } // Login on behalf of another user. if (request->query.count ("login")) { request->session_logic ()->switchUser (objectUsername); redirect_browser (request, session_switch_url ()); return ""; } // User accounts to display. vector <string> tbody; // Retrieve assigned users. vector <string> users = access_user_assignees (webserver_request); for (auto & username : users) { // Gather details for this user account. objectUserLevel = request->database_users ()->getUserLevel (username); string namedrole = Filter_Roles::text (objectUserLevel); string email = request->database_users ()->getUserToEmail (username); if (email == "") email = "--"; tbody.push_back ("<tr>"); tbody.push_back ("<td><a href=\"?user="******"&delete\">✗</a> " + username + "</td>"); tbody.push_back ("<td>│</td>"); tbody.push_back ("<td><a href=\"?user="******"&level\">" + namedrole + "</a></td>"); tbody.push_back ("<td>│</td>"); tbody.push_back ("<td><a href=\"?user="******"&email\">" + email + "</a></td>"); tbody.push_back ("<td>│</td>"); tbody.push_back ("<td>"); if (objectUserLevel < Filter_Roles::manager ()) { for (auto & bible : allbibles) { bool exists = Database_Privileges::getBibleBookExists (username, bible, 0); if (exists) { bool read, write; Database_Privileges::getBible (username, bible, read, write); if (objectUserLevel >= Filter_Roles::translator ()) write = true; tbody.push_back ("<a href=\"?user="******"&removebible=" + bible + "\">✗</a>"); tbody.push_back ("<a href=\"/bible/settings?bible=" + bible + "\">" + bible + "</a>"); tbody.push_back ("<a href=\"write?user="******"&bible=" + bible + "\">"); int readwritebooks = 0; vector <int> books = request->database_bibles ()->getBooks (bible); for (auto book : books) { Database_Privileges::getBibleBook (username, bible, book, read, write); if (write) readwritebooks++; } tbody.push_back ("(" + convert_to_string (readwritebooks) + "/" + convert_to_string (books.size ()) + ")"); tbody.push_back ("</a>"); tbody.push_back ("|"); } } } if (objectUserLevel >= Filter_Roles::manager ()) { // Managers and higher roles have access to all Bibles. tbody.push_back ("(" + translate ("all") + ")"); } else { tbody.push_back ("<a href=\"?user="******"&addbible=\">➕</a>"); } tbody.push_back ("</td>"); tbody.push_back ("<td>│</td>"); tbody.push_back ("<td>"); if (objectUserLevel >= Filter_Roles::manager ()) { // Managers and higher roles have all privileges. tbody.push_back ("(" + translate ("all") + ")"); } else { tbody.push_back ("<a href=\"privileges?user="******"\">" + translate ("edit") + "</a>"); } tbody.push_back ("</td>"); // Logging for another user. if (myLevel > objectUserLevel) { tbody.push_back ("<td>│</td>"); tbody.push_back ("<td>"); tbody.push_back ("<a href=\"?user="******"&login\">" + translate ("Login") + "</a>"); tbody.push_back ("</td>"); } tbody.push_back ("</tr>"); } view.set_variable ("tbody", filter_string_implode (tbody, "\n")); page += view.render ("manage", "users"); page += Assets_Page::footer (); if (user_updated) notes_logic_maintain_note_assignees (true); if (privileges_updated) database_privileges_client_create (objectUsername, true); return page; }
void statistics_statistics () { Webserver_Request request; Database_Mail database_mail = Database_Mail (&request); Database_Modifications database_modifications; Database_Notes database_notes (&request); Database_Logs::log (translate("Sending statistics"), Filter_Roles::manager ()); string siteUrl = config_logic_site_url (); vector <string> bibles = request.database_bibles()->getBibles (); vector <string> users = request.database_users ()->getUsers (); for (auto & user : users) { string subject = "Bibledit " + translate("statistics"); vector <string> body; if (request.database_config_user()->getUserPendingChangesNotification (user)) { vector <int> ids = database_modifications.getNotificationIdentifiers (user); body.push_back ("<p><a href=\"" + siteUrl + changes_changes_url () + "\">" + translate("Number of change notifications") + "</a>: " + convert_to_string (ids.size()) + "</p>\n"); } if (request.database_config_user()->getUserAssignedNotesStatisticsNotification (user)) { vector <int> ids = database_notes.selectNotes ( bibles, // Bibles. 0, // Book 0, // Chapter 0, // Verse 3, // Passage selector. 0, // Edit selector. 0, // Non-edit selector. "", // Status selector. "", // Bible selector. user, // Assignment selector. 0, // Subscription selector. -1, // Severity selector. 0, // Text selector. "", // Search text. -1); // Limit. body.push_back ("<p><a href=\"" + siteUrl + notes_index_url () + "?presetselection=assigned\">" + translate("Number of consultation notes assigned to you awaiting your response") + "</a>: " + convert_to_string (ids.size ()) + "</p>\n"); } if (request.database_config_user()->getUserSubscribedNotesStatisticsNotification (user)) { body.push_back ("<p>" + translate("Number of consultation notes you are subscribed to") + ":</p>\n"); body.push_back ("<ul>\n"); request.session_logic ()->setUsername (user); vector <int> ids = database_notes.selectNotes ( bibles, // Bible. 0, // Book 0, // Chapter 0, // Verse 3, // Passage selector. 0, // Edit selector. 0, // Non-edit selector. "", // Status selector. "", // Bible selector. "", // Assignment selector. 1, // Subscription selector. -1, // Severity selector. 0, // Text selector. "", // Search text. -1); // Limit. body.push_back ("<li><a href=\"" + siteUrl + notes_index_url () + "?presetselection=subscribed\">" + translate("Total") + "</a>: " + convert_to_string (ids.size ()) + "</li>\n"); ids = database_notes.selectNotes ( bibles, // Bible. 0, // Book 0, // Chapter 0, // Verse 3, // Passage selector. 0, // Edit selector. 1, // Non-edit selector. "", // Status selector. "", // Bible selector. "", // Assignment selector. 1, // Subscription selector. -1, // Severity selector. 0, // Text selector. "", // Search text. -1); // Limit. body.push_back ("<li><a href=\"" + siteUrl + notes_index_url () + "?presetselection=subscribeddayidle\">" + translate("Inactive for a day") + "</a>: " + convert_to_string (ids.size ()) + "</li>\n"); ids = database_notes.selectNotes ( bibles, // Bible. 0, // Book 0, // Chapter 0, // Verse 3, // Passage selector. 0, // Edit selector. 3, // Non-edit selector. "", // Status selector. "", // Bible selector. "", // Assignment selector. 1, // Subscription selector. -1, // Severity selector. 0, // Text selector. "", // Search text. -1); // Limit. body.push_back ("<li><a href=\"" + siteUrl + notes_index_url () + "?presetselection=subscribedweekidle\">" + translate("Inactive for a week") + "</a>: " + convert_to_string (ids.size ()) + "</li>\n"); body.push_back ("</ul>\n"); request.session_logic ()->setUsername (""); } if (!body.empty ()) { string mailbody = filter_string_implode (body, "\n"); database_mail.send (user, subject, mailbody); } } }
// Cleans and resets the data in the Bibledit installation. void demo_clean_data () { Database_Logs::log ("Cleaning up the demo data"); Webserver_Request request; // Set user to the demo credentials (admin) as this is the user who is always logged-in in a demo installation. request.session_logic ()->setUsername (session_admin_credentials ()); // Delete empty stylesheet that may have been there. request.database_styles()->revokeWriteAccess ("", styles_logic_standard_sheet ()); request.database_styles()->deleteSheet (""); styles_sheets_create_all (); // Set the export stylesheet to "Standard" for all Bibles and the admin. vector <string> bibles = request.database_bibles()->getBibles (); for (auto & bible : bibles) { Database_Config_Bible::setExportStylesheet (bible, styles_logic_standard_sheet ()); } request.database_config_user()->setStylesheet (styles_logic_standard_sheet ()); // Set the site language to "Default" Database_Config_General::setSiteLanguage (""); // Ensure the default users are there. map <string, int> users = { make_pair ("guest", Filter_Roles::guest ()), make_pair ("member", Filter_Roles::member ()), make_pair ("consultant", Filter_Roles::consultant ()), make_pair ("translator", Filter_Roles::translator ()), make_pair ("manager", Filter_Roles::manager ()), make_pair (session_admin_credentials (), Filter_Roles::admin ()) }; for (auto & element : users) { if (!request.database_users ()->usernameExists (element.first)) { request.database_users ()->addNewUser(element.first, element.first, element.second, ""); } request.database_users ()->updateUserLevel (element.first, element.second); } // Create / update sample Bible. demo_create_sample_bible (); // Create sample notes. demo_create_sample_notes (&request); // Create samples for the workbenches. demo_create_sample_workbenches (&request); // Set navigator to John 3:16. Ipc_Focus::set (&request, 43, 3, 16); // Set and/or trim resources to display. // Too many resources crash the demo: Limit the amount. vector <string> resources = request.database_config_user()->getActiveResources (); bool reset_resources = false; if (resources.size () > 25) reset_resources = true; vector <string> defaults = demo_logic_default_resources (); for (auto & name : defaults) { if (!in_array (name, resources)) reset_resources = true; } if (reset_resources) { resources = demo_logic_default_resources (); request.database_config_user()->setActiveResources (resources); } // No flipped basic <> advanded mode. request.database_config_user ()->setFlipInterfaceMode (false); }
string user_account (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; string page; Assets_Header header = Assets_Header (translate("Account"), webserver_request); header.addBreadCrumb (menu_logic_settings_menu (), menu_logic_settings_text ()); page = header.run (); Assets_View view; string username = request->session_logic()->currentUser (); string email = request->database_users()->getUserToEmail (username); bool actions_taken = false; vector <string> success_messages; // Form submission handler. if (request->post.count ("submit")) { bool form_is_valid = true; string currentpassword = request->post ["currentpassword"]; string newpassword = request->post ["newpassword"]; string newpassword2 = request->post ["newpassword2"]; string newemail = request->post ["newemail"]; if ((newpassword != "") || (newpassword2 != "")) { if (newpassword.length () < 4) { form_is_valid = false; view.set_variable ("new_password_invalid_message", translate("Password should be at least four characters long")); } if (newpassword2.length () < 4) { form_is_valid = false; view.set_variable ("new_password2_invalid_message", translate("Password should be at least four characters long")); } if (newpassword2 != newpassword) { form_is_valid = false; view.set_variable ("new_password2_invalid_message", translate("Passwords do not match")); } if (!request->database_users()->matchUsernamePassword (username, currentpassword)) { form_is_valid = false; view.set_variable ("current_password_invalid_message", translate("Current password is not valid")); } if (form_is_valid) { request->database_users()->updateUserPassword (username, newpassword); actions_taken = true; success_messages.push_back (translate("The new password was saved")); } } if (newemail != "") { if (!filter_url_email_is_valid (newemail)) { form_is_valid = false; view.set_variable ("new_email_invalid_message", translate("Email address is not valid")); } if (!request->database_users()->matchUsernamePassword (username, currentpassword)) { form_is_valid = false; view.set_variable ("current_password_invalid_message", translate("Current password is not valid")); } if (form_is_valid) { Confirm_Worker confirm_worker = Confirm_Worker (webserver_request); string initial_subject = translate("Email address verification"); string initial_body = translate("Somebody requested to change the email address that belongs to your account."); string query = request->database_users()->updateEmailQuery (username, newemail); string subsequent_subject = translate("Email address change"); string subsequent_body = translate("The email address that belongs to your account has been changed successfully."); confirm_worker.setup (newemail, initial_subject, initial_body, query, subsequent_subject, subsequent_body); actions_taken = true; success_messages.push_back (translate("A verification email was sent to ") + newemail); } } if (!actions_taken) { success_messages.push_back (translate("No changes were made")); } } view.set_variable ("username", filter_string_sanitize_html (username)); view.set_variable ("email", filter_string_sanitize_html (email)); string success_message = filter_string_implode (success_messages, "\n"); view.set_variable ("success_messages", success_message); if (!actions_taken) view.enable_zone ("no_actions_taken"); page += view.render ("user", "account"); page += Assets_Page::footer (); return page; }
string changes_changes (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; Database_Modifications database_modifications; Database_Notes database_notes = Database_Notes (request); bool touch = request->session_logic ()->touchEnabled (); string page; Assets_Header header = Assets_Header (translate("Changes"), request); header.setStylesheet (); header.addBreadCrumb (menu_logic_translate_menu (), menu_logic_translate_text ()); if (touch) header.jQueryMobileTouchOn (); page += header.run (); Assets_View view; string username = request->session_logic()->currentUser (); // Handle AJAX call to remove a change notification. if (request->post.count ("remove")) { int remove = convert_to_int (request->post["remove"]); trash_change_notification (request, remove); database_modifications.deleteNotification (remove); #ifdef CLIENT_PREPARED request->database_config_user ()->addRemovedChange (remove); #endif request->database_config_user ()->setChangeNotificationsChecksum (""); return ""; } // Handle AJAX call to navigate to the passage belonging to the change notification. if (request->post.count ("navigate")) { string navigate = request->post["navigate"]; int id = convert_to_int (navigate); Passage passage = database_modifications.getNotificationPassage (id); if (passage.book) { Ipc_Focus::set (request, passage.book, passage.chapter, convert_to_int (passage.verse)); Navigation_Passage::recordHistory (request, passage.book, passage.chapter, convert_to_int (passage.verse)); } // Set the correct default Bible for the user. string bible = database_modifications.getNotificationBible (id); if (!bible.empty ()) request->database_config_user()->setBible (bible); return ""; } // Remove personal changes notifications and their matching change notifications in the Bible. if (request->query.count ("match")) { vector <int> ids = database_modifications.clearNotificationMatches (username, changes_personal_category (), changes_bible_category ()); #ifdef CLIENT_PREPARED // Client records deletions for sending to the Cloud. for (auto & id : ids) { request->database_config_user ()->addRemovedChange (id); } #endif // Clear checksum cache. request->database_config_user ()->setChangeNotificationsChecksum (""); } // Remove all the personal change notifications. if (request->query.count ("personal")) { vector <int> ids = database_modifications.getNotificationTeamIdentifiers (username, changes_personal_category (), true); for (auto id : ids) { trash_change_notification (request, id); database_modifications.deleteNotification (id); #ifdef CLIENT_PREPARED request->database_config_user ()->addRemovedChange (id); #endif request->database_config_user ()->setChangeNotificationsChecksum (""); } } // Remove all the Bible change notifications. if (request->query.count ("bible")) { vector <int> ids = database_modifications.getNotificationTeamIdentifiers (username, changes_bible_category (), true); for (auto id : ids) { trash_change_notification (request, id); database_modifications.deleteNotification (id); #ifdef CLIENT_PREPARED request->database_config_user ()->addRemovedChange (id); #endif request->database_config_user ()->setChangeNotificationsChecksum (""); } } // Remove all the change notifications made by a certain user. if (request->query.count ("dismiss")) { string user = request->query ["dismiss"]; vector <int> ids = database_modifications.getNotificationTeamIdentifiers (username, user, true); for (auto id : ids) { trash_change_notification (request, id); database_modifications.deleteNotification (id); #ifdef CLIENT_PREPARED request->database_config_user ()->addRemovedChange (id); #endif request->database_config_user ()->setChangeNotificationsChecksum (""); } } // Read the identifiers. // Limit the number of results to keep the page reasonabley fast even if there are many notifications. vector <int> personal_ids = database_modifications.getNotificationPersonalIdentifiers (username, changes_personal_category (), true); vector <int> bible_ids = database_modifications.getNotificationTeamIdentifiers (username, changes_bible_category (), true); vector <int> ids = database_modifications.getNotificationIdentifiers (username, true); string textblock; for (auto id : ids) { Passage passage = database_modifications.getNotificationPassage (id); string link = filter_passage_link_for_opening_editor_at (passage.book, passage.chapter, passage.verse); string category = database_modifications.getNotificationCategory (id); if (category == changes_personal_category ()) category = "😊"; if (category == changes_bible_category ()) category = "📖"; string modification = database_modifications.getNotificationModification (id); textblock.append ("<div id=\"entry" + convert_to_string (id) + "\">\n"); textblock.append ("<a href=\"expand\" id=\"expand" + convert_to_string (id) + "\"> ⊞ </a>\n"); textblock.append ("<a href=\"remove\" id=\"remove" + convert_to_string (id) + "\"> ✗ </a>\n"); textblock.append (link + "\n"); textblock.append (category + "\n"); textblock.append (modification + "\n"); textblock.append ("</div>\n"); } view.set_variable ("textblock", textblock); string loading = "\"" + translate("Loading ...") + "\""; string script = "var loading = " + loading + ";"; view.set_variable ("script", script); // Enable links to dismiss categories of notifications depending on whether there's anything to dismiss. if (!personal_ids.empty () && !bible_ids.empty ()) view.enable_zone ("matching"); if (!personal_ids.empty ()) view.enable_zone ("personal"); if (!bible_ids.empty ()) view.enable_zone ("bible"); // Add links to clear the notifications from the individual contributors. string dismissblock; vector <string> users = request->database_users ()->getUsers (); for (auto & user : users) { vector <int> ids = database_modifications.getNotificationTeamIdentifiers (username, user, true); if (!ids.empty ()) { dismissblock.append ("<p>* <a href=\"?dismiss="); dismissblock.append (user); dismissblock.append ("\">"); dismissblock.append (user); dismissblock.append (": "); dismissblock.append (translate("all of them")); dismissblock.append (": "); dismissblock.append (convert_to_string (ids.size ())); dismissblock.append ("</a></p>\n"); } } view.set_variable ("dismissblock", dismissblock); view.set_variable ("VERSION", config_logic_version ()); if (touch) view.enable_zone ("touch"); page += view.render ("changes", "changes"); page += Assets_Page::footer (); return page; }
void changes_modifications () { Database_Logs::log ("Change notifications: Generating", Filter_Roles::translator ()); // Notifications are not available to clients for the duration of processing them. config_globals_change_notifications_available = false; // Data objects. Webserver_Request request; Database_Modifications database_modifications; // Check on the health of the modifications database and (re)create it if needed. if (!database_modifications.healthy ()) database_modifications.erase (); database_modifications.create (); // Create online change notifications for users who made changes in Bibles // through the web editor or through a client. // It runs before the team changes. // This produces the desired order of the notifications in the GUI. // At the same time, produce change statistics per user. // Get the users who will receive the changes entered by the contributors. vector <string> recipients; { vector <string> users = request.database_users ()->getUsers (); for (auto & user : users) { if (request.database_config_user ()->getContributorChangesNotificationsOnline (user)) { recipients.push_back (user); } } } // Storage for the statistics. map <string, int> user_change_statistics; float modification_time_total = 0; int modification_time_count = 0; vector <string> users = database_modifications.getUserUsernames (); if (!users.empty ()) Database_Logs::log ("Change notifications: Per user", Filter_Roles::translator ()); for (auto user : users) { // Total changes made by this user. int change_count = 0; // Go through the Bibles changed by the current user. vector <string> bibles = database_modifications.getUserBibles (user); for (auto bible : bibles) { // Body of the email to be sent. string email = "<p>" + translate("You have entered the changes below in a Bible editor.") + " " + translate ("You may check if it made its way into the Bible text.") + "</p>"; size_t empty_email_length = email.length (); // Go through the books in that Bible. vector <int> books = database_modifications.getUserBooks (user, bible); for (auto book : books) { // Go through the chapters in that book. vector <int> chapters = database_modifications.getUserChapters (user, bible, book); for (auto chapter : chapters) { // Get the sets of identifiers for that chapter, and set some variables. vector <Database_Modifications_Id> IdSets = database_modifications.getUserIdentifiers (user, bible, book, chapter); //int referenceOldId = 0; int referenceNewId = 0; int newId = 0; int lastNewId = 0; bool restart = true; // Go through the sets of identifiers. for (auto IdSet : IdSets) { int oldId = IdSet.oldid; newId = IdSet.newid; if (restart) { changes_process_identifiers (&request, user, recipients, bible, book, chapter, referenceNewId, newId, email, change_count, modification_time_total, modification_time_count); //referenceOldId = oldId; referenceNewId = newId; lastNewId = newId; restart = false; continue; } if (oldId == lastNewId) { lastNewId = newId; } else { restart = true; } } // Process the last set of identifiers. changes_process_identifiers (&request, user, recipients, bible, book, chapter, referenceNewId, newId, email, change_count, modification_time_total, modification_time_count); } } // Check whether there's any email to be sent. if (email.length () != empty_email_length) { // Send the user email with the user's personal changes if the user opted to receive it. if (request.database_config_user()->getUserUserChangesNotification (user)) { string subject = translate("Changes you entered in") + " " + bible; if (!client_logic_client_enabled ()) email_schedule (user, subject, email); } } } // Store change statistics for this user. user_change_statistics [user] = change_count; // Clear the user's changes in the database. database_modifications.clearUserUser (user); // Clear checksum cache. request.database_config_user ()->setUserChangeNotificationsChecksum (user, ""); } // Generate the notifications, online and by email, // for the changes in the Bibles entered by anyone since the previous notifications were generated. vector <string> bibles = database_modifications.getTeamDiffBibles (); for (auto bible : bibles) { string stylesheet = Database_Config_Bible::getExportStylesheet (bible); vector <string> changeNotificationUsers; vector <string> users = request.database_users ()->getUsers (); for (auto user : users) { if (access_bible_read (&request, bible, user)) { if (request.database_config_user()->getUserGenerateChangeNotifications (user)) { changeNotificationUsers.push_back (user); } } } users.clear (); // The number of changes processed so far for this Bible. int processedChangesCount = 0; // The files get stored at http://site.org:<port>/revisions/<Bible>/<date> int seconds = filter_date_seconds_since_epoch (); string timepath; timepath.append (convert_to_string (filter_date_numerical_year (seconds))); timepath.append ("-"); timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_month (seconds)), 2, '0')); timepath.append ("-"); timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_month_day (seconds)), 2, '0')); timepath.append (" "); timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_hour (seconds)), 2, '0')); timepath.append (":"); timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_minute (seconds)), 2, '0')); timepath.append (":"); timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_second (seconds)), 2, '0')); string directory = filter_url_create_root_path ("revisions", bible, timepath); filter_url_mkdir (directory); // Produce the USFM and html files. filter_diff_produce_verse_level (bible, directory); // Create online page with changed verses. string versesoutputfile = filter_url_create_path (directory, "changed_verses.html"); filter_diff_run_file (filter_url_create_path (directory, "verses_old.txt"), filter_url_create_path (directory, "verses_new.txt"), versesoutputfile); // Storage for body of the email with the changes. vector <string> email_changes; // Generate the online change notifications. vector <int> books = database_modifications.getTeamDiffBooks (bible); for (auto book : books) { vector <int> chapters = database_modifications.getTeamDiffChapters (bible, book); for (auto chapter : chapters) { Database_Logs::log ("Change notifications: " + bible + " " + filter_passage_display (book, chapter, ""), Filter_Roles::translator ()); string old_chapter_usfm = database_modifications.getTeamDiff (bible, book, chapter); string new_chapter_usfm = request.database_bibles()->getChapter (bible, book, chapter); vector <int> old_verse_numbers = usfm_get_verse_numbers (old_chapter_usfm); vector <int> new_verse_numbers = usfm_get_verse_numbers (new_chapter_usfm); vector <int> verses = old_verse_numbers; verses.insert (verses.end (), new_verse_numbers.begin (), new_verse_numbers.end ()); verses = array_unique (verses); sort (verses.begin (), verses.end()); for (auto verse : verses) { string old_verse_usfm = usfm_get_verse_text (old_chapter_usfm, verse); string new_verse_usfm = usfm_get_verse_text (new_chapter_usfm, verse); if (old_verse_usfm != new_verse_usfm) { processedChangesCount++; // In case of too many change notifications, processing them would take too much time, so take a few shortcuts. string old_html = "<p>" + old_verse_usfm + "</p>"; string new_html = "<p>" + new_verse_usfm + "</p>"; string old_text = old_verse_usfm; string new_text = new_verse_usfm; if (processedChangesCount < 800) { Filter_Text filter_text_old = Filter_Text (bible); Filter_Text filter_text_new = Filter_Text (bible); filter_text_old.html_text_standard = new Html_Text (""); filter_text_new.html_text_standard = new Html_Text (""); filter_text_old.text_text = new Text_Text (); filter_text_new.text_text = new Text_Text (); filter_text_old.addUsfmCode (old_verse_usfm); filter_text_new.addUsfmCode (new_verse_usfm); filter_text_old.run (stylesheet); filter_text_new.run (stylesheet); old_html = filter_text_old.html_text_standard->getInnerHtml (); new_html = filter_text_new.html_text_standard->getInnerHtml (); old_text = filter_text_old.text_text->get (); new_text = filter_text_new.text_text->get (); } string modification = filter_diff_diff (old_text, new_text); database_modifications.recordNotification (changeNotificationUsers, changes_bible_category (), bible, book, chapter, verse, old_html, modification, new_html); string passage = filter_passage_display (book, chapter, convert_to_string (verse)) + ": "; if (old_text != new_text) { email_changes.push_back (passage + modification); } else { email_changes.push_back (translate ("The following passage has no change in the text.") + " " + translate ("The change is in the formatting only.") + " " + translate ("The USFM code is given for reference.")); email_changes.push_back (passage); email_changes.push_back (translate ("Old code:") + " " + old_verse_usfm); email_changes.push_back (translate ("New code:") + " " + new_verse_usfm); } } } // Delete the diff data for this chapter, for two reasons: // 1. New diffs for this chapter can be stored straightaway. // 2. In case of large amounts of diff data, and this function gets killed, // then the next time it runs again, it will continue from where it was killed. database_modifications.deleteTeamDiffChapter (bible, book, chapter); } } // Email the changes to the subscribed users. if (!email_changes.empty ()) { string body; for (auto & line : email_changes) { body.append ("<div>"); body.append (line); body.append ("</div>\n"); } string subject = translate("Recent changes:") + " " + bible; vector <string> users = request.database_users ()->getUsers (); for (auto & user : users) { if (request.database_config_user()->getUserBibleChangesNotification (user)) { if (access_bible_read (&request, bible, user)) { if (!client_logic_client_enabled ()) { email_schedule (user, subject, body); } } } } } } // Index the data and remove expired notifications. Database_Logs::log ("Change notifications: Indexing", Filter_Roles::translator ()); database_modifications.indexTrimAllNotifications (); // Remove expired downloadable revisions. string directory = filter_url_create_root_path ("revisions"); int now = filter_date_seconds_since_epoch (); bibles = filter_url_scandir (directory); for (auto &bible : bibles) { string folder = filter_url_create_path (directory, bible); int time = filter_url_file_modification_time (folder); int days = (now - time) / 86400; if (days > 31) { filter_url_rmdir (folder); } else { vector <string> revisions = filter_url_scandir (folder); for (auto & revision : revisions) { string path = filter_url_create_path (folder, revision); int time = filter_url_file_modification_time (path); int days = (now - time) / 86400; if (days > 31) { filter_url_rmdir (path); Database_Logs::log ("Removing expired downloadable revision notification: " + bible + " " + revision, Filter_Roles::translator ()); } } } } // Clear checksum caches. users = request.database_users ()->getUsers (); for (auto user : users) { request.database_config_user ()->setUserChangeNotificationsChecksum (user, ""); } // Vacuum the modifications index, as it might have been updated. database_modifications.vacuum (); // Make the notifications available again to clients. config_globals_change_notifications_available = true; // Store the statistics in the database. if (modification_time_count) { // Take average timestamp of all timestamps. int timestamp = round (modification_time_total / modification_time_count); for (auto & element : user_change_statistics) { // Store dated change statistics per user. string user = element.first; int count = element.second; Database_Statistics::store_changes (timestamp, user, count); } } Database_Logs::log ("Change notifications: Ready", Filter_Roles::translator ()); }