string edit_load (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; string bible = request->query ["bible"]; int book = convert_to_int (request->query ["book"]); int chapter = convert_to_int (request->query ["chapter"]); // Store a copy of the USFM loaded in the editor for later reference. storeLoadedUsfm (webserver_request, bible, book, chapter, "edit"); string stylesheet = request->database_config_user()->getStylesheet (); string usfm = request->database_bibles()->getChapter (bible, book, chapter); Editor_Usfm2Html editor_usfm2html; editor_usfm2html.load (usfm); editor_usfm2html.stylesheet (stylesheet); editor_usfm2html.run (); string html = editor_usfm2html.get (); // To make editing empty verses easier, convert spaces to non-breaking spaces, so they appear in the editor. if (usfm_contains_empty_verses (usfm)) { string search = "<span> </span>"; string replace = "<span>" + unicode_non_breaking_space_entity () + "</span>"; html = filter_string_str_replace (search, replace, html); } string user = request->session_logic ()->currentUser (); bool write = access_bible_book_write (webserver_request, user, bible, book); return Checksum_Logic::send (html, write); }
string Navigation_Passage::getChaptersFragment (void * webserver_request, string bible, int book, int chapter) { Webserver_Request * request = (Webserver_Request *) webserver_request; vector <int> chapters; if (bible.empty ()) { Database_Versifications database_versifications; chapters = database_versifications.getChapters ("English", book, true); } else { chapters = request->database_bibles()->getChapters (bible, book); } string html; addSelectorLink (html, "previous", "applychapter", "[" + translate ("previous") + "]", false); addSelectorLink (html, "next", "applychapter", "[" + translate ("next") + "]", false); addSelectorLink (html, "cancel", "applychapter", "[" + translate ("cancel") + "]", false); for (auto ch : chapters) { bool selected = (ch == chapter); addSelectorLink (html, convert_to_string (ch), "applychapter", convert_to_string (ch), selected); } addSelectorLink (html, "cancel", "applychapter", "[" + translate ("cancel") + "]", false); html.insert (0, "<span id=\"applychapter\">" + translate ("Select chapter")); html.append ("</span>"); return html; }
string public_create (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; Database_Notes database_notes (webserver_request); Notes_Logic notes_logic = Notes_Logic (webserver_request); string page; Assets_Header header = Assets_Header (translate("Create note"), request); page += header.run (); Assets_View view; string bible = request->database_config_user()->getBible (); int book = Ipc_Focus::getBook (webserver_request); int chapter = Ipc_Focus::getChapter (webserver_request); int verse = Ipc_Focus::getVerse (webserver_request); string chapter_usfm = request->database_bibles()->getChapter (bible, book, chapter); string verse_usfm = usfm_get_verse_text (chapter_usfm, verse); string stylesheet = Database_Config_Bible::getExportStylesheet (bible); Filter_Text filter_text = Filter_Text (bible); filter_text.html_text_standard = new Html_Text (bible); filter_text.addUsfmCode (verse_usfm); filter_text.run (stylesheet); string versetext = filter_text.html_text_standard->getInnerHtml (); view.set_variable ("versetext", versetext); if (request->post.count ("submit")) { string summary = filter_string_trim (request->post["summary"]); if (summary.empty ()) summary = translate ("Feedback"); string contents = "<p>" + versetext + "</p>" + filter_string_trim (request->post["contents"]); int identifier = notes_logic.createNote (bible, book, chapter, verse, summary, contents, false); // A note created by a public user is made public to all. database_notes.setPublic (identifier, true); // Subscribe the user to the note. // Then the user receives email about any updates made on this note. database_notes.subscribe (identifier); // Go to the main public notes page. redirect_browser (request, public_index_url ()); return ""; } if (request->post.count ("cancel")) { redirect_browser (request, public_index_url ()); return ""; } string passage = filter_passage_display (book, chapter, convert_to_string (verse)); view.set_variable ("passage", passage); page += view.render ("public", "create"); page += Assets_Page::footer (); return page; }
string checks_suppress (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; Database_Check database_check; string page; page = Assets_Page::header (translate ("Suppressed check results"), webserver_request); Assets_View view; if (request->query.count ("release")) { int release = convert_to_int (request->query["release"]); database_check.release (release); view.set_variable ("success", translate("The check result is no longer suppressed.")); } // Get the Bibles the user has write-access to. vector <string> bibles; { vector <string> all_bibles = request->database_bibles()->getBibles (); for (auto bible : all_bibles) { if (access_bible_write (webserver_request, bible)) { bibles.push_back (bible); } } } string block; vector <Database_Check_Hit> suppressions = database_check.getSuppressions (); for (auto suppression : suppressions) { string bible = suppression.bible; // Only display entries for Bibles the user has write access to. if (in_array (bible, bibles)) { int id = suppression.rowid; bible = filter_string_sanitize_html (bible); string passage = filter_passage_display_inline ({Passage ("", suppression.book, suppression.chapter, convert_to_string (suppression.verse))}); string result = filter_string_sanitize_html (suppression.data); result.insert (0, bible + " " + passage + " "); block.append ("<p style=\"color:grey;\">\n"); block.append ("<a href=\"suppress?release=" + convert_to_string (id) + "\">\n"); block.append (emoji_wastebasket () + "\n"); block.append ("</a>\n"); block.append (result + "\n"); block.append ("</p>\n"); } } view.set_variable ("block", block); page += view.render ("checks", "suppress"); page += Assets_Page::footer (); return page; }
string editone_preview (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; bool touch = request->session_logic ()->touchEnabled (); string page; Assets_Header header = Assets_Header (translate("Preview"), request); header.setNavigator (); header.setEditorStylesheet (); if (touch) header.jQueryTouchOn (); header.addBreadCrumb (menu_logic_translate_menu (), menu_logic_translate_text ()); header.refresh (5, "index"); page = header.run (); Assets_View view; // Get active Bible, and check read access to it. // If needed, change Bible to one it has read access to. string bible = access_bible_clamp (request, request->database_config_user()->getBible ()); string cls = Filter_Css::getClass (bible); string font = Fonts_Logic::getTextFont (bible); int direction = Database_Config_Bible::getTextDirection (bible); int lineheight = Database_Config_Bible::getLineHeight (bible); int letterspacing = Database_Config_Bible::getLetterSpacing (bible); view.set_variable ("custom_class", cls); view.set_variable ("custom_css", Filter_Css::getCss (cls, Fonts_Logic::getFontPath (font), direction, lineheight, letterspacing)); int book = Ipc_Focus::getBook (webserver_request); int chapter = Ipc_Focus::getChapter (webserver_request); //int verse = Ipc_Focus::getVerse (webserver_request); string stylesheet = request->database_config_user()->getStylesheet (); string usfm = request->database_bibles()->getChapter (bible, book, chapter); Editor_Usfm2Html editor_usfm2html; editor_usfm2html.load (usfm); editor_usfm2html.stylesheet (stylesheet); editor_usfm2html.run (); string html = editor_usfm2html.get (); view.set_variable ("html", html); page += view.render ("editone", "preview"); page += Assets_Page::footer (); return page; }
string edit_focus (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; // Get Bible: If an empty Bible is given, bail out. string bible = request->query ["bible"]; if (bible.empty ()) return ""; // Get book: If no book is given: Bail out. int book = convert_to_int (request->query ["book"]); if (!book) return ""; // Get chapter. int chapter = convert_to_int (request->query ["chapter"]); string stylesheet = request->database_config_user()->getStylesheet (); string usfm = request->database_bibles()->getChapter (bible, book, chapter); int verse = Ipc_Focus::getVerse (request); Editor_Usfm2Html editor_usfm2html; editor_usfm2html.load (usfm); editor_usfm2html.stylesheet (stylesheet); editor_usfm2html.run (); int startingOffset = 0; int endingOffset = 0; // To deal with a combined verse, go through the offsets, and pick the correct one. for (auto element : editor_usfm2html.verseStartOffsets) { int vs = element.first; int offset = element.second; if (vs <= verse) startingOffset = offset; if (endingOffset == 0) { if (vs > verse) { endingOffset = offset; } } } if (verse) { startingOffset += convert_to_string (verse).length () + 1; } if (endingOffset) { endingOffset--; } else { endingOffset = editor_usfm2html.textLength; } string data = convert_to_string (startingOffset); data.append ("\n"); data.append (convert_to_string (endingOffset)); return data; }
Passage Navigation_Passage::getPreviousVerse (void * webserver_request, string bible, int book, int chapter, int verse) { verse--; if (bible != "") { Webserver_Request * request = (Webserver_Request *) webserver_request; vector <int> verses = usfm_get_verse_numbers (request->database_bibles()->getChapter (bible, book, chapter)); if (find (verses.begin(), verses.end(), verse) == verses.end()) { if (!verses.empty ()) verse = verses [0]; } } Passage passage = Passage ("", book, chapter, convert_to_string (verse)); return passage; }
Passage Navigation_Passage::getPreviousChapter (void * webserver_request, string bible, int book, int chapter) { chapter--; if (bible != "") { Webserver_Request * request = (Webserver_Request *) webserver_request; vector <int> chapters = request->database_bibles ()->getChapters (bible, book); if (find (chapters.begin(), chapters.end(), chapter) == chapters.end()) { if (!chapters.empty ()) chapter = chapters [0]; } } Passage passage = Passage ("", book, chapter, "1"); return passage; }
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; }
string xrefs_next (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; string bible = request->database_config_user()->getTargetXrefBible (); int currentBook = Ipc_Focus::getBook (webserver_request); int currentChapter = Ipc_Focus::getChapter (webserver_request); Passage currentPassage = Passage ("", currentBook, currentChapter, "1"); int currentLocation = filter_passage_to_integer (currentPassage); vector <int> books = request->database_bibles()->getBooks (bible); for (auto book : books) { vector <int> chapters = request->database_bibles()->getChapters (bible, book); for (auto chapter : chapters) { if (chapter == 0) continue; Passage passage = Passage ("", book, chapter, "1"); int location = filter_passage_to_integer (passage); if (location > currentLocation) { string usfm = request->database_bibles()->getChapter (bible, book, chapter); auto xrefs = usfm_extract_notes (usfm, {"x"}); if (xrefs.empty ()) { Ipc_Focus::set (webserver_request, book, chapter, 1); redirect_browser (request, xrefs_index_url ()); return ""; } } } } redirect_browser (request, xrefs_index_url ()); return ""; }
// Function to safely store a chapter. // It saves the chapter if the new USFM does not differ too much from the existing USFM. // On success it returns an empty string. // On failure it returns the reason of the failure. // This function proves useful in cases that the text in the Bible editor gets corrupted // due to human error. // It also is useful in cases where the session is deleted from the server, // where the text in the editors would get corrupted. // It also is useful in view of an unstable connection between browser and server, to prevent data corruption. string usfm_safely_store_chapter (void * webserver_request, string bible, int book, int chapter, string usfm) { Webserver_Request * request = (Webserver_Request *) webserver_request; // Existing chapter contents. string existing = request->database_bibles()->getChapter (bible, book, chapter); // Bail out if the existing chapter equals the USFM to be saved. if (usfm == existing) return ""; // Safety check. string message = usfm_save_is_safe (webserver_request, existing, usfm, true); if (!message.empty ()) return message; // Safety checks have passed: Save chapter. Bible_Logic::storeChapter (bible, book, chapter, usfm); return ""; }
string editverse_load (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; string bible = request->query ["bible"]; int book = convert_to_int (request->query ["book"]); int chapter = convert_to_int (request->query ["chapter"]); int verse = convert_to_int (request->query ["verse"]); string usfm = request->database_bibles()->getChapter (bible, book, chapter); usfm = usfm_get_verse_text (usfm, verse); usfm = filter_string_str_replace ("\n", "<br>", usfm); string chapter_verse_text; string needle = "\\c"; if (verse) needle = "\\v"; size_t pos = usfm.find (needle); if (pos != string::npos) { if (pos < 2) { usfm.erase (0, pos + 2); usfm = filter_string_trim (usfm); chapter_verse_text = usfm_peek_verse_number (usfm); usfm.erase (0, chapter_verse_text.length ()); usfm = filter_string_trim (usfm); } } usfm.insert (0, "<span contenteditable=\"true\">"); usfm.append ("</span>"); if (!chapter_verse_text.empty ()) { string spacer; if (verse) spacer = " "; usfm.insert (0, "<span " + filter_css_grey_background () + ">" + needle + " " + chapter_verse_text + spacer + "</span>"); } string user = request->session_logic ()->currentUser (); bool write = access_bible_book_write (webserver_request, user, bible, book); return Checksum_Logic::send (usfm, write); }
string Navigation_Passage::getVersesFragment (void * webserver_request, string bible, int book, int chapter, int verse) { Webserver_Request * request = (Webserver_Request *) webserver_request; vector <int> verses; if (bible == "") { Database_Versifications database_versifications; verses = database_versifications.getVerses (english (), book, chapter); } else { verses = usfm_get_verse_numbers (request->database_bibles()->getChapter (bible, book, chapter)); } string html; html.append (" "); for (auto vs : verses) { bool selected = (verse == vs); addSelectorLink (html, convert_to_string (vs), "applyverse", convert_to_string (vs), selected); } addSelectorLink (html, "cancel", "applyverse", "[" + translate ("cancel") + "]", false); html.insert (0, "<span id=\"applyverse\">" + translate ("Select verse")); html.append ("</span>"); return html; }
// 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"); } } } }
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; }
string editusfm_save (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; string bible = request->post["bible"]; int book = convert_to_int (request->post["book"]); int chapter = convert_to_int (request->post["chapter"]); string usfm = request->post["usfm"]; string checksum = request->post["checksum"]; if (request->post.count ("bible") && request->post.count ("book") && request->post.count ("chapter") && request->post.count ("usfm")) { if (Checksum_Logic::get (usfm) == checksum) { usfm = filter_url_tag_to_plus (usfm); usfm = filter_string_trim (usfm); if (usfm != "") { if (unicode_string_is_valid (usfm)) { string stylesheet = request->database_config_user()->getStylesheet(); vector <BookChapterData> book_chapter_text = usfm_import (usfm, stylesheet); for (BookChapterData & data : book_chapter_text) { int book_number = data.book; int chapter_number = data.chapter; string chapter_data_to_save = data.data; if (((book_number == book) || (book_number == 0)) && (chapter_number == chapter)) { string ancestor_usfm = getLoadedUsfm (webserver_request, bible, book, chapter, "editusfm"); // Collect some data about the changes for this user. string username = request->session_logic()->currentUser (); int oldID = request->database_bibles()->getChapterId (bible, book, chapter); string oldText = ancestor_usfm; string newText = chapter_data_to_save; // Merge if the ancestor is there and differs from what's in the database. if (!ancestor_usfm.empty ()) { string server_usfm = request->database_bibles ()->getChapter (bible, book, chapter); if (server_usfm != ancestor_usfm) { // Prioritize the USFM to save. chapter_data_to_save = filter_merge_run (ancestor_usfm, server_usfm, chapter_data_to_save); Database_Logs::log (translate ("Merging and saving chapter.")); } } // Check on write access. if (access_bible_book_write (request, "", bible, book)) { // Safely store the chapter. string message = usfm_safely_store_chapter (request, bible, book, chapter, chapter_data_to_save); if (message.empty()) { #ifndef CLIENT_PREPARED // Server configuration: Store details for the user's changes. int newID = request->database_bibles()->getChapterId (bible, book, chapter); Database_Modifications database_modifications; database_modifications.recordUserSave (username, bible, book, chapter, oldID, oldText, newID, newText); Database_Git::store_chapter (username, bible, book, chapter, oldText, newText); #endif // Store a copy of the USFM loaded in the editor for later reference. storeLoadedUsfm (webserver_request, bible, book, chapter, "editusfm"); return locale_logic_text_saved (); } return message; } else { return translate("No write access"); } } else { Database_Logs::log ("The following data could not be saved and was discarded: " + chapter_data_to_save); return translate("Passage mismatch"); } } } else { Database_Logs::log ("The text was not valid Unicode UTF-8. The chapter could not saved and has been reverted to the last good version."); return translate("Needs Unicode"); } } else { Database_Logs::log ("There was no text. Nothing was saved. The original text of the chapter was reloaded."); return translate("Nothing to save"); } } else { request->response_code = 409; return translate("Checksum error"); } } else { return translate("Nothing to save"); } return translate ("Confusing data"); }
string checks_index (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; Database_Check database_check; string page; Assets_Header header = Assets_Header (translate("Checks"), webserver_request); header.addBreadCrumb (menu_logic_tools_menu (), menu_logic_tools_text ()); page = header.run (); Assets_View view; if (request->query.count ("approve")) { int approve = convert_to_int (request->query["approve"]); database_check.approve (approve); view.set_variable ("success", translate("The entry was suppressed.")); } if (request->query.count ("delete")) { int erase = convert_to_int (request->query["delete"]); database_check.erase (erase); view.set_variable ("success", translate("The entry was deleted for just now.")); } // Get the Bibles the user has write-access to. vector <int> bibleIDs; vector <string> bibles = request->database_bibles()->getBibles (); for (auto bible : bibles) { if (access_bible_write (webserver_request, bible)) { int id = request->database_bibles()->getID (bible); bibleIDs.push_back (id); } } string resultblock; vector <Database_Check_Hit> hits = database_check.getHits (); for (auto hit : hits) { int bibleID = hit.bible; if (find (bibleIDs.begin(), bibleIDs.end (), bibleID) != bibleIDs.end ()) { int id = hit.rowid; string bible = filter_string_sanitize_html (request->database_bibles()->getName (bibleID)); int book = hit.book; int chapter = hit.chapter; int verse = hit.verse; string link = filter_passage_link_for_opening_editor_at (book, chapter, convert_to_string (verse)); string information = filter_string_sanitize_html (hit.data); resultblock.append ("<p>\n"); resultblock.append ("<a href=\"index?approve=" + convert_to_string (id) + "\"> ✔ </a>\n"); resultblock.append ("<a href=\"index?delete=" + convert_to_string (id) + "\"> ✗ </a>\n"); resultblock.append (bible); resultblock.append (" "); resultblock.append (link); resultblock.append (" "); resultblock.append (information); resultblock.append ("</p>\n"); } } view.set_variable ("resultblock", resultblock); page += view.render ("checks", "index"); page += Assets_Page::footer (); return page; }
string resource_logic_get_html (void * webserver_request, string resource, int book, int chapter, int verse, bool add_verse_numbers) { Webserver_Request * request = (Webserver_Request *) webserver_request; string html; Database_UsfmResources database_usfmresources; Database_ImageResources database_imageresources; Database_Mappings database_mappings; // Lists of the various types of resources. vector <string> bibles = request->database_bibles()->getBibles (); vector <string> usfms; if (config_logic_client_prepared ()) { usfms = client_logic_usfm_resources_get (); // As from February 2016 a client no longer automatically downloads USFM resources off the server. // But a client takes in account existing USFM resources it has downloaded before. vector <string> old_usfms = database_usfmresources.getResources (); usfms.insert (usfms.end (), old_usfms.begin (), old_usfms.end ()); } else { usfms = database_usfmresources.getResources (); } vector <string> externals = resource_external_names (); vector <string> images = database_imageresources.names (); vector <string> lexicons = lexicon_logic_resource_names (); // Possible SWORD details. string sword_module = sword_logic_get_remote_module (resource); string sword_source = sword_logic_get_source (resource); // Determine the type of the current resource. bool isBible = in_array (resource, bibles); bool isUsfm = in_array (resource, usfms); bool isExternal = in_array (resource, externals); bool isImage = in_array (resource, images); bool isLexicon = in_array (resource, lexicons); bool isSword = (!sword_source.empty () && !sword_module.empty ()); // Retrieve versification system of the active Bible. string bible = request->database_config_user ()->getBible (); string bible_versification = Database_Config_Bible::getVersificationSystem (bible); // Determine the versification system of the current resource. string resource_versification; if (isBible || isUsfm) { resource_versification = Database_Config_Bible::getVersificationSystem (bible); } else if (isExternal) { resource_versification = resource_external_mapping (resource); } else if (isImage) { } else if (isLexicon) { resource_versification = database_mappings.original (); if (resource == KJV_LEXICON_NAME) resource_versification = "English"; } else if (isSword) { resource_versification = "English"; } else { } // If the resource versification system differs from the Bible's versification system, // map the focused verse of the Bible to a verse in the Resource. // There are resources without versification system: Do nothing about them. vector <Passage> passages; if ((bible_versification != resource_versification) && !resource_versification.empty ()) { passages = database_mappings.translate (bible_versification, resource_versification, book, chapter, verse); } else { passages.push_back (Passage ("", book, chapter, convert_to_string (verse))); } // If there's been a mapping, the resource should include the verse number for clarity. if (passages.size () != 1) add_verse_numbers = true; for (auto passage : passages) { if (verse != convert_to_int (passage.verse)) { add_verse_numbers = true; } } for (auto passage : passages) { int book = passage.book; int chapter = passage.chapter; int verse = convert_to_int (passage.verse); string possible_included_verse; if (add_verse_numbers) possible_included_verse = convert_to_string (verse) + " "; if (isImage) possible_included_verse.clear (); html.append (resource_logic_get_verse (webserver_request, resource, book, chapter, verse)); } return html; }
// This is the most basic version that fetches the text of a $resource. // It works on server and on client. // It uses the cache. string resource_logic_get_verse (void * webserver_request, string resource, int book, int chapter, int verse) { Webserver_Request * request = (Webserver_Request *) webserver_request; string data; Database_UsfmResources database_usfmresources; Database_ImageResources database_imageresources; // Lists of the various types of resources. // As from February 2016 a client no longer automatically downloads USFM resources from the Cloud. // But a client takes in account existing USFM resources it has downloaded before. vector <string> bibles = request->database_bibles()->getBibles (); vector <string> local_usfms = database_usfmresources.getResources (); vector <string> remote_usfms; if (config_logic_client_prepared ()) { remote_usfms = client_logic_usfm_resources_get (); } vector <string> externals = resource_external_names (); vector <string> images = database_imageresources.names (); vector <string> lexicons = lexicon_logic_resource_names (); // Possible SWORD details. string sword_module = sword_logic_get_remote_module (resource); string sword_source = sword_logic_get_source (resource); // Determine the type of the current resource. bool isBible = in_array (resource, bibles); bool isLocalUsfm = in_array (resource, local_usfms); bool isRemoteUsfm = in_array (resource, remote_usfms); bool isExternal = in_array (resource, externals); bool isImage = in_array (resource, images); bool isLexicon = in_array (resource, lexicons); bool isSword = (!sword_source.empty () && !sword_module.empty ()); if (isBible || isLocalUsfm) { string chapter_usfm; if (isBible) chapter_usfm = request->database_bibles()->getChapter (resource, book, chapter); if (isLocalUsfm) chapter_usfm = database_usfmresources.getUsfm (resource, book, chapter); string verse_usfm = usfm_get_verse_text (chapter_usfm, verse); string stylesheet = styles_logic_standard_sheet (); Filter_Text filter_text = Filter_Text (resource); filter_text.html_text_standard = new Html_Text (""); filter_text.addUsfmCode (verse_usfm); filter_text.run (stylesheet); data = filter_text.html_text_standard->getInnerHtml (); } else if (isRemoteUsfm) { data = resource_logic_client_fetch_cache_from_cloud (resource, book, chapter, verse); } else if (isExternal) { if (config_logic_client_prepared ()) { // A client fetches it from the cache or from the Cloud, // or, for older versions, from the offline resources database. // As of 12 December 2015, the offline resources database is not needed anymore. // It can be removed after a year or so. Database_OfflineResources database_offlineresources; if (database_offlineresources.exists (resource, book, chapter, verse)) { data = database_offlineresources.get (resource, book, chapter, verse); } else { data = resource_logic_client_fetch_cache_from_cloud (resource, book, chapter, verse); } } else { // The server fetches it from the web, via the http cache. data.append (resource_external_cloud_fetch_cache_extract (resource, book, chapter, verse)); } } else if (isImage) { vector <string> images = database_imageresources.get (resource, book, chapter, verse); for (auto & image : images) { data.append ("<div><img src=\"/resource/imagefetch?name=" + resource + "&image=" + image + "\" alt=\"Image resource\" style=\"width:100%\"></div>"); } } else if (isLexicon) { data = lexicon_logic_get_html (request, resource, book, chapter, verse); } else if (isSword) { data = sword_logic_get_text (sword_source, sword_module, book, chapter, verse); } else { // Nothing found. } // Any font size given in a paragraph style may interfere with the font size setting for the resources // as given in Bibledit. For that reason remove the class name from a paragraph style. for (unsigned int i = 0; i < 5; i++) { string fragment = "p class=\""; size_t pos = data.find (fragment); if (pos != string::npos) { size_t pos2 = data.find ("\"", pos + fragment.length () + 1); if (pos2 != string::npos) { data.erase (pos + 1, pos2 - pos + 1); } } } // NET Bible updates. data = filter_string_str_replace ("<span class=\"s ", "<span class=\"", data); return data; }
string manage_index (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; string page; Assets_Header header = Assets_Header (translate("Manage"), webserver_request); header.addBreadCrumb (menu_logic_settings_menu (), menu_logic_settings_text ()); page = header.run (); Assets_View view; string success; string error; // Force re-index Bibles. if (request->query ["reindex"] == "bibles") { Database_Config_General::setIndexBibles (true); tasks_logic_queue (REINDEXBIBLES, {"1"}); redirect_browser (request, journal_index_url ()); return ""; } // Re-index consultation notes. if (request->query ["reindex"] == "notes") { Database_Config_General::setIndexNotes (true); tasks_logic_queue (REINDEXNOTES); redirect_browser (request, journal_index_url ()); return ""; } // Delete a font. string deletefont = request->query ["deletefont"]; if (!deletefont.empty ()) { string font = filter_url_basename (deletefont); bool font_in_use = false; vector <string> bibles = request->database_bibles ()->getBibles (); for (auto & bible : bibles) { if (font == Fonts_Logic::getTextFont (bible)) font_in_use = true; } if (!font_in_use) { // Only delete a font when it is not in use. Fonts_Logic::erase (font); } else { error = translate("The font could not be deleted because it is in use"); } } // Upload a font. if (request->post.count ("uploadfont")) { string filename = request->post ["filename"]; string path = filter_url_create_root_path ("fonts", filename); string fontdata = request->post ["fontdata"]; filter_url_file_put_contents (path, fontdata); success = translate("The font has been uploaded."); } // Assemble the font block html. vector <string> fonts = Fonts_Logic::getFonts (); vector <string> fontsblock; for (auto & font : fonts) { fontsblock.push_back ("<p>"); #ifndef CLIENT_PREPARED fontsblock.push_back ("<a href=\"?deletefont=" + font+ "\" title=\"" + translate("Delete font") + "\"> ✗ </a>"); #endif fontsblock.push_back (font); fontsblock.push_back ("</p>"); } view.set_variable ("fontsblock", filter_string_implode (fontsblock, "\n")); #ifdef CLIENT_PREPARED view.enable_zone ("client"); view.set_variable ("cloudlink", client_logic_link_to_cloud (manage_index_url (), "")); #else view.enable_zone ("server"); #endif view.set_variable ("success", success); view.set_variable ("error", error); page += view.render ("manage", "index"); page += Assets_Page::footer (); return page; }
string sync_settings (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; Sync_Logic sync_logic = Sync_Logic (webserver_request); // Check on the credentials. if (!sync_logic.credentials_okay ()) return ""; // Client makes a prioritized server call: Record the client's IP address. sync_logic.prioritized_ip_address_record (); // Get the relevant parameters the client POSTed to us, the server. int action = convert_to_int (request->post ["a"]); string value = request->post ["v"]; // The value can be all Bibles, or one Bible. string bible_s = request->post ["b"]; switch (action) { case Sync_Logic::settings_get_total_checksum: { return sync_logic.settings_checksum (filter_string_explode (bible_s, '\n')); } case Sync_Logic::settings_send_workbench_urls: { request->database_config_user()->setWorkbenchURLs (value); return ""; } case Sync_Logic::settings_get_workbench_urls: { return request->database_config_user()->getWorkbenchURLs (); } case Sync_Logic::settings_send_workbench_widths: { request->database_config_user()->setWorkbenchWidths (value); return ""; } case Sync_Logic::settings_get_workbench_widths: { return request->database_config_user()->getWorkbenchWidths (); } case Sync_Logic::settings_send_workbench_heights: { request->database_config_user()->setWorkbenchHeights (value); return ""; } case Sync_Logic::settings_get_workbench_heights: { return request->database_config_user()->getWorkbenchHeights (); } case Sync_Logic::settings_send_resources_organization: { vector <string> resources = filter_string_explode (value, '\n'); request->database_config_user()->setActiveResources (resources); return ""; } case Sync_Logic::settings_get_resources_organization: { vector <string> resources = request->database_config_user()->getActiveResources (); return filter_string_implode (resources, "\n"); } case Sync_Logic::settings_get_bible_id: { return convert_to_string (request->database_bibles()->getID (bible_s)); } case Sync_Logic::settings_get_bible_font: { return Database_Config_Bible::getTextFont (bible_s); } } // Bad request. // Delay a while to obstruct a flood of bad requests. this_thread::sleep_for (chrono::seconds (1)); request->response_code = 400; return ""; }
string search_replacepre2 (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; string siteUrl = Database_Config_General::getSiteURL (); // Get search variables from the query. string searchfor = request->query ["q"]; string replacewith = request->query ["r"]; bool casesensitive = (request->query ["c"] == "true"); string id = request->query ["id"]; bool searchplain = (request->query ["p"] == "true"); // Get the Bible and passage for this identifier. Passage details = Passage::from_text (id); string bible = details.bible; int book = details.book; int chapter = details.chapter; string verse = details.verse; // Get the plain text or the USFM. string text; if (searchplain) { text = search_logic_get_bible_verse_text (bible, book, chapter, convert_to_int (verse)); } else { text = search_logic_get_bible_verse_usfm (bible, book, chapter, convert_to_int (verse)); } // Clickable passage. string link = filter_passage_link_for_opening_editor_at (book, chapter, verse); string oldtext = filter_string_markup_words ({searchfor}, text); string newtext (text); if (casesensitive) { newtext = filter_string_str_replace (searchfor, replacewith, newtext); } else { vector <string> needles = filter_string_search_needles (searchfor, text); for (auto & needle : needles) { newtext = filter_string_str_replace (needle, replacewith, newtext); } } if (replacewith != "") newtext = filter_string_markup_words ({replacewith}, newtext); // The id sent to the browser contains bible identifier, book, chapter, and verse. int bibleID = request->database_bibles()->getID (bible); vector <string> bits = {convert_to_string (bibleID), convert_to_string (book), convert_to_string (chapter), verse}; string s_id = filter_string_implode (bits, "_"); // Check whether the user has write access to the book. string user = request->session_logic ()->currentUser (); bool write = access_bible_book_write (webserver_request, user, bible, book); // Create output. string output; output.append ("<div id=\"" + s_id + "\">\n"); output.append ("<p>"); if (write) output.append ("<a href=\"replace\"> ✔ </a> <a href=\"delete\"> ✗ </a> "); output.append (link); output.append ("</p>\n"); output.append ("<p>" + oldtext + "</p>\n"); output.append ("<p>"); if (write) output.append (newtext); else output.append (locale_logic_text_no_privileges_modify_book ()); output.append ("</p>\n"); output.append ("</div>\n"); // Output to browser. return output; }
string notes_notes (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; Database_Notes database_notes (webserver_request); string bible = access_bible_clamp (webserver_request, request->database_config_user()->getBible()); int book = Ipc_Focus::getBook (webserver_request); int chapter = Ipc_Focus::getChapter (webserver_request); int verse = Ipc_Focus::getVerse (webserver_request); int passage_selector = request->database_config_user()->getConsultationNotesPassageSelector(); int edit_selector = request->database_config_user()->getConsultationNotesEditSelector(); int non_edit_selector = request->database_config_user()->getConsultationNotesNonEditSelector(); string status_selector = request->database_config_user()->getConsultationNotesStatusSelector(); string bible_selector = request->database_config_user()->getConsultationNotesBibleSelector(); string assignment_selector = request->database_config_user()->getConsultationNotesAssignmentSelector(); bool subscription_selector = request->database_config_user()->getConsultationNotesSubscriptionSelector(); int severity_selector = request->database_config_user()->getConsultationNotesSeveritySelector(); int text_selector = request->database_config_user()->getConsultationNotesTextSelector(); string search_text = request->database_config_user()->getConsultationNotesSearchText(); int passage_inclusion_selector = request->database_config_user()->getConsultationNotesPassageInclusionSelector(); int text_inclusion_selector = request->database_config_user()->getConsultationNotesTextInclusionSelector(); // The Bibles the current user has access to. vector <string> bibles = access_bible_bibles (webserver_request, request->session_logic()->currentUser ()); // The admin disables notes selection on Bibles, // so the admin sees all notes, including notes referring to non-existing Bibles. if (request->session_logic ()->currentLevel () == Filter_Roles::admin ()) bibles.clear (); vector <int> identifiers = database_notes.selectNotes (bibles, book, chapter, verse, passage_selector, edit_selector, non_edit_selector, status_selector, bible_selector, assignment_selector, subscription_selector, severity_selector, text_selector, search_text, -1); // In case there aren't too many notes, there's enough time to sort them in passage order. if (identifiers.size () <= 200) { vector <int> passage_sort_keys; for (auto & identifier : identifiers) { int passage_sort_key = 0; vector <float> numeric_passages; vector <Passage> passages = database_notes.getPassages (identifier); for (auto & passage : passages) { numeric_passages.push_back (filter_passage_to_integer (passage)); } if (!numeric_passages.empty ()) { float average = accumulate (numeric_passages.begin (), numeric_passages.end (), 0) / numeric_passages.size (); passage_sort_key = round (average); } passage_sort_keys.push_back (passage_sort_key); } quick_sort (passage_sort_keys, identifiers, 0, identifiers.size ()); } string notesblock; for (auto & identifier : identifiers) { string summary = database_notes.getSummary (identifier); vector <Passage> passages = database_notes.getPassages (identifier); string verses = filter_passage_display_inline (passages); // A simple way to make it easier to see the individual notes in the list, // when the summaries of some notes are long, is to display the passage first. summary.insert (0, verses + " | "); string verse_text; if (passage_inclusion_selector) { vector <Passage> passages = database_notes.getPassages (identifier); for (auto & passage : passages) { string usfm = request->database_bibles()->getChapter (bible, passage.book, passage.chapter); string text = usfm_get_verse_text (usfm, convert_to_int (passage.verse)); if (!verse_text.empty ()) verse_text.append ("<br>"); verse_text.append (text); } } string content; if (text_inclusion_selector) { content = database_notes.getContents (identifier); } notesblock.append ("<a name=\"note" + convert_to_string (identifier) + "\"></a>\n"); notesblock.append ("<p><a href=\"note?id=" + convert_to_string (identifier) + "\">" + summary + "</a></p>\n"); if (!verse_text.empty ()) notesblock.append ("<p>" + verse_text + "</p>\n"); if (!content.empty ()) notesblock.append ("<p>" + content + "</p>\n"); } if (identifiers.empty ()) { return translate("This selection does not display any notes."); } return notesblock; }
// Function to safely store a verse. // It saves the verse if the new USFM does not differ too much from the existing USFM. // On success it returns an empty message. // On failure it returns a message that specifies the reason why it could not be saved. // This function proves useful in cases that the text in the Bible editor gets corrupted // due to human error. // It also is useful in cases where the session is deleted from the server, // where the text in the editors would get corrupted. // It also is useful in view of an unstable connection between browser and server, to prevent data corruption. // It handles combined verses. string usfm_safely_store_verse (void * webserver_request, string bible, int book, int chapter, int verse, string usfm) { Webserver_Request * request = (Webserver_Request *) webserver_request; usfm = filter_string_trim (usfm); // Check that the USFM to be saved is for the correct verse. vector <int> save_verses = usfm_get_verse_numbers (usfm); if ((verse != 0) && !save_verses.empty ()) { save_verses.erase (save_verses.begin()); } if (save_verses.empty ()) { Database_Logs::log ("The USFM contains no verse information: " + usfm); return translate ("Missing verse number"); } if (!in_array (verse, save_verses)) { vector <string> vss; for (auto vs : save_verses) vss.push_back (convert_to_string (vs)); Database_Logs::log ("The USFM contains verse(s) " + filter_string_implode (vss, " ") + " while it wants to save to verse " + convert_to_string (verse) + ": " + usfm); return translate ("Verse mismatch"); } // Get the existing chapter USFM. string chapter_usfm = request->database_bibles()->getChapter (bible, book, chapter); // Get the existing USFM fragment for the verse to save. string existing_verse_usfm = usfm_get_verse_text (chapter_usfm, verse); existing_verse_usfm = filter_string_trim (existing_verse_usfm); // Check that there is a match between the existing verse numbers and the verse numbers to save. vector <int> existing_verses = usfm_get_verse_numbers (existing_verse_usfm); save_verses = usfm_get_verse_numbers (usfm); bool verses_match = true; if (save_verses.size () == existing_verses.size ()) { for (unsigned int i = 0; i < save_verses.size (); i++) { if (save_verses [i] != existing_verses [i]) verses_match = false; } } else { verses_match = false; } if (!verses_match) { vector <string> existing, save; for (auto vs : existing_verses) existing.push_back (convert_to_string (vs)); for (auto vs : save_verses) save.push_back (convert_to_string (vs)); Database_Logs::log ("The USFM contains verse(s) " + filter_string_implode (save, " ") + " which would overwrite a fragment that contains verse(s) " + filter_string_implode (existing, " ") + ": " + usfm); return translate ("Cannot overwrite another verse"); } // Bail out if the new USFM is the same as the existing. if (usfm == existing_verse_usfm) { return ""; } // Check maximum difference between new and existing USFM. string message = usfm_save_is_safe (webserver_request, existing_verse_usfm, usfm, false); if (!message.empty ()) return message; // Store the new verse USFM in the existing chapter USFM. size_t pos = chapter_usfm.find (existing_verse_usfm); size_t length = existing_verse_usfm.length (); chapter_usfm.erase (pos, length); chapter_usfm.insert (pos, usfm); // Safety checks have passed: Save chapter. Bible_Logic::storeChapter (bible, book, chapter, chapter_usfm); // Done: OK. return ""; }
string edit_index (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; bool touch = request->session_logic ()->touchEnabled (); if (request->query.count ("switchbook") && request->query.count ("switchchapter")) { int switchbook = convert_to_int (request->query ["switchbook"]); int switchchapter = convert_to_int (request->query ["switchchapter"]); int switchverse = 1; if (request->query.count ("switchverse")) switchverse = convert_to_int (request->query ["switchverse"]); Ipc_Focus::set (request, switchbook, switchchapter, switchverse); Navigation_Passage::recordHistory (request, switchbook, switchchapter, switchverse); } string page; Assets_Header header = Assets_Header (translate("Edit"), request); header.setNavigator (); header.setEditorStylesheet (); if (touch) header.jQueryTouchOn (); header.notifItOn (); header.addBreadCrumb (menu_logic_translate_menu (), menu_logic_translate_text ()); page = header.run (); if (request->query.count ("changebible")) { string changebible = request->query ["changebible"]; if (changebible == "") { Dialog_List dialog_list = Dialog_List ("index", translate("Select which Bible to open in the editor"), "", ""); 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); } } } Assets_View view; // Active Bible, and check access. string bible = access_bible_clamp (request, request->database_config_user()->getBible ()); if (request->query.count ("bible")) bible = access_bible_clamp (request, request->query ["bible"]); view.set_variable ("bible", bible); // Store the active Bible in the page's javascript. view.set_variable ("navigationCode", Navigation_Passage::code (bible)); int verticalCaretPosition = request->database_config_user ()->getVerticalCaretPosition (); string script = "var editorChapterLoaded = '" + locale_logic_text_loaded () + "';\n" "var editorChapterSaving = '" + locale_logic_text_saving () + "';\n" "var editorChapterSaved = '" + locale_logic_text_saved () + "';\n" "var editorChapterRetrying = '" + locale_logic_text_retrying () + "';\n" "var editorChapterReformat = '" + locale_logic_text_reformat () + "';\n" "var editorWriteAccess = true;\n" "var verticalCaretPosition = " + convert_to_string (verticalCaretPosition) + ";\n" "var unsentBibleDataTimeoutWarning = '" + bible_logic_unsent_unreceived_data_warning (false) + "';\n"; config_logic_swipe_enabled (webserver_request, script); view.set_variable ("script", script); string clss = Filter_Css::getClass (bible); string font = Fonts_Logic::getTextFont (bible); int direction = Database_Config_Bible::getTextDirection (bible); int lineheight = Database_Config_Bible::getLineHeight (bible); int letterspacing = Database_Config_Bible::getLetterSpacing (bible); view.set_variable ("custom_class", clss); view.set_variable ("custom_css", Filter_Css::getCss (clss, Fonts_Logic::getFontPath (font), direction, lineheight, letterspacing)); // In basic mode the editor has no controls and fewer indicators. // In basic mode, the user can just edit text, and cannot style it. bool basic_mode = config_logic_basic_mode (webserver_request); if (!basic_mode) view.enable_zone ("advancedmode"); // Whether to enable fast Bible editor switching. if (!basic_mode && request->database_config_user ()->getFastEditorSwitchingAvailable ()) { view.enable_zone ("fastswitcheditor"); } // Whether to enable the styles button. if (request->database_config_user ()->getEnableStylesButtonVisualEditors ()) { view.enable_zone ("stylesbutton"); } page += view.render ("edit", "index"); page += Assets_Page::footer (); return page; }
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; }
// 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); }
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); } } }
string editone_load (void * webserver_request) { Webserver_Request * request = (Webserver_Request *) webserver_request; string bible = request->query ["bible"]; int book = convert_to_int (request->query ["book"]); int chapter = convert_to_int (request->query ["chapter"]); int verse = convert_to_int (request->query ["verse"]); string part = request->query ["part"]; string stylesheet = request->database_config_user()->getStylesheet (); string chapter_usfm = request->database_bibles()->getChapter (bible, book, chapter); string focused_verse_usfm = usfm_get_verse_text (chapter_usfm, verse); vector <int> verses = usfm_get_verse_numbers (chapter_usfm); int highest_verse = 0; if (!verses.empty ()) highest_verse = verses.back (); string prefix_usfm = usfm_get_verse_range_text (chapter_usfm, 0, verse - 1, focused_verse_usfm); string suffix_usfm = usfm_get_verse_range_text (chapter_usfm, verse + 1, highest_verse, focused_verse_usfm); // Last paragraph style of the prefix: To be used for the starting visual style for the focused verse. string prefix_paragraph_style; string prefix_html; if (!prefix_usfm.empty ()) { Editor_Usfm2Html editor_usfm2html; editor_usfm2html.load (prefix_usfm); editor_usfm2html.stylesheet (stylesheet); editor_usfm2html.run (); prefix_html = editor_usfm2html.get (); prefix_paragraph_style = editor_usfm2html.currentParagraphStyle; } // Last paragraph style of the focused verse: For the starting visual style of the suffix. string focused_verse_paragraph_style; string focused_verse_html; if (!focused_verse_usfm.empty ()) { Editor_Usfm2Html editor_usfm2html; editor_usfm2html.load (focused_verse_usfm); editor_usfm2html.stylesheet (stylesheet); editor_usfm2html.run (); focused_verse_html = editor_usfm2html.get (); focused_verse_html = editone_load_remove_id_notes (focused_verse_html); focused_verse_paragraph_style = editor_usfm2html.currentParagraphStyle; } // If the first paragraph of the focused verse does not have a paragraph style applied, // apply the last paragraph style of the prefix to the first paragraph of the focused verse. // For example, html like this: // <p><span class="v">7</span><span> </span><span>For Yahweh knows the way of the righteous,</span></p><p class="q2"><span>but the way of the wicked shall perish.</span></p> // ... becomes like this: // <p class="q1"><span class="v">7</span><span /><span>For Yahweh knows the way of the righteous,</span></p><p class="q2"><span>but the way of the wicked shall perish.</span></p> string focused_verse_applied_style; if (!focused_verse_html.empty ()) { if (!prefix_paragraph_style.empty ()) { xml_document document; document.load_string (focused_verse_html.c_str(), parse_ws_pcdata_single); xml_node p_node = document.first_child (); string p_style = p_node.attribute ("class").value (); if (p_style.empty ()) { p_node.append_attribute ("class") = prefix_paragraph_style.c_str (); // Send the applied paragraph style to the browser, // for later use when it saves the modified verse text. focused_verse_applied_style = prefix_paragraph_style; } stringstream output; document.print (output, "", format_raw); focused_verse_html = output.str (); } } string suffix_html; if (!suffix_usfm.empty ()) { Editor_Usfm2Html editor_usfm2html; editor_usfm2html.load (suffix_usfm); editor_usfm2html.stylesheet (stylesheet); editor_usfm2html.run (); suffix_html = editor_usfm2html.get (); suffix_html = editone_load_remove_id_notes (suffix_html); } // If the first paragraph of the suffix does not have a paragraph style applied, // apply the last paragraph style of the focused verse to the first paragraph of the suffix. // For example, html like this: // <p><span class="v">7</span><span> </span><span>For Yahweh knows the way of the righteous,</span></p><p class="q2"><span>but the way of the wicked shall perish.</span></p> // ... becomes like this: // <p class="q1"><span class="v">7</span><span /><span>For Yahweh knows the way of the righteous,</span></p><p class="q2"><span>but the way of the wicked shall perish.</span></p> if (!suffix_html.empty ()) { if (!focused_verse_paragraph_style.empty ()) { xml_document document; document.load_string (suffix_html.c_str(), parse_ws_pcdata_single); xml_node p_node = document.first_child (); string p_style = p_node.attribute ("class").value (); if (p_style.empty ()) { p_node.append_attribute ("class") = focused_verse_paragraph_style.c_str (); } stringstream output; document.print (output, "", format_raw); suffix_html = output.str (); } } string data; data.append (focused_verse_applied_style); data.append ("#_be_#"); data.append (prefix_html); data.append ("#_be_#"); data.append (focused_verse_html); data.append ("#_be_#"); data.append (suffix_html); string user = request->session_logic ()->currentUser (); bool write = access_bible_book_write (webserver_request, user, bible, book); data = Checksum_Logic::send (data, write); return data; }
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; }