Example #1
0
// This provides a html link to Bibledit Cloud / $path, and displays $linktext.
string client_logic_link_to_cloud (string path, string linktext)
{
  string url;
  if (client_logic_client_enabled ()) {
    string address = Database_Config_General::getServerAddress ();
    int port = Database_Config_General::getServerPort ();
    url = address + ":" + convert_to_string (port);
    if (!path.empty ()) {
      url.append ("/");
      url.append (path);
    }
  } else {
    // Client disconnected: Provide the link and the text to connect to the Cloud.
    url = "/" + client_index_url ();
    linktext.append (" ");
    linktext.append (translate("You are not yet connected to Bibledit Cloud."));
    linktext.append (" ");
    linktext.append (translate("Connect."));
  }

  if (linktext.empty ()) {
    // Empty link text: Select the link itself as the text to display.
    linktext = url;
  }
  
  string link = "<a href=\"" + url + "\">" + linktext + "</a>";
  return link;
}
Example #2
0
// If $minute is negative, it syncs.
// If $minute >=0, it determines from the settings whether to sync.
void sendreceive_queue_sync (int minute)
{
  // Deal with a numerical minute to find out whether it's time to automatically sync.
  if (minute >= 0) {
    int repeat = Database_Config_General::getRepeatSendReceive ();
    // Sync every hour.
    if (repeat == 1) {
      minute = minute % 60;
      if (minute == 0) minute = -1;
    }
    // Sync every five minutes.
    if (repeat == 2) {
      minute = minute % 5;
      if (minute == 0) minute = -1;
    }
  }
  
  // Send and receive: It is time now, or it is manual.
  // Only queue a sync task if it is not running at the moment.
  if (minute < 0) {
    
    // Send / receive only works in Client mode.
    if (client_logic_client_enabled ()) {
      
      if (tasks_logic_queued (SYNCBIBLES)) {
        Database_Logs::log ("Still synchronizing Bibles");
      } else {
        tasks_logic_queue (SYNCBIBLES);
      }
      if (tasks_logic_queued (SYNCNOTES)) {
        Database_Logs::log ("Still synchronizing Notes");
      } else {
        tasks_logic_queue (SYNCNOTES);
      }
      if (tasks_logic_queued (SYNCSETTINGS)) {
        Database_Logs::log ("Still synchronizing Settings");
      } else {
        tasks_logic_queue (SYNCSETTINGS);
      }
      if (tasks_logic_queued (SYNCCHANGES)) {
        Database_Logs::log ("Still synchronizing Changes");
      } else {
        tasks_logic_queue (SYNCCHANGES);
      }
      if (tasks_logic_queued (SYNCFILES)) {
        Database_Logs::log ("Still synchronizing Files");
      } else {
        tasks_logic_queue (SYNCFILES);
      }
      // Sync resources always, because it checks on its own whether to do something.
      tasks_logic_queue (SYNCRESOURCES);
    }
    
    // Store the most recent time that the sync action ran.
    Database_Config_General::setLastSendReceive (filter_date_seconds_since_epoch ());
  }
}
Example #3
0
bool journal_index_acl (void * webserver_request)
{
  // In Client mode, anyone can view the journal.
  if (client_logic_client_enabled ()) {
    return true;
  }
  // The role of Consultant or higher can view the journal.
  if (Filter_Roles::access_control (webserver_request, Filter_Roles::consultant ())) {
    return true;
  }
  // No access.
  return false;
}
Example #4
0
// Sets the bible for note identifier.
void Notes_Logic::setBible (int identifier, const string& bible)
{
  Database_Notes database_notes (webserver_request);
  database_notes.setBible (identifier, bible);
  if (client_logic_client_enabled ()) {
    // Client: record the action in the database.
    string user = ((Webserver_Request *) webserver_request)->session_logic ()->currentUser ();
    Database_NoteActions database_noteactions = Database_NoteActions ();
    database_noteactions.record (user, identifier, Sync_Logic::notes_put_bible, "");
  } else {
    // Server: do nothing extra.
  }
}
Example #5
0
void Notes_Logic::unmarkForDeletion (int identifier)
{
  Database_Notes database_notes (webserver_request);
  database_notes.unmarkForDeletion (identifier);
  if (client_logic_client_enabled ()) {
    // Client: record the action in the database.
    string user = ((Webserver_Request *) webserver_request)->session_logic ()->currentUser ();
    Database_NoteActions database_noteactions = Database_NoteActions ();
    database_noteactions.record (user, identifier, Sync_Logic::notes_put_unmark_delete, "");
  } else {
    // Server: do nothing extra.
  }
}
Example #6
0
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;
}
Example #7
0
void Notes_Logic::assignUser (int identifier, const string& user)
{
  Database_Notes database_notes (webserver_request);
  if (client_logic_client_enabled ()) {
    // Client: record the action in the database.
    string myuser = ((Webserver_Request *) webserver_request)->session_logic ()->currentUser ();
    Database_NoteActions database_noteactions;
    database_noteactions.record (myuser, identifier, Sync_Logic::notes_put_assign, user);
  } else {
    // Server: do the notifications.
    // Assign logic comes before the database action in this particular case.
    handlerAssignNote (identifier, user);
  }
  database_notes.assignUser (identifier, user);
}
Example #8
0
void Notes_Logic::erase (int identifier)
{
  Database_Notes database_notes (webserver_request);
  if (client_logic_client_enabled ()) {
    // Client: record the action in the database.
    string user = ((Webserver_Request *) webserver_request)->session_logic ()->currentUser ();
    Database_NoteActions database_noteactions = Database_NoteActions ();
    database_noteactions.record (user, identifier, Sync_Logic::notes_put_delete, "");
  } else {
    // Server: notification.
    handlerDeleteNote (identifier);
  }
  trash_consultation_note (webserver_request, identifier);
  database_notes.erase (identifier);
}
Example #9
0
void Notes_Logic::handlerAssignNote (int identifier, const string& user)
{
  // Take no action in client mode.
  if (client_logic_client_enabled ()) return;

  Database_Config_User database_config_user = Database_Config_User (webserver_request);
  if (database_config_user.getUserAssignedConsultationNoteNotification (user)) {
    // Only email the user if the user was not yet assigned this note.
    Database_Notes database_notes (webserver_request);
    vector <string> assignees = database_notes.getAssignees (identifier);
    if (find (assignees.begin(), assignees.end(), user) == assignees.end()) {
      emailUsers (identifier, translate("Assigned"), {user}, false);
    }
  }
}
Example #10
0
// Add a comment to an exiting note identified by identifier.
void Notes_Logic::addComment (int identifier, const string& comment)
{
  // Do nothing if there was no content.
  if (comment == "") return;

  Database_Notes database_notes (webserver_request);
  database_notes.addComment (identifier, comment);
  if (client_logic_client_enabled ()) {
    // Client: record the action in the database.
    string user = ((Webserver_Request *) webserver_request)->session_logic ()->currentUser ();
    Database_NoteActions database_noteactions = Database_NoteActions ();
    database_noteactions.record (user, identifier, Sync_Logic::notes_put_comment, comment);
  } else {
    // Server: do the notifications.
    handlerAddComment (identifier);
  }
}
Example #11
0
// Returns a warning in case the client is connected to the open demo server.
string demo_client_warning ()
{
  string warning;
  if (client_logic_client_enabled ()) {
    string address = Database_Config_General::getServerAddress ();
    if (address == demo_address ()) {
      int port = Database_Config_General::getServerPort ();
      if (port == demo_port ()) {
        warning.append (translate("You are connected to a public demo of Bibledit Cloud."));
        warning.append (" ");
        warning.append (translate("Everybody can modify the data on that server."));
        warning.append (" ");
        warning.append (translate("After send and receive your data will reflect the data on the server."));
      }
    }
  }
  return warning;
}
Example #12
0
// The client runs this function to fetch a general resource $name from the Cloud,
// or from its local cache,
// and to update the local cache with the fetched content, if needed,
// and to return the requested content.
string resource_logic_client_fetch_cache_from_cloud (string resource, int book, int chapter, int verse)
{
  // Ensure that the cache for this resource exists on the client.
  if (!Database_Cache::exists (resource, book)) {
    Database_Cache::create (resource, book);
  }
  
  // If the content exists in the cache, return that content.
  if (Database_Cache::exists (resource, book, chapter, verse)) {
    return Database_Cache::retrieve (resource, book, chapter, verse);
  }
  
  // Fetch this resource from Bibledit Cloud or from the cache.
  string address = Database_Config_General::getServerAddress ();
  int port = Database_Config_General::getServerPort ();
  if (!client_logic_client_enabled ()) {
    // If the client has not been connected to a cloud instance,
    // fetch the resource from the Bibledit Cloud demo.
    address = demo_address ();
    port = demo_port ();
  }
  
  string url = client_logic_url (address, port, sync_resources_url ());
  url = filter_url_build_http_query (url, "r", filter_url_urlencode (resource));
  url = filter_url_build_http_query (url, "b", convert_to_string (book));
  url = filter_url_build_http_query (url, "c", convert_to_string (chapter));
  url = filter_url_build_http_query (url, "v", convert_to_string (verse));
  string error;
  string content = filter_url_http_get (url, error);
  
  if (error.empty ()) {
    // No error: Cache content.
    Database_Cache::cache (resource, book, chapter, verse, content);
  } else {
    // Error: Log it, and return it.
    Database_Logs::log (resource + ": " + error);
    content.append (error);
  }

  // Done.
  return content;
}
Example #13
0
// Deal with AJAX call for a possible new journal entry.
string journal_index_ajax (Webserver_Request * request, string filename)
{
  int userLevel = request->session_logic()->currentLevel ();
  // Sample filetime: "141708283400041520".
  // It is the number of seconds past the Unix epoch, plus the microseconds within the current second.
  Database_Logs database_logs = Database_Logs ();
  string result = database_logs.getNext (filename);
  if (!result.empty()) {
    int entryLevel = convert_to_int (result);
    // Cloud: Pay attention to only rendering journal entries of sufficient user level.
    // Client: Render any journal entry.
    if ((entryLevel <= userLevel) || client_logic_client_enabled ()) {
      result = render_journal_entry (result);
    } else {
      result.clear ();
    }
    result.insert (0, filename + "\n");
  }
  return result;
}
Example #14
0
// Create a consultation note.
// $bible: The notes's Bible.
// $book, $chapter, $verse: The note's passage.
// $summary: The note's summary.
// $contents: The note's contents.
// $raw: Import $contents as it is.
// It returns the $identifier of this new note.
int Notes_Logic::createNote (string bible, int book, int chapter, int verse, string summary, string contents, bool raw)
{
  summary = filter_string_str_replace ("\n", "", summary);
  Database_Notes database_notes (webserver_request);
  int note_id = database_notes.storeNewNote (bible, book, chapter, verse, summary, contents, raw);
  if (client_logic_client_enabled ()) {
    // Client: record the action in the database.
    Database_NoteActions database_noteactions;
    Webserver_Request * request = (Webserver_Request *) webserver_request;
    database_noteactions.record (request->session_logic()->currentUser (), note_id, Sync_Logic::notes_put_create_initiate, "");
    database_noteactions.record (request->session_logic()->currentUser (), note_id, Sync_Logic::notes_put_summary, "");
    // The contents to submit to the server, take it from the database, as it was updated in the logic above.
    database_noteactions.record (request->session_logic()->currentUser (), note_id, Sync_Logic::notes_put_contents, database_notes.getContents (note_id));
    database_noteactions.record (request->session_logic()->currentUser (), note_id, Sync_Logic::notes_put_bible, "");
    database_noteactions.record (request->session_logic()->currentUser (), note_id, Sync_Logic::notes_put_passages, "");
    database_noteactions.record (request->session_logic()->currentUser (), note_id, Sync_Logic::notes_put_create_complete, "");
  } else {
    // Server: do the notifications.
    handlerNewNote (note_id);
  }
  return note_id;
}
Example #15
0
// This handles email to users.
// identifier: the note that is being handled.
// label: prefix to the subject line of the email.
// users: array of users to be mailed.
// postpone: whether to postpone sending the email till the evening.
void Notes_Logic::emailUsers (int identifier, const string& label, const vector <string> & users, bool postpone)
{
  // Databases.
  Database_Notes database_notes (webserver_request);
  Database_Mail database_mail = Database_Mail(webserver_request);

  // Send mail to all users.
  string summary = database_notes.getSummary (identifier);
  string passages = filter_passage_display_inline (database_notes.getPassages (identifier));
  string contents = database_notes.getContents (identifier);

  // Include links to the Cloud: One to the note, and one to the active desktop.
  contents.append ("<br>\n");
  contents.append ("<p>");
  contents.append ("<a href=\"");
  string notelink = Database_Config_General::getSiteURL () + notes_note_url () + "?id=" + convert_to_string (identifier);
  contents.append (notelink);
  contents.append ("\">");
  contents.append (translate ("View or respond online"));
  contents.append ("</a>");
  contents.append (" " + translate ("or") + " ");

  contents.append ("<a href=\"");
  string desktoplink = Database_Config_General::getSiteURL () + workbench_index_url () + "?note=" + convert_to_string (identifier);
  contents.append (desktoplink);
  contents.append ("\">");
  contents.append (translate ("open the desktop online"));
  contents.append ("</a>");

  contents.append ("</p>\n");
  string mailto = "mailto:" + Database_Config_General::getSiteMailAddress () + "?subject=(CNID" + convert_to_string (identifier) + ")";
  contents.append ("<p><a href=\"");
  contents.append (mailto);
  contents.append ("\">Respond by email</a></p>\n");

  // Deal with possible postponing email till 9 PM.
  int timestamp = filter_date_seconds_since_epoch ();
  if (postpone) {
    int localseconds = filter_date_local_seconds (timestamp);
    float localhour = filter_date_numerical_hour (localseconds) + (float) filter_date_numerical_minute (localseconds) / 60;
    if (localhour < 21) {
      float difference = 21 - localhour;
      timestamp += (3600 * difference) - 10;
    }
  }

  // Send (but not in client mode).
  for (auto & user : users) {
    if (!client_logic_client_enabled ()) {
      string subject = label;
      subject.append (" | ");
      subject.append (passages);
      subject.append (" | ");
      subject.append (summary);
      subject.append (" | (CNID");
      subject.append (convert_to_string (identifier));
      subject.append (")");
      database_mail.send (user, subject, contents, timestamp);
    }
  }
}
Example #16
0
string sword_logic_get_text (string source, string module, int book, int chapter, int verse)
{
#ifdef HAVE_CLIENT

  // Client checks for and optionally creates the cache for this SWORD module.
  if (!Database_Cache::exists (module, book)) {
    Database_Cache::create (module, book);
  }

  // If this module/passage exists in the cache, return it (it updates the access days in the cache).
  if (Database_Cache::exists (module, book, chapter, verse)) {
    return Database_Cache::retrieve (module, book, chapter, verse);
  }

  // Fetch this SWORD resource from the server.
  string address = Database_Config_General::getServerAddress ();
  int port = Database_Config_General::getServerPort ();
  if (!client_logic_client_enabled ()) {
    // If the client has not been connected to a cloud instance,
    // fetch the SWORD content from the Bibledit Cloud demo.
    address = demo_address ();
    port = demo_port ();
  }
  string url = client_logic_url (address, port, sync_resources_url ());
  string resource = "[" + source + "][" + module + "]";
  url = filter_url_build_http_query (url, "r", resource);
  url = filter_url_build_http_query (url, "b", convert_to_string (book));
  url = filter_url_build_http_query (url, "c", convert_to_string (chapter));
  url = filter_url_build_http_query (url, "v", convert_to_string (verse));
  string error;
  string html = filter_url_http_get (url, error, true);
  
  // In case of an error, don't cache that error, but let the user see it.
  if (!error.empty ()) return error;

  // Client caches this info for later.
  // Except in case of predefined responses from the Cloud.
  if (html != sword_logic_installing_module_text ()) {
    if (html != sword_logic_fetch_failure_text ()) {
      Database_Cache::cache (module, book, chapter, verse, html);
    }
  }
  
  return html;
  
#else

  string module_text;
  bool module_available = false;

  string osis = Database_Books::getOsisFromId (book);
  string chapter_verse = convert_to_string (chapter) + ":" + convert_to_string (verse);

  // See notes on function sword_logic_diatheke
  // for why it is not currently fetching content via a SWORD library call.
  // module_text = sword_logic_diatheke (module, osis, chapter, verse, module_available);
  
  // Running diatheke only works when it runs in the SWORD installation directory.
  string sword_path = sword_logic_get_path ();
  // Running several instances of diatheke simultaneously fails.
  sword_logic_diatheke_run_mutex.lock ();
  // The server fetches the module text as follows:
  // diatheke -b KJV -k Jn 3:16
  int result = filter_shell_vfork (module_text, sword_path, "diatheke", "-b", module.c_str(), "-k", osis.c_str(), chapter_verse.c_str());
  sword_logic_diatheke_run_mutex.unlock ();
  if (result != 0) return sword_logic_fetch_failure_text ();
  
  // Touch the cache so the server knows that the module has been accessed just now.
  string url = sword_logic_virtual_url (module, 0, 0, 0);
  database_filebased_cache_get (url);

  // If the module has not been installed, the output of "diatheke" will be empty.
  // If the module was installed, but the requested passage is out of range,
  // the output of "diatheke" contains the module name, so it won't be empty.
  module_available = !module_text.empty ();
  
  if (!module_available) {
    
    // Check whether the SWORD module exists.
    vector <string> modules = sword_logic_get_available ();
    string smodules = filter_string_implode (modules, "");
    if (smodules.find ("[" + module + "]") != string::npos) {
      // Schedule SWORD module installation.
      // (It used to be the case that this function, to get the text,
      // would wait till the SWORD module was installed, and then after installation,
      // return the text from that module.
      // But due to long waiting on Bibledit demo, while it would install multiple modules,
      // the Bibledit demo would become unresponsive.
      // So, it's better to return immediately with an informative text.)
      sword_logic_install_module_schedule (source, module);
      // Return standard 'installing' information. Client knows not to cache this.
      return sword_logic_installing_module_text ();
    } else {
      return "Cannot find SWORD module " + module;
    }
  }
  
  // Remove any OSIS elements.
  filter_string_replace_between (module_text, "<", ">", "");
  
  // Remove the passage name that diatheke adds.
  // A reliable signature for this is the chapter and verse plus subsequent colon.
  size_t pos = module_text.find (" " + chapter_verse + ":");
  if (pos != string::npos) {
    pos += 2;
    pos += chapter_verse.size ();
    module_text.erase (0, pos);
  }
  
  // Remove the module name that diatheke adds.
  module_text = filter_string_str_replace ("(" + module + ")", "", module_text);
  
  // Clean whitespace away.
  module_text = filter_string_trim (module_text);
  
  return module_text;

#endif

  return "";
}
Example #17
0
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;
}
Example #18
0
// This handles notifications for the users
// identifier: the note that is being handled.
// notification: the type of action on the consultation note.
void Notes_Logic::notifyUsers (int identifier, int notification)
{
  // Take no action in client mode.
  if (client_logic_client_enabled ()) return;

  // Data objects.
  Webserver_Request * request = (Webserver_Request *) webserver_request;
  Database_Notes database_notes (webserver_request);
  
  // This note's Bible.
  string bible = database_notes.getBible (identifier);

  // Subscription and assignment not to be used for notes marked for deletion,
  // because marking notes for deletion is nearly the same as deleting them straightaway.
  if (notification != notifyMarkNoteForDeletion) {

    // Whether current user gets subscribed to the note.
    if (request->database_config_user ()->getSubscribeToConsultationNotesEditedByMe ()) {
      database_notes.subscribe (identifier);
    }

    // Users to get subscribed to the note, or to whom the note is to be assigned.
    vector <string> users = request->database_users ()->getUsers ();
    for (const string & user : users) {
      if (access_bible_read (webserver_request, bible, user)) {
        if (request->database_config_user ()->getNotifyUserOfAnyConsultationNotesEdits (user)) {
          database_notes.subscribeUser (identifier, user);
        }
        if (request->database_config_user ()->getUserAssignedToConsultationNotesChanges (user)) {
          database_notes.assignUser (identifier, user);
        }
      }
    }
  }

  // The recipients who receive a notification by email.
  vector <string> recipients;

  // Subscribers who receive email.
  vector <string> subscribers = database_notes.getSubscribers (identifier);
  for (const string & subscriber : subscribers) {
    if (request->database_config_user ()->getUserSubscribedConsultationNoteNotification (subscriber)) {
      recipients.push_back (subscriber);
    }
  }

  // Assignees who receive email.
  vector <string> assignees = database_notes.getAssignees (identifier);
  for (const string & assignee : assignees) {
    if (request->database_config_user ()->getUserAssignedConsultationNoteNotification (assignee)) {
      recipients.push_back (assignee);
    }
  }

  // In case the consultation note is deleted or marked for deletion,
  // notify only the users with this specific notification set.
  if ((notification == notifyNoteDelete) || (notification == notifyMarkNoteForDeletion)) {
    recipients.clear ();
    vector <string> users = request->database_users ()->getUsers ();
    for (const auto & user : users) {
      if (request->database_config_user ()->getUserDeletedConsultationNoteNotification (user)) {
        if (access_bible_read (webserver_request, bible, user)) {
          recipients.push_back (user);
        }
      }
    }
  }

  // Remove duplicates from the recipients.
  set <string> unique (recipients.begin (), recipients.end ());
  recipients.assign (unique.begin (), unique.end());

  // Deal with suppressing mail to the user when he made the update himself.
  string username = request->session_logic ()->currentUser ();
  if (request->database_config_user ()->getUserSuppressMailFromYourUpdatesNotes (username)) {
    recipients.erase (remove (recipients.begin(), recipients.end(), username), recipients.end());
  }

  // Generate the label prefixed to the subject line of the email.
  string label = translate("General");
  switch (notification) {
    case notifyNoteNew             : label = translate("New");                 break;
    case notifyNoteComment         : label = translate("Comment");             break;
    case notifyNoteDelete          : label = translate("Deleted");             break;
    case notifyMarkNoteForDeletion : label = translate("Marked for deletion"); break;
  }

  // Optional postponing sending email.
  bool postpone = false;
  if (notification == notifyNoteNew) {
    if (request->database_config_user ()->getPostponeNewNotesMails ()) {
      postpone = true;
    }
  }

  // Send mail to all recipients.
  emailUsers (identifier, label, recipients, postpone);
}
Example #19
0
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;
}
Example #20
0
void sendreceive_resources ()
{
  if (sendreceive_resources_watchdog) {
    int time = filter_date_seconds_since_epoch ();
    if (time < (sendreceive_resources_watchdog + 900)) {
      return;
    }
    Database_Logs::log ("Resources: " + translate("Watchdog timeout"), Filter_Roles::translator ());
    sendreceive_resources_done ();
  }

  // If any of the prioritized synchronization tasks run, postpone the current task and do not start it.
  if (sendreceive_logic_prioritized_task_is_active ()) {
    sendreceive_resources_done ();
    this_thread::sleep_for (chrono::seconds (5));
    tasks_logic_queue (SYNCRESOURCES);
    return;
  }

  sendreceive_resources_interrupt = false;

  // If there's nothing to cache, bail out.
  vector <string> resources = Database_Config_General::getResourcesToCache ();
  if (resources.empty ()) return;
  
  sendreceive_resources_kick_watchdog ();

  // Error counter.
  int error_count = 0;
  
  // Resource to cache.
  string resource = resources [0];
  
  // Erase the two older storage locations that were used to cache resources in earlier versions of Bibledit.
  {
    Database_OfflineResources database_offlineresources;
    Database_UsfmResources database_usfmresources;
    database_offlineresources.erase (resource);
    database_usfmresources.deleteResource (resource);
  }

  Database_Logs::log ("Starting to install resource:" " " + resource, Filter_Roles::consultant ());
  
  Database_Versifications database_versifications;
  
  vector <int> books = database_versifications.getMaximumBooks ();
  for (auto & book : books) {
    sendreceive_resources_delay_during_prioritized_tasks ();
    if (sendreceive_resources_interrupt) continue;
    
    // Database layout is per book: Create a database for this book.
    Database_Cache::create (resource, book);

    // Last downloaded passage in a previous download operation.
    int last_downloaded_passage = 0;
    {
      pair <int, int> progress = Database_Cache::progress (resource, book);
      int chapter = progress.first;
      int verse = progress.second;
      Passage passage ("", book, chapter, convert_to_string (verse));
      last_downloaded_passage = filter_passage_to_integer (passage);
    }
    
    // List of passages recorded in the database that had errors on a previous download operation.
    vector <int> previous_errors;
    {
      vector <pair <int, int> > errors = Database_Cache::errors (resource, book);
      for (auto & element : errors) {
        int chapter = element.first;
        int verse = element.second;
        Passage passage ("", book, chapter, convert_to_string (verse));
        int numeric_error = filter_passage_to_integer (passage);
        previous_errors.push_back (numeric_error);
      }
    }
    
    string bookName = Database_Books::getEnglishFromId (book);
    
    vector <int> chapters = database_versifications.getMaximumChapters (book);
    for (auto & chapter : chapters) {
      sendreceive_resources_delay_during_prioritized_tasks ();
      if (sendreceive_resources_interrupt) continue;
      bool downloaded = false;
      string message = resource + ": " + bookName + " chapter " + convert_to_string (chapter);
      vector <int> verses = database_versifications.getMaximumVerses (book, chapter);
      for (auto & verse : verses) {
        sendreceive_resources_delay_during_prioritized_tasks ();
        if (sendreceive_resources_interrupt) continue;
        // Numeric representation of passage to deal with.
        Passage passage ("", book, chapter, convert_to_string (verse));
        int numeric_passage = filter_passage_to_integer (passage);
        // Conditions to download this verse:
        // 1. The passage is past the last downloaded passage.
        bool download_verse_past = numeric_passage > last_downloaded_passage;
        // 2. The passage was recorded as an error in a previous download operation.
        bool download_verse_error = in_array (numeric_passage, previous_errors);
        // Whether to download the verse.
        if (download_verse_past || download_verse_error) {
          // Fetch the text for the passage.
          bool server_is_installing_module = false;
          int wait_iterations = 0;
          string html, error;
          do {
            // Fetch this resource from the server.
            string address = Database_Config_General::getServerAddress ();
            int port = Database_Config_General::getServerPort ();
            // If the client has not been connected to a cloud instance,
            // fetch the resource from the Bibledit Cloud demo.
            if (!client_logic_client_enabled ()) {
              address = demo_address ();
              port = demo_port ();
            }
            string url = client_logic_url (address, port, sync_resources_url ());
            url = filter_url_build_http_query (url, "r", filter_url_urlencode (resource));
            url = filter_url_build_http_query (url, "b", convert_to_string (book));
            url = filter_url_build_http_query (url, "c", convert_to_string (chapter));
            url = filter_url_build_http_query (url, "v", convert_to_string (verse));
            error.clear ();
            html = filter_url_http_get (url, error);
            server_is_installing_module = (html == sword_logic_installing_module_text ());
            if (server_is_installing_module) {
              Database_Logs::log ("Waiting while Bibledit Cloud installs the requested SWORD module");
              this_thread::sleep_for (chrono::seconds (60));
              wait_iterations++;
            }
          } while (server_is_installing_module && (wait_iterations < 5));
          // Record the passage as having been done in case it was a regular download,
          // rather than one to retry a previous download error.
          if (download_verse_past) Database_Cache::progress (resource, book, chapter, verse);
          // Clear the registered error in case the verse download corrects it.
          if (download_verse_error) Database_Cache::error (resource, book, chapter, verse, false);
          if (error.empty ()) {
            // Cache the verse data.
            if (!Database_Cache::exists (resource, book)) Database_Cache::create (resource, book);
            Database_Cache::cache (resource, book, chapter, verse, html);
          } else {
            // Record an error.
            Database_Cache::error (resource, book, chapter, verse, true);
            if (message.find (error) == string::npos) {
              message.append ("; " + error);
            }
            error_count++;
            this_thread::sleep_for (chrono::seconds (1));
          }
          downloaded = true;
        }
        sendreceive_resources_kick_watchdog ();
      }
      message += "; done";
      if (downloaded) Database_Logs::log (message, Filter_Roles::manager ());
    }
  }
  
  // Done.
  if (error_count) {
    string msg = "Error count while downloading resource: " + convert_to_string (error_count);
    Database_Logs::log (msg, Filter_Roles::consultant ());
  }
  Database_Logs::log ("Completed installing resource:" " " + resource, Filter_Roles::consultant ());
  // In case of too many errors, schedule the resource download again.
  bool re_schedule_download = false;
  if (error_count) {
    if (!sendreceive_resources_interrupt) {
      re_schedule_download = true;
      Database_Logs::log ("Errors: Re-scheduling resource installation", Filter_Roles::consultant ());
    }
  }
  // Store new download schedule.
  resources = Database_Config_General::getResourcesToCache ();
  resources = filter_string_array_diff (resources, {resource});
  if (re_schedule_download) {
    resources.push_back (resource);
  }
  Database_Config_General::setResourcesToCache (resources);

  sendreceive_resources_done ();
  
  sendreceive_resources_interrupt = false;

  // If there's another resource waiting to be cached, schedule it for caching.
  if (!resources.empty ()) tasks_logic_queue (SYNCRESOURCES);
}
Example #21
0
string journal_index (void * webserver_request)
{
  Webserver_Request * request = (Webserver_Request *) webserver_request;
  int userLevel = request->session_logic()->currentLevel ();

  string filename = request->query ["filename"];
  if (!filename.empty ()) {
    return journal_index_ajax (request, filename);
  }

  Database_Logs database_logs = Database_Logs ();

  
  Assets_Header header = Assets_Header (translate ("Journal"), webserver_request);
  header.addBreadCrumb (menu_logic_tools_menu (), menu_logic_tools_text ());
  string page = header.run ();


  Assets_View view;


  if (request->query.count ("clear")) {
    database_logs.clear ();
    // If the logbook has been cleared on a mobile device, and the screen shuts off,
    // and then the user activates the screen on the mobile device,
    // the logbook will then again be cleared, because that was the last opened URL.
    // Redirecting the browser to a clean URL fixes this behaviour.
    redirect_browser (request, journal_index_url ());
    return "";
  }

  
  string lastfilename;
  vector <string> entries = database_logs.get (lastfilename);


  string lines;
  for (auto entry : entries) {
    int entryLevel = convert_to_int (entry);
    // Cloud: Pay attention to only rendering journal entries of sufficient user level.
    // Client: Render any journal entry.
    if (!client_logic_client_enabled ()) {
      if (entryLevel > userLevel) continue;
    }
    entry = render_journal_entry (entry);
    lines.append ("<p>");
    lines.append (entry);
    lines.append ("</p>\n");
  }
  view.set_variable ("lines", lines);

  
  // Pass the filename of the most recent entry to javascript
  // for use by the AJAX calls for getting subsequent journal entries.
  // It should be passed as a String object in JavaScript.
  // Because when it were passed as an Int, JavaScript would round the value off.
  // And rounding it off often led to double journal entries.
  string script = "var filename = \"" + lastfilename + "\";";
  view.set_variable ("script", script);


  page += view.render ("journal", "index");

  page += Assets_Page::footer ();

  return page;
}
Example #22
0
string client_index (void * webserver_request)
{
  Webserver_Request * request = (Webserver_Request *) webserver_request;
  
  Assets_View view;
  
  if (request->query.count ("disable")) {
    client_logic_enable_client (false);
    client_index_remove_all_users (request);
    Database_Config_General::setRepeatSendReceive (0);
    Database_Config_General::setUnsentBibleDataTime (0);
    Database_Config_General::setUnreceivedBibleDataTime (0);
  }
  
  bool connect = request->post.count ("connect");
  bool demo = request->query.count ("demo");
  if (connect || demo) {
    
    string address;
    if (connect) address = request->post ["address"];
    if (demo) address = demo_address ();
    if (address.find ("http") == string::npos) address = filter_url_set_scheme (address, false);
    Database_Config_General::setServerAddress (address);
    
    int port = convert_to_int (config_logic_http_network_port ());
    if (connect) port = convert_to_int (request->post ["port"]);
    if (demo) port = demo_port ();
    Database_Config_General::setServerPort (port);
    
    string user;
    if (connect) user = request->post ["user"];
    if (demo) user = session_admin_credentials ();
    
    string pass;
    if (connect) pass = request->post ["pass"];
    if (demo) pass = session_admin_credentials (); 

    string response = client_logic_connection_setup (user, md5 (pass));
    int iresponse = convert_to_int (response);

    if ((iresponse >= Filter_Roles::guest ()) && (iresponse <= Filter_Roles::admin ())) {
      // Enable client mode upon a successful connection.
      client_index_enable_client (request, user, pass, iresponse);
      // Feedback.
      view.set_variable ("success", translate("Connection is okay."));
    } else {
      view.set_variable ("error", translate ("Could not create a connection with Bibledit Cloud") + ": " + response);
    }
  }

  if (client_logic_client_enabled ())
    view.enable_zone ("clienton");
  else
    view.enable_zone ("clientoff");
  
  string address = Database_Config_General::getServerAddress ();
  view.set_variable ("address", address);
  
  int port = Database_Config_General::getServerPort ();
  view.set_variable ("port", convert_to_string (port));
  
  view.set_variable ("url", client_logic_link_to_cloud ("", ""));
  
  vector <string> users = request->database_users ()->getUsers ();
  for (auto & user : users) {
    int level = request->database_users()->get_level (user);
    view.set_variable ("role", Filter_Roles::text (level));
  }
  
  view.set_variable ("demo", demo_client_warning ());

  string page;

  // Since the role of the user may change after a successful connection to the server,
  // the menu generation in the header should be postponed till when the actual role is known.
  page = Assets_Page::header (translate ("Server"), webserver_request);
  
  page += view.render ("client", "index");
  
  page += Assets_Page::footer ();
  
  return page;
}
Example #23
0
// 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");
      }
    }
  }
}
Example #24
0
void timer_index ()
{
  bool client = client_logic_client_enabled ();
  int previous_second = -1;
  int previous_minute = -1;
  int previous_fraction = -1;
  while (config_globals_running) {

    try {

      // Wait shortly.
      this_thread::sleep_for (chrono::milliseconds (100));
      
      // Wait tilll the data structures have been initialized.
      if (!config_globals_data_initialized) continue;
      
      // The current time, localized.
      int seconds_since_epoch = filter_date_seconds_since_epoch ();
      int local_seconds = filter_date_local_seconds (seconds_since_epoch);
      int second = filter_date_numerical_second (local_seconds);
      int minute = filter_date_numerical_minute (local_seconds);
      int hour = filter_date_numerical_hour (local_seconds);
      int weekday = filter_date_numerical_week_day (local_seconds);
      
      // Run once per second.
      if (second == previous_second) continue;
      previous_second = second;

      // Every second: Deal with queued and/or active tasks.
      tasks_run_check ();
      
      // Run the part below every so many seconds.
      int fraction = second / 5;
      if (fraction != previous_fraction) {
        previous_fraction = fraction;
      }
      
      // Run the part below once per minute.
      if (minute == previous_minute) continue;
      previous_minute = minute;
      
      // Every minute send out queued email, except in client mode.
      if (!client) tasks_logic_queue (SENDEMAIL);

      // Check for new mail every five minutes.
      // Do not check more often with gmail else the account may be shut down.
      if ((!client) && ((minute % 5) == 0)) {
        tasks_logic_queue (RECEIVEEMAIL);
      }

      // At the nineth minute after every full hour rotate the journal.
      // The nine is chosen, because the journal rotation will summarize the send/receive messages.
      // In case send/receive happens every five minutes, it is expected that under normal circumstances
      // the whole process of sending/receivning will be over, so summarization can then be done.
      if (minute == 9) tasks_logic_queue (ROTATEJOURNAL);
      
      // Client sends/receives Bibles and Consultation.
      sendreceive_queue_sync (minute);
      
      // Sending and receiving Bibles to and from the git repository.
      // On a production website running on an inexpensive virtual private server
      // with 512 Mbyte of memory and a fast network connection,
      // sending and receiving two Bibles takes more than 15 minutes when there are many changes.
      bool sendreceive = ((hour == 0) && (minute == 5));
      bool repeat = ((minute % 5) == 0);
      if (sendreceive || repeat) {
        sendreceive_queue_all (sendreceive);
      }
      
      // Deal with the changes in the Bible made per user.
      // Deal with notifications for the daily changes in the Bibles.
      // This takes a few minutes on a production machine with two Bibles and changes in several chapters.
      // It runs in a server configuration, not on a client.
      if (!client) {
        if ((hour == 0) && (minute == 20)) {
          changes_logic_start ();
        }
      }

      // Run the checks on the Bibles.
      // This takes 15 minutes on a production machine with two Bibles.
      if (!client) {
        if ((hour == 0) && (minute == 30)) {
          checks_logic_start_all ();
        }
      }
      
      // Database maintenance and trimming.
      // It takes a few minutes on a production machine.
      if ((hour == 0) && (minute == 50)) {
        tasks_logic_queue (MAINTAINDATABASE);
      }
      
      // Export the Bibles to the various output formats.
      // This may take an hour on a production machine.
      // This hour was in PHP. In C++ it is much faster.
      if (!client) {
        if ((hour == 1) && (minute == 10)) {
          Export_Logic::scheduleAll ();
        }
      }
      
      // Delete temporal files older than a few days.
      if ((hour == 2) && (minute == 0)) {
        tasks_logic_queue (CLEANTMPFILES);
      }
      
      // Re-index Bibles and notes.
      // Only update missing indexes.
      if ((hour == 2) && (minute == 0)) {
        Database_State::create ();
        Database_Config_General::setIndexBibles (true);
        tasks_logic_queue (REINDEXBIBLES);
        Database_Config_General::setIndexNotes (true);
        tasks_logic_queue (REINDEXNOTES);
      }
      
      // Actions for a demo installation.
      if (minute == 10) {
        if (config_logic_demo_enabled ()) {
          tasks_logic_queue (CLEANDEMO);
        }
      }
      
      // Sprint burndown.
      // It runs every hour in the Cloud.
      // The script itself determines what to do at which hour of the day or day of the week or day of the month.
      if (!client) {
        if (minute == 5) {
          tasks_logic_queue (SPRINTBURNDOWN);
        }
      }

      // Quit at midnight if flag is set.
      if (config_globals_quit_at_midnight) {
        if (hour == 0) {
          if (minute == 1) {
            if (!Database_Config_General::getJustStarted ()) {
              if (tasks_run_active_count ()) {
                Database_Logs::log ("Server is due to restart itself but does not because of active jobs");
              } else {
                Database_Logs::log ("Server restarts itself");
                exit (0);
              }
            }
          }
          // Clear flag in preparation of restart next minute.
          // This flag also has the purpose of ensuring the server restarts once during that minute,
          // rather than restarting repeatedly many times during that minute.
          if (minute == 0) {
            Database_Config_General::setJustStarted (false);
          }
        }
      }
      
      // Email notes statistics to the users.
      if (!client) {
        if ((hour == 3) && (minute == 0)) {
          tasks_logic_queue (NOTESSTATISTICS);
        }
      }

      // Update SWORD stuff once a week.
      if (weekday == 1) {
        // Refresh module list.
        if ((!client) && (hour == 3) && (minute == 5)) {
          tasks_logic_queue (REFRESHSWORDMODULES);
        }
        // Update installed SWORD modules, shortly after the module list has been refreshed.
        if ((!client) && (hour == 3) && (minute == 15)) {
          tasks_logic_queue (UPDATESWORDMODULES);
        }
      }
      
      // The Cloud updates the list of USFM resources once a week.
      if (weekday == 1) {
        if ((!client) && (hour == 3) && (minute == 10)) {
          tasks_logic_queue (LISTUSFMRESOURCES);
        }
      }
      

    } catch (exception & e) {
      Database_Logs::log (e.what ());
    } catch (exception * e) {
      Database_Logs::log (e->what ());
    } catch (...) {
      Database_Logs::log ("A general internal error occurred in the timers");
    }

  }
}
void changes_modifications ()
{
  Database_Logs::log ("Change notifications: Generating", Filter_Roles::translator ());

  
  // Notifications are not available to clients for the duration of processing them.
  config_globals_change_notifications_available = false;
  
  
  // Data objects.
  Webserver_Request request;
  Database_Modifications database_modifications;


  // Check on the health of the modifications database and (re)create it if needed.
  if (!database_modifications.healthy ()) database_modifications.erase ();
  database_modifications.create ();
  
  
  // Create online change notifications for users who made changes in Bibles
  // through the web editor or through a client.
  // It runs before the team changes.
  // This produces the desired order of the notifications in the GUI.
  // At the same time, produce change statistics per user.
  
  // Get the users who will receive the changes entered by the contributors.
  vector <string> recipients;
  {
    vector <string> users = request.database_users ()->getUsers ();
    for (auto & user : users) {
      if (request.database_config_user ()->getContributorChangesNotificationsOnline (user)) {
        recipients.push_back (user);
      }
    }
  }

  // Storage for the statistics.
  map <string, int> user_change_statistics;
  float modification_time_total = 0;
  int modification_time_count = 0;
  
  vector <string> users = database_modifications.getUserUsernames ();
  if (!users.empty ()) Database_Logs::log ("Change notifications: Per user", Filter_Roles::translator ());
  for (auto user : users) {

    // Total changes made by this user.
    int change_count = 0;
    
    // Go through the Bibles changed by the current user.
    vector <string> bibles = database_modifications.getUserBibles (user);
    for (auto bible : bibles) {
      
      // Body of the email to be sent.
      string email = "<p>" + translate("You have entered the changes below in a Bible editor.") + " " + translate ("You may check if it made its way into the Bible text.") + "</p>";
      size_t empty_email_length = email.length ();
      
      // Go through the books in that Bible.
      vector <int> books = database_modifications.getUserBooks (user, bible);
      for (auto book : books) {
        
        // Go through the chapters in that book.
        vector <int> chapters = database_modifications.getUserChapters (user, bible, book);
        for (auto chapter : chapters) {
          
          // Get the sets of identifiers for that chapter, and set some variables.
          vector <Database_Modifications_Id> IdSets = database_modifications.getUserIdentifiers (user, bible, book, chapter);
          //int referenceOldId = 0;
          int referenceNewId = 0;
          int newId = 0;
          int lastNewId = 0;
          bool restart = true;
          
          // Go through the sets of identifiers.
          for (auto IdSet : IdSets) {
            
            int oldId = IdSet.oldid;
            newId = IdSet.newid;
            
            if (restart) {
              changes_process_identifiers (&request, user, recipients, bible, book, chapter, referenceNewId, newId, email, change_count, modification_time_total, modification_time_count);
              //referenceOldId = oldId;
              referenceNewId = newId;
              lastNewId = newId;
              restart = false;
              continue;
            }
            
            if (oldId == lastNewId) {
              lastNewId = newId;
            } else {
              restart = true;
            }
          }
          
          // Process the last set of identifiers.
          changes_process_identifiers (&request, user, recipients, bible, book, chapter, referenceNewId, newId, email, change_count, modification_time_total, modification_time_count);
          
        }
      }

      // Check whether there's any email to be sent.
      if (email.length () != empty_email_length) {
        // Send the user email with the user's personal changes if the user opted to receive it.
        if (request.database_config_user()->getUserUserChangesNotification (user)) {
          string subject = translate("Changes you entered in") + " " + bible;
          if (!client_logic_client_enabled ()) email_schedule (user, subject, email);
        }
      }
    }
    
    // Store change statistics for this user.
    user_change_statistics [user] = change_count;

    // Clear the user's changes in the database.
    database_modifications.clearUserUser (user);
    
    
    // Clear checksum cache.
    request.database_config_user ()->setUserChangeNotificationsChecksum (user, "");
  }
  
  
  // Generate the notifications, online and by email,
  // for the changes in the Bibles entered by anyone since the previous notifications were generated.
  vector <string> bibles = database_modifications.getTeamDiffBibles ();
  for (auto bible : bibles) {
    
    
    string stylesheet = Database_Config_Bible::getExportStylesheet (bible);
    
    
    vector <string> changeNotificationUsers;
    vector <string> users = request.database_users ()->getUsers ();
    for (auto user : users) {
      if (access_bible_read (&request, bible, user)) {
        if (request.database_config_user()->getUserGenerateChangeNotifications (user)) {
          changeNotificationUsers.push_back (user);
        }
      }
    }
    users.clear ();
    
    
    // The number of changes processed so far for this Bible.
    int processedChangesCount = 0;
    
    
    // The files get stored at http://site.org:<port>/revisions/<Bible>/<date>
    int seconds = filter_date_seconds_since_epoch ();
    string timepath;
    timepath.append (convert_to_string (filter_date_numerical_year (seconds)));
    timepath.append ("-");
    timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_month (seconds)), 2, '0'));
    timepath.append ("-");
    timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_month_day (seconds)), 2, '0'));
    timepath.append (" ");
    timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_hour (seconds)), 2, '0'));
    timepath.append (":");
    timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_minute (seconds)), 2, '0'));
    timepath.append (":");
    timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_second (seconds)), 2, '0'));
    string directory = filter_url_create_root_path ("revisions", bible, timepath);
    filter_url_mkdir (directory);
    
    
    // Produce the USFM and html files.
    filter_diff_produce_verse_level (bible, directory);
    
    
    // Create online page with changed verses.
    string versesoutputfile = filter_url_create_path (directory, "changed_verses.html");
    filter_diff_run_file (filter_url_create_path (directory, "verses_old.txt"), filter_url_create_path (directory, "verses_new.txt"), versesoutputfile);
    
    
    // Storage for body of the email with the changes.
    vector <string> email_changes;
    
    
    // Generate the online change notifications.
    vector <int> books = database_modifications.getTeamDiffBooks (bible);
    for (auto book : books) {
      vector <int> chapters = database_modifications.getTeamDiffChapters (bible, book);
      for (auto chapter : chapters) {
        Database_Logs::log ("Change notifications: " + bible + " " + filter_passage_display (book, chapter, ""), Filter_Roles::translator ());
        string old_chapter_usfm = database_modifications.getTeamDiff (bible, book, chapter);
        string new_chapter_usfm = request.database_bibles()->getChapter (bible, book, chapter);
        vector <int> old_verse_numbers = usfm_get_verse_numbers (old_chapter_usfm);
        vector <int> new_verse_numbers = usfm_get_verse_numbers (new_chapter_usfm);
        vector <int> verses = old_verse_numbers;
        verses.insert (verses.end (), new_verse_numbers.begin (), new_verse_numbers.end ());
        verses = array_unique (verses);
        sort (verses.begin (), verses.end());
        for (auto verse : verses) {
          string old_verse_usfm = usfm_get_verse_text (old_chapter_usfm, verse);
          string new_verse_usfm = usfm_get_verse_text (new_chapter_usfm, verse);
          if (old_verse_usfm != new_verse_usfm) {
            processedChangesCount++;
            // In case of too many change notifications, processing them would take too much time, so take a few shortcuts.
            string old_html = "<p>" + old_verse_usfm + "</p>";
            string new_html = "<p>" + new_verse_usfm + "</p>";
            string old_text = old_verse_usfm;
            string new_text = new_verse_usfm;
            if (processedChangesCount < 800) {
              Filter_Text filter_text_old = Filter_Text (bible);
              Filter_Text filter_text_new = Filter_Text (bible);
              filter_text_old.html_text_standard = new Html_Text ("");
              filter_text_new.html_text_standard = new Html_Text ("");
              filter_text_old.text_text = new Text_Text ();
              filter_text_new.text_text = new Text_Text ();
              filter_text_old.addUsfmCode (old_verse_usfm);
              filter_text_new.addUsfmCode (new_verse_usfm);
              filter_text_old.run (stylesheet);
              filter_text_new.run (stylesheet);
              old_html = filter_text_old.html_text_standard->getInnerHtml ();
              new_html = filter_text_new.html_text_standard->getInnerHtml ();
              old_text = filter_text_old.text_text->get ();
              new_text = filter_text_new.text_text->get ();
            }
            string modification = filter_diff_diff (old_text, new_text);
            database_modifications.recordNotification (changeNotificationUsers, changes_bible_category (), bible, book, chapter, verse, old_html, modification, new_html);
            string passage = filter_passage_display (book, chapter, convert_to_string (verse))   + ": ";
            if (old_text != new_text) {
              email_changes.push_back (passage  + modification);
            } else {
              email_changes.push_back (translate ("The following passage has no change in the text.") + " " + translate ("The change is in the formatting only.") + " " + translate ("The USFM code is given for reference."));
              email_changes.push_back (passage);
              email_changes.push_back (translate ("Old code:") + " " + old_verse_usfm);
              email_changes.push_back (translate ("New code:") + " " + new_verse_usfm);
            }
          }
        }
        // Delete the diff data for this chapter, for two reasons:
        // 1. New diffs for this chapter can be stored straightaway.
        // 2. In case of large amounts of diff data, and this function gets killed,
        //    then the next time it runs again, it will continue from where it was killed.
        database_modifications.deleteTeamDiffChapter (bible, book, chapter);
      }
    }
    
    
    // Email the changes to the subscribed users.
    if (!email_changes.empty ()) {
      string body;
      for (auto & line : email_changes) {
        body.append ("<div>");
        body.append (line);
        body.append ("</div>\n");
      }
      string subject = translate("Recent changes:") + " " + bible;
      vector <string> users = request.database_users ()->getUsers ();
      for (auto & user : users) {
        if (request.database_config_user()->getUserBibleChangesNotification (user)) {
          if (access_bible_read (&request, bible, user)) {
            if (!client_logic_client_enabled ()) {
              email_schedule (user, subject, body);
            }
          }
        }
      }
    }
    
    
  }

  
  // Index the data and remove expired notifications.
  Database_Logs::log ("Change notifications: Indexing", Filter_Roles::translator ());
  database_modifications.indexTrimAllNotifications ();

  
  // Remove expired downloadable revisions.
  string directory = filter_url_create_root_path ("revisions");
  int now = filter_date_seconds_since_epoch ();
  bibles = filter_url_scandir (directory);
  for (auto &bible : bibles) {
    string folder = filter_url_create_path (directory, bible);
    int time = filter_url_file_modification_time (folder);
    int days = (now - time) / 86400;
    if (days > 31) {
      filter_url_rmdir (folder);
    } else {
      vector <string> revisions = filter_url_scandir (folder);
      for (auto & revision : revisions) {
        string path = filter_url_create_path (folder, revision);
        int time = filter_url_file_modification_time (path);
        int days = (now - time) / 86400;
        if (days > 31) {
          filter_url_rmdir (path);
          Database_Logs::log ("Removing expired downloadable revision notification: " + bible + " " + revision, Filter_Roles::translator ());
        }
      }
    }
  }
  
  
  // Clear checksum caches.
  users = request.database_users ()->getUsers ();
  for (auto user : users) {
    request.database_config_user ()->setUserChangeNotificationsChecksum (user, "");
  }
  
  
  // Vacuum the modifications index, as it might have been updated.
  database_modifications.vacuum ();
  
  
  // Make the notifications available again to clients.
  config_globals_change_notifications_available = true;

  
  // Store the statistics in the database.
  if (modification_time_count) {
    // Take average timestamp of all timestamps.
    int timestamp = round (modification_time_total / modification_time_count);
    for (auto & element : user_change_statistics) {
      // Store dated change statistics per user.
      string user = element.first;
      int count = element.second;
      Database_Statistics::store_changes (timestamp, user, count);
    }
  }
  

  Database_Logs::log ("Change notifications: Ready", Filter_Roles::translator ());
}