EduNetworkBuilder/EduNetworkBuilder/PersonClass.cs

651 lines
26 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.IO;
using System.Windows.Forms;
using System.Drawing;
using System.Security.Cryptography;
using System.Security.Cryptography.Xml;
namespace EduNetworkBuilder
{
public class PersonClass
{
public string filepath=""; //the path of the file. We use the username as the file-name.
public string StudentFilePath = "";//If admin, this is used to store the path of the directory where student files are stored.
string _UserName="";
/// <summary>
/// The username of the person. Will be used as a filename. Cannot be changed once it is set
/// </summary>
public string UserName { get { return _UserName; } }
/// <summary>
/// The filename, without the path, of the file. UserName + ".enbu"
/// </summary>
public string FileName { get { return UserName + ".enbu"; } }
/// <summary>
/// The full name of the person. Can have spaces and punctuation. Can change as one wants it changed.
/// </summary>
public string FullName = "";
public string Password = "";
string PasswordHint = "";
/// <summary>
/// The AltPassword is mainly used to hold the admin decrypting password. The student account will
/// use this to encrypt the student password, so the admin can open their file.
/// </summary>
public string AltPassword = "";
/// <summary>
/// Used to determine if the user we are working with is the admin account.
/// </summary>
public bool isAdmin { get; protected set; }
public string EncryptionPassword { get { return UserName + Password; } }
public List<SchoolworkClass> Projects = new List<SchoolworkClass>();
public List<PersonClass> Students = new List<PersonClass>(); //only teachers (admins) will have students.
public NBSettings UserSettings = new NBSettings();
public bool ChangePassAtFirstLogin = true;
private PersonClass()
{ }
public PersonClass(String User, bool MakeAdmin)
{
_UserName = User;
isAdmin = MakeAdmin;
}
public PersonClass(string Filename)
{
Load(Filename);
string FileAndExtension = Path.GetFileName(Filename);
string JustFile = Path.GetFileNameWithoutExtension(Filename);
if(UserName != JustFile)
{
throw new Exception(string.Format(NB.Translate("PPF_TamperedFile"), FileAndExtension, UserName));
}
}
public PersonClass(XmlNode reader)
{
Load(reader);
}
/// <summary>
/// Try to load the specified filename. Return a new person record if it succeeds.
/// Return null if it fails to load. (bad password, bad file, etc)
/// </summary>
/// <param name="Filename">The filename to load</param>
/// <param name="password">The password to try</param>
/// <returns></returns>
public static PersonClass TryLoad(string Filename, string password)
{
PersonClass pc = new PersonClass();
if (pc.Load(Filename, password))
return pc;
return null;
}
public void ChangePassword(string NewPassword)
{
Password = NewPassword;
}
public void ChangePassword(bool TryOnce=false)
{
bool done = false;
while (!done)
{
if (TryOnce) done = true; //Just attempt it once.
string OldPassword = "";
if (Password != "")
{
OldPassword = NB.TextPromptBox(NB.Translate("PPF_OldPassword"), Properties.Resources.NBIco, true);
if (OldPassword != Password)
{
MessageBox.Show(NB.Translate("PPF_PassMismatch"));
continue;
}
}
//The passwords match, or there was no initial password.
string Password1 = NB.TextPromptBox(NB.Translate("PPF_NewPass"), Properties.Resources.NBIco, true);
if (Password1.Length < 4)
{
MessageBox.Show(NB.Translate("PPF_PassTooShort"));
continue;
}
if (Password1 == OldPassword)
{
MessageBox.Show(NB.Translate("PPF_PassDidNotChange"));
continue;
}
string Password2 = NB.TextPromptBox(NB.Translate("PPF_VerifyPass"), Properties.Resources.NBIco, true);
if (Password1 != Password2)
{
MessageBox.Show(NB.Translate("PPF_PassValidateMismatch"));
continue;
}
ChangePassword(Password1);
if (ChangePassAtFirstLogin) ChangePassAtFirstLogin = false;//It was done. No need to do it again.
done = true;
}
}
#region Load and Save
//Load and save functions
public bool Load(string filename, string password = "")
{
XmlDocument xmlDoc = new XmlDocument();
if (File.Exists(filename))
{
xmlDoc.Load(filename);
if (TryToDecrypt(xmlDoc, password) != null)
{
Load(xmlDoc);
return true;
}
}
return false;
}
public void Load(XmlNode TheNode)
{
foreach (XmlNode Individual in TheNode.ChildNodes)
{
XmlNodeType myNodetype = Individual.NodeType;
if (myNodetype == XmlNodeType.Element)
{
switch (Individual.Name.ToLower())
{
case "edunetworkbuilderuser":
case "edunetworkbuilder":
case "user":
Load(Individual);
break;
case "username":
_UserName = Individual.InnerText;
break;
case "fullname":
FullName = Individual.InnerText;
break;
case "passwordhint":
PasswordHint = Individual.InnerText;
break;
case "altpassword":
AltPassword = Individual.InnerText;
break;
case "studentfilepath":
StudentFilePath = Individual.InnerText;
break;
case "isadmin":
bool isadmin = false;
bool.TryParse(Individual.InnerText, out isadmin);
isAdmin = isadmin;
break;
case "changepasswordatlogin":
bool changepw = false;
bool.TryParse(Individual.InnerText, out changepw);
ChangePassAtFirstLogin = changepw;
break;
case "password":
Password = Individual.InnerText;
break;
case "settings":
UserSettings = NB.Deserialize<NBSettings>(Individual.InnerText);
break;
case "project":
case "schoolwork":
SchoolworkClass SWC = new SchoolworkClass(Individual);
Projects.Add(SWC);
break;
case "student":
PersonClass PC = new PersonClass(Individual);
Students.Add(PC);
break;
}
}
}
}
public void Save(bool doRotation = false)
{
//Save to our pre-existing / pre-defined file
string filename = Path.Combine(filepath, FileName);
Save(filename, doRotation);
}
public void Save(string filename, bool doRotation = false)
{
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.NewLineOnAttributes = true;
if (doRotation && File.Exists(filename))
NB.MoveFileWithRotation(filename); //make a backup of the file
//Generate a doc that has a writer attached. All the normal save funcs work with the writer,
//But, we can encrypt the XmlDocument
XmlDocument doc = new XmlDocument();
using (XmlWriter writer = doc.CreateNavigator().AppendChild())
{
//XmlWriter writer = XmlWriter.Create(filename, settings);
//Now we write the file:
writer.WriteStartDocument();
writer.WriteStartElement("EduNetworkBuilderUser");
writer.WriteComment("This is a user file for EduNetworkBuilder.");
Save(writer);
writer.WriteEndElement();
writer.WriteEndDocument();
}
if(TryToEncrypt(doc) != null)
doc.Save(filename);
else
{
//We should blow up gracefully. Not sure why we failed.
}
}
public void Save(XmlWriter writer, bool AsStudent=false)
{
//Save the language name
//save the number of items
//Save all the items
string Element = "User";
if (AsStudent) Element = "Student";
writer.WriteStartElement(Element);
writer.WriteElementString("UserName", UserName);
writer.WriteElementString("FullName", FullName);
writer.WriteElementString("PasswordHint", PasswordHint);
if(isAdmin) //only admins have students, so they are the only ones who need to store this path
writer.WriteElementString("StudentFilePath", StudentFilePath);
if (AltPassword == "") AltPassword = TrippleDESDocumentEncryption.GenMachinePW(NB.GetRandom());
writer.WriteElementString("AltPassword", AltPassword);
writer.WriteElementString("IsAdmin", isAdmin.ToString());
writer.WriteElementString("ChangePasswordAtLogin", ChangePassAtFirstLogin.ToString());
string settingsstring = NB.SerializeObject<NBSettings>(UserSettings);
writer.WriteElementString("Settings", settingsstring);
writer.WriteElementString("Password", Password);
foreach (PersonClass PC in Students)
PC.Save(writer, true); //Save as a student entry
//Save all the devices
foreach (SchoolworkClass One in Projects)
{
One.Save(writer);
}
writer.WriteEndElement();
}
private XmlDocument TryToEncrypt(XmlDocument What)
{
string UserPassword = UserName + Password;
if (UserPassword == "") return null; //This should never happen
string salt = TrippleDESDocumentEncryption.GenSalt(NB.GetRandom());
TripleDES tDESkey = TrippleDESDocumentEncryption.GenKey(UserPassword, salt);
TrippleDESDocumentEncryption xmlTDES = new TrippleDESDocumentEncryption(What, tDESkey);
try
{
// Encrypt the "user" element.
xmlTDES.Encrypt("User");
//make the entries for the key
XmlNode tNode = xmlTDES.Doc.CreateElement("EncryptedKey");
tNode.InnerText = UserPassword;
XmlElement inputElement = xmlTDES.Doc.GetElementsByTagName("EncryptedData")[0] as XmlElement;
xmlTDES.Doc.DocumentElement.InsertAfter(tNode, inputElement);
//add the salt
tNode = xmlTDES.Doc.CreateElement("Salt");
tNode.InnerText = salt;
xmlTDES.Doc.DocumentElement.InsertAfter(tNode, inputElement);
//encrypt the user key with the admin key
xmlTDES.SetKey(AltPassword, salt);
xmlTDES.Encrypt("EncryptedKey");
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return null;
}
return xmlTDES.Doc;
}
private XmlDocument TryToDecrypt(XmlDocument What, string PassToTry)
{
bool first_failed = false;
XmlElement inputElement = What.GetElementsByTagName("Salt")[0] as XmlElement;
if (inputElement == null) return What; //No salt means it was never encrypted. Load as normal
string salt = inputElement.InnerText;
// Create a new TripleDES key.
TripleDES tDESkey = TrippleDESDocumentEncryption.GenKey(PassToTry, salt);
TrippleDESDocumentEncryption xmlTDES = new TrippleDESDocumentEncryption(What, tDESkey);
//First, try to decrypt it using the user password.
try
{
xmlTDES.Decrypt("User");
}
catch (LoginException le)
{
//we ignore this. Just try it again.
first_failed = true;
}
catch(Exception e) //This is bad. Blow up and exit the function
{
MessageBox.Show(e.ToString());
return null;
}
if (first_failed)
{ //try a second one, decrypting the alt key and then using that. The admin does this.
// Create a new instance of the TrippleDESDocumentEncryption object
// defined in this sample.
try
{
xmlTDES.Decrypt("EncryptedKey");
inputElement = xmlTDES.Doc.GetElementsByTagName("EncryptedKey")[0] as XmlElement;
string userpw = inputElement.InnerText;
xmlTDES.SetKey(userpw, salt);
xmlTDES.Decrypt("User");
}
catch (LoginException le)
{
//Neither password worked. Exit out so we can prompt for an appropriate password
return null;
}
catch (Exception e) //This is bad. Blow up and exit the function
{
MessageBox.Show(e.ToString());
return null;
}
}
return xmlTDES.Doc;
}
#endregion
public bool AddHomework(SchoolworkClass ToAdd)
{
//Check to see if we already exist
foreach(SchoolworkClass one in Projects)
{
if (one.ThisID == ToAdd.ThisID) return false;
}
Projects.Add(ToAdd);
if (ToAdd.ThisID >= UserSettings.NextID) UserSettings.NextID = (uint)(ToAdd.ThisID + 1);
return true;
}
public bool HasHomework(SchoolworkClass ToCheck, bool compare_ID)
{
//Check to see if we already exist
foreach (SchoolworkClass one in Projects)
{
if (compare_ID && one.ThisID == ToCheck.ThisID)
return true;
else if (!compare_ID && one.Name.ToLower() == ToCheck.Name.ToLower())
return true;
}
return false;
}
public bool RegisterNewlySubmittedHW(SchoolworkClass ToCheck)
{
//Check to see if we already exist
foreach (SchoolworkClass one in Projects)
{
if (one.ThisID == ToCheck.ThisID)
{
if (one.SaveDate == ToCheck.SaveDate)
return false; //They were the same
}
}
//If we get here, we do not yet have it. Add it to the user.
Projects.Add(ToCheck.Clone());
return true;
}
public void LoadHomeworkFile(string HomeworkFile)
{
if (!File.Exists(HomeworkFile)) return; //The file does not exist
if (! (Path.GetExtension(HomeworkFile).ToLower() == ".enbh")) return; //It is not a homework file
List<SchoolworkClass> WhatWeRead = SchoolworkClass.LoadMultiple(HomeworkFile);
if(WhatWeRead.Count > 0) //We have something to import
{
bool IsOK = true;
//Verify that all the homeworks are OK to load
foreach(SchoolworkClass one in WhatWeRead)
{
//if one is already submitted, reject it
if(one.IsSumbitted || one.IsGraded)
{
IsOK = false;
}
}
//Load them
if(IsOK)
{
int totalHW =0;
int Skipped = 0;
int imported = 0;
if (isAdmin)
{
Dictionary<string, bool> InitialList = new Dictionary<string, bool>();
//use a check-box list
foreach (SchoolworkClass one in WhatWeRead)
{
//Use due date and name + already exists (name / ID)
string Info = one.DueDate.ToShortDateString();
Info += "\t" + one.Name;
if (HasHomework(one, true)) Info += " * " + string.Format(NB.Translate("PC_HWHasID"),one.ThisID);
if (HasHomework(one, false)) Info += " ** " + string.Format(NB.Translate("PC_HWHasName"));
InitialList.Add(Info, false);
}
Dictionary<string, bool> ResponseList = NB.CheckPromptBox(InitialList, "Import", Properties.Resources.NBIco);
if (ResponseList == null) return; //They canceled out.
int index = 0;
foreach(string One in ResponseList.Keys)
{
totalHW++;
if (ResponseList[One])
{ //We are supposed to import it.
if (HasHomework(WhatWeRead[index], true))
{ //We already have this ID. Get a new ID
WhatWeRead[index].ChangeID(this); //Pass in this user, whoever we are
}
//Now we can add it without issue.
AddHomework(WhatWeRead[index]);
imported++;
}
else
Skipped++;
index++;
}
//they check if they want to import
//We never overwrite, we make a new one, with new ID
}
else
{
foreach (SchoolworkClass one in WhatWeRead)
{
totalHW++;
if (AddHomework(one))
{
imported++;
}
else
{
Skipped++;
}
}
}
MessageBox.Show(string.Format(NB.Translate("PC_HWFileImportStats"),"\n\t" + totalHW, "\n\t" + imported, "\n\t" + Skipped));
}
else
{
MessageBox.Show(NB.Translate("PC_AlreadySubmittedOrGraded"));
}
}
}
/// <summary>
/// Organize the projects into a nice project tree.
/// </summary>
/// <returns></returns>
public TreeNode WorkTree(TreeNode theTop=null, bool ForTeacher = false)
{
//Sort by due date, and within the due dates, sort by save date
Projects.Sort((q, p) => p.Compare(q));
List<SchoolworkClass> DrawFrom = new List<SchoolworkClass>();
List<SchoolworkClass> MasterList = new List<SchoolworkClass>();
bool SomethingWasBolded = false; //Set this to true if we make something bold
DrawFrom.AddRange(Projects);
for (int i = DrawFrom.Count - 1; i >= 0; i--)
{
if (DrawFrom[i].isMaster)
{
MasterList.Add(DrawFrom[i]);
DrawFrom.RemoveAt(i);
}
}
//Now, all the masters are in Master, and submitted projects are in DrawFrom.
TreeNode Node;
TreeNode Top;
TreeNode Master;
//We have a class tree
if (theTop == null)
{
Top = new TreeNode(NB.Translate("PPF_Class"));
Top.Tag = "Class"; //Do not translate this
}
else
{
Top = theTop;
}
Font tFont = NB.GetFont();
foreach (SchoolworkClass MasterSW in MasterList)
{
int count = 0;
int ungraded = 0;
Master = new TreeNode(MasterSW.Name);
Master.Tag = MasterSW;
for (int i = DrawFrom.Count -1; i>=0; i--)
{
if(DrawFrom[i].ThisID == MasterSW.ThisID)
{
Node = new TreeNode(DrawFrom[i].Name);
Node.Tag = DrawFrom[i];
string tooltip = FullName + "\n";
tooltip += DrawFrom[i].Name + "\n";
tooltip += DrawFrom[i].Description + "\n";
if(DrawFrom[i].IsSumbitted)
tooltip += "Submitted: " + DrawFrom[i].SaveDate.ToShortDateString() + "\n";
if(DrawFrom[i].theProject != null)
{
List<string> tMessages = DrawFrom[i].theProject.GetMessageStrings();
foreach(string one in tMessages)
{
tooltip += one + "\n";
}
}
Node.ToolTipText = tooltip;
Master.Nodes.Add(Node);
if (ForTeacher && !DrawFrom[i].IsGraded) //Bold ungraded homework
{
Node.NodeFont = new Font(tFont.FontFamily, tFont.Size, FontStyle.Bold);
SomethingWasBolded = true;
ungraded++;
}
count++; //How many submitted versions are there
}
}
if(!ForTeacher && count ==0) //bold homework that has not yet been submitted
{
Master.NodeFont = new Font(tFont.FontFamily, tFont.Size, FontStyle.Bold);
SomethingWasBolded = true;
} else if(ForTeacher && ungraded > 0) //If we have ungraded submissions
{
Master.NodeFont = new Font(tFont.FontFamily, tFont.Size, FontStyle.Bold);
SomethingWasBolded = true;
}
Top.Nodes.Add(Master);
}
if(ForTeacher && SomethingWasBolded)
Top.NodeFont = new Font(tFont.FontFamily, tFont.Size, FontStyle.Bold);
return Top;
}
/// <summary>
/// Return the student with the specified UserName
/// </summary>
/// <param name="UserName">The user name to find</param>
/// <returns>The specified person, or null if no such person exists.</returns>
public PersonClass StudentWithTheUserName(string UserName)
{
if (!isAdmin) return null; //Students do not store other student records
foreach(PersonClass PC in Students)
{
if (PC.UserName == UserName) return PC;
}
return null;
}
/// <summary>
/// Import the settings from a student record
/// </summary>
/// <param name="ImportedStudent">the student we are importing from</param>
/// <returns>The count of homeworks we added. -1 if there is an error</returns>
public int TeacherImportStudentHomework(PersonClass ImportedStudent)
{
if (!isAdmin) return -1;
PersonClass StoredStudent = StudentWithTheUserName(ImportedStudent.UserName);
if (StoredStudent == null) return -1;
int NumSubmitted = 0;
//Retrieve all the stored settings, in case we need to re-write their file
StoredStudent.UserSettings = ImportedStudent.UserSettings;
//Pull out their password, in case we want to retrieve it. (sigh)
StoredStudent.Password = ImportedStudent.Password;
// loop through all networks
foreach (SchoolworkClass SWC in ImportedStudent.Projects)
{
// Find any that are submitted, which are not in the admin file
if (StoredStudent.RegisterNewlySubmittedHW(SWC))
{
// copy them into the admin file
if(SWC.IsSumbitted) //Only report submitted homeworks back.
NumSubmitted++;
}
}
return NumSubmitted;
}
}
}