using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Mail; using System.Reflection; using System.Security; using System.Text; using System.Text.RegularExpressions; using log4net; using Nini.Config; using OpenMetaverse; using OpenMetaverse.StructuredData; using OpenSim.Data.MySQL; using OpenSim.Framework; using OpenSim.Framework.Servers; using OpenSim.Framework.Servers.HttpServer; using OpenSim.Server.Handlers.Base; namespace OpenSim.Server.Handlers.Web { public class WebServerConnector : ServiceConnector { // This is all slow and clunky, it's not a real web server, just something to use if you don't want a real one. private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); private IConfigSource m_Config; protected MySQLRaw m_database = null; private Hashtable mime = new Hashtable(); private Hashtable ssi = new Hashtable(); private IPAddress m_IP; private IHttpServer m_server; // private IHttpServer m_SSLserver = null; private string m_domain = ""; private uint m_http_port; // private uint m_https_port = 0; private Dictionary m_auth = new Dictionary(); private static Dictionary m_firstNames = new Dictionary(); private static Dictionary m_lastNames = new Dictionary(); private static Dictionary m_fullNames = new Dictionary(); /* TODO - shelved for now, rewrite it in Lua for lighttpd after the release. private string m_SMTP_server; private string m_SMTP_port; private string m_SMTP_user; private string m_SMTP_password; private string m_email_from; private SmtpClient m_smtp; */ public WebServerConnector(IConfigSource config, IHttpServer server, string configName) : base(config, server, configName) { string dllName = String.Empty; string connString = String.Empty; m_Config = config; m_server = server; m_IP = MainServer.Instance.ListenIPAddress; m_http_port = server.Port; // Try reading the [DatabaseService] section, if it exists IConfig dbConfig = m_Config.Configs["DatabaseService"]; if (dbConfig != null) { if (dllName == String.Empty) dllName = dbConfig.GetString("StorageProvider", String.Empty); if (connString == String.Empty) connString = dbConfig.GetString("ConnectionString", String.Empty); } if (dllName.Equals(String.Empty)) throw new Exception("No StorageProvider configured"); //// TODO - Should do the plugin thing to pick between database backends. //// Or not, we are all using MariaDB anyway. // m_Database = LoadPlugin(dllName, new Object[] { connString }); m_database = new MySQLRaw(connString); mime.Add(".gz", "application/gzip"); mime.Add(".js", "application/javascript"); mime.Add(".json", "application/json"); mime.Add(".pdf", "application/pdf"); mime.Add(".rtf", "application/rtf"); mime.Add(".zip", "application/zip"); mime.Add(".xz", "application/x-xz"); mime.Add(".gif", "image/gif"); mime.Add(".png", "image/png"); mime.Add(".jp2", "image/jp2"); mime.Add(".jpg2", "image/jp2"); mime.Add(".jpe", "image/jpeg"); mime.Add(".jpg", "image/jpeg"); mime.Add(".jpeg", "image/jpeg"); mime.Add(".svg", "image/svg+xml"); mime.Add(".svgz", "image/svg+xml"); mime.Add(".tif", "image/tiff"); mime.Add(".tiff", "image/tiff"); mime.Add(".css", "text/css"); mime.Add(".html", "text/html"); mime.Add(".htm", "text/html"); mime.Add(".shtml", "text/html"); // mime.Add(".md", "text/markdown"); // mime.Add(".markdown","text/markdown"); mime.Add(".txt", "text/plain"); // Grab some info. IConfig cfg = m_Config.Configs["GridInfoService"]; string HomeURI = Util.GetConfigVarFromSections(m_Config, "HomeURI", new string[] { "Startup", "Hypergrid" }, String.Empty); ssi.Add("grid", cfg.GetString("gridname", "my grid")); ssi.Add("uri", cfg.GetString("login", HomeURI)); ssi.Add("version", VersionInfo.Version); cfg = m_Config.Configs["Const"]; m_domain = cfg.GetString("HostName", "localhost"); /* TODO - shelved for now, rewrite it in Lua for lighttpd after the release. // Copied from OpenSim/Region/OptionalModules/ViewerSupport/GodNamesModule.cs cfg = m_Config.Configs["GodNames"]; if (null != cfg) { m_log.Info("[WEB SERVICE]: Loading god names."); string conf_str = cfg.GetString("FullNames", String.Empty); if (String.Empty != conf_str) { foreach (string strl in conf_str.Split(',')) { string strlan = strl.Trim(" \t".ToCharArray()); m_log.InfoFormat("[WEB SERVICE]: Adding {0} as a god name", strlan); m_fullNames.Add(strlan, strlan); } } conf_str = cfg.GetString("FirstNames", String.Empty); if (String.Empty != conf_str) { foreach (string strl in conf_str.Split(',')) { string strlan = strl.Trim(" \t".ToCharArray()); m_log.InfoFormat("[WEB SERVICE]: Adding {0} as a god first name", strlan); m_firstNames.Add(strlan, strlan); } } conf_str = cfg.GetString("Surnames", String.Empty); if (String.Empty != conf_str) { foreach (string strl in conf_str.Split(',')) { string strlan = strl.Trim(" \t".ToCharArray()); m_log.InfoFormat("[WEB SERVICE]: Adding {0} as a god last name", strlan); m_lastNames.Add(strlan, strlan); } } } else m_log.Info("[WEB SERVICE]: No god names loaded."); // Add the email client. cfg = m_Config.Configs["SMTP"]; if (null != cfg) { m_log.Info("[WEB SERVICE]: Loading email configuration."); m_SMTP_server = cfg.GetString("SMTP_SERVER_HOSTNAME", "127.0.0.1"); m_SMTP_port = cfg.GetString("SMTP_SERVER_PORT", "25"); m_SMTP_user = cfg.GetString("SMTP_SERVER_LOGIN", ""); m_SMTP_password = cfg.GetString("SMTP_SERVER_PASSWORD", ""); m_email_from = cfg.GetString("host_domain_header_from", "grid@localhost"); m_smtp = new SmtpClient { Host = m_SMTP_server, Port = Convert.ToInt16(m_SMTP_port), EnableSsl = false, DeliveryMethod = SmtpDeliveryMethod.Network, Credentials = new NetworkCredential(m_SMTP_user, m_SMTP_password), Timeout = 20000 }; } */ // Add the HTTP and HTTPS handlers. server.AddHTTPHandler("/web/", WebRequestHandler); /* TODO - shelved for now, rewrite it in Lua for lighttpd after the release. IConfig networkConfig = m_Config.Configs["Network"]; if (null != networkConfig) { m_https_port = (uint) networkConfig.GetInt("https_port", 0); if (0 != m_https_port) { m_SSLserver = MainServer.GetHttpServer(m_https_port, null); if (null != m_SSLserver) m_SSLserver.AddHTTPHandler("/web/", WebRequestHandlerSSL); } } */ } // AAARGGGH, in the request we don't get the HTTP/S, domain name, nor port number we were called from. So we have to fake it, sorta. /* TODO - shelved for now, rewrite it in Lua for lighttpd after the release. private Hashtable WebRequestHandlerSSL(Hashtable request) { return Handler(request, true); } */ private Hashtable WebRequestHandler(Hashtable request) { return Handler(request, false); } private Hashtable Handler(Hashtable request, bool usedSSL) { Hashtable reply = new Hashtable(); Hashtable replyHeaders = new Hashtable(); Hashtable cookies = new Hashtable(); Hashtable fields = new Hashtable(); List errors = new List(); string reqpath = (string) request["uri"]; string method = (string) request["http-method"]; string type = (string) request["content-type"]; string body = (string) request["body"]; string[] query = (string[]) request["querystringkeys"]; Hashtable headers = (Hashtable) request["headers"]; Hashtable vars = (Hashtable) request["requestvars"]; string file = reqpath.Remove(0, 5); string path = Path.Combine(Util.webDir(), file); // m_log.InfoFormat("[WEB SERVICE]: {0} {1} {2} : {3} {4}, server IP {5} content type {6}, body {7}.", // headers["remote_addr"].ToString(), method, m_domain, (usedSSL ? m_https_port : m_http_port), reqpath, m_IP, type, body); m_log.InfoFormat("[WEB SERVICE]: {0} {1} {2} : {3} {4}, server IP {5} content type {6}, body {7}.", headers["remote_addr"].ToString(), method, m_domain, m_http_port, reqpath, m_IP, type, body); if (! Path.GetFullPath(path).StartsWith(Path.GetFullPath(Util.webDir()))) { m_log.ErrorFormat("[WEB SERVICE]: INVALID PATH {0} != {1}", Path.GetFullPath(path), Path.GetFullPath(Util.webDir())); reply["int_response_code"] = 404; reply["content_type"] = "text/html"; reply["str_response_string"] = "404 Unknown page" + "404 error, can't find the " + reqpath + " page.

 

"; return reply; } long locIn = m_database.Count("Presence", "RegionID != '00000000-0000-0000-0000-000000000000'"); // Locals online but not HGing, and HGers in world. long HGin = m_database.Count("Presence", "UserID NOT IN (SELECT PrincipalID FROM UserAccounts)"); // HGers in world. ssi["hgers"] = HGin.ToString(); ssi["inworld"] = (locIn - HGin).ToString(); ssi["outworld"] = m_database.Count("hg_traveling_data", "GridExternalName != '" + ssi["uri"] + "'").ToString(); // Locals that are HGing. ssi["members"] = m_database.Count("UserAccounts").ToString(); ssi["month"] = m_database.Count("GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))").ToString(); ssi["sims"] = m_database.Count("regions").ToString(); foreach (DictionaryEntry h in headers) { if ("cookie" == h.Key.ToString()) { string[] cks = h.Value.ToString().Split(';'); foreach (String c in cks) { string[] ck = c.Split('='); cookies[ck[0].Trim(' ')] = ck[1].Trim(' '); } } } if ("POST" == method) { string[] bdy = body.Split('&'); body = ""; foreach (String bd in bdy) { string[] b = bd.Split('='); if (b.Length == 0) continue; string n = System.Web.HttpUtility.UrlDecode(b[0]); string v = ""; if (b.Length > 1) v = System.Web.HttpUtility.UrlDecode(b[1]); fields[n] = bobbyTables(n, v); body = body + "

" + n + " = " + v + "

\n"; } } foreach (String q in query) { // m_log.InfoFormat("[WEB SERVICE]: {0} {1} query {2} = {3}", method, reqpath, q, (string) request[q]); fields[q] = bobbyTables(q, (string) request[q]); } // foreach (DictionaryEntry h in headers) // m_log.DebugFormat("[WEB SERVICE]: {0} {1} header {2} = {3}", method, reqpath, (string) h.Key, (string) h.Value); // I dunno what these vars are or where they come from, never actually seen them. // Ah, viewers send them, and they seem to be identical to the query that viewers also send. // foreach (DictionaryEntry h in vars) // m_log.InfoFormat("[WEB SERVICE]: {0} {1} var {2} = {3}", method, reqpath, (string) h.Key, (string) h.Value); reply["int_response_code"] = 200; if (("GET" == method) || ("HEAD" == method)) { if (File.Exists(path)) { DateTime dt = File.GetLastWriteTimeUtc(path); string m = (string) mime[Path.GetExtension(path).ToLower()]; reply["content_type"] = m; if ((null == m) || ("text/" != m.Substring(0, 5))) { string ifdtr = (string) headers["if-modified-since"]; if (null != ifdtr) { try { DateTime ifdt = DateTime.Parse(ifdtr, System.Globalization.CultureInfo.InvariantCulture); if (0 >= DateTime.Compare(ifdt, dt)) { reply["int_response_code"] = 304; m_log.InfoFormat("[WEB SERVICE]: If-Modified-Since is earliar or equal to Last-Modified, from {0}", reqpath); reply["headers"] = replyHeaders; if ("HEAD" == method) { reply.Remove("bin_response_data"); reply.Remove("str_response_string"); } return reply; } } catch (Exception) { m_log.WarnFormat("[WEB SERVICE]: Invalid If-Modified-Since header, ignoring it, from {0} - {1}", reqpath, ifdtr); } } replyHeaders["Last-Modified"] = dt.ToString("R"); reply["bin_response_data"] = File.ReadAllBytes(path); } else { replyHeaders["Cache-Control"] = "no-cache"; StreamReader csr = File.OpenText(path); string content = csr.ReadToEnd(); // Slow and wasteful, but I'm expecting only tiny web files, not accessed very often. foreach (DictionaryEntry v in ssi) { content = content.Replace("", (string) v.Value); } reply["str_response_string"] = content; csr.Close(); } } /* TODO - shelved for now, rewrite it in Lua for lighttpd after the release. else { if ("account.html" == file) { if (usedSSL) reply["str_response_string"] = loginPage(null, ""); else // Force HTTPS by redirecting. { reply["int_response_code"] = 200; reply["content_type"] = "text/html"; reply["str_response_string"] = "404 Unknown page" + "" + ""; } } else { m_log.ErrorFormat("[WEB SERVICE]: Unable to read {0}.", reqpath); reply["int_response_code"] = 404; reply["content_type"] = "text/html"; reply["str_response_string"] = "404 Unknown page" + "404 error, can't find the " + reqpath + " page.

 

"; } } */ } else if ("POST" == method) { // if ("account.html" == file) // { // string doit = fields["doit"].ToString(); // string toke_n_munchie = ""; // replyHeaders["Cache-Control"] = "no-cache"; /* TODO - Switch to using prepared SQL statements. Actually authenticate them. Deal with dictionary attacks by slowing down access on password failures etc. Regenerate token on authentication. Store users UUID. Invalidate token on logout and password reset. Logout when invalidating tokens. Deal with validation and password reset emails, likely with the same code. Deal with editing yourself. Deal with editing others, but only as god. */ /* TODO - shelved for now, rewrite it in Lua for lighttpd after the release. if ((null == cookies["toke_n_munchie"]) || (null == fields["toke_n_munchie"]) || ("" == cookies["toke_n_munchie"].ToString()) || ("" == fields["toke_n_munchie"].ToString())) toke_n_munchie = newSession(doit, headers, ref fields, ref replyHeaders); else if (cookies["toke_n_munchie"].ToString() != fields["toke_n_munchie"].ToString()) errors.Add("Invalid session."); else { toke_n_munchie = cookies["toke_n_munchie"].ToString(); Hashtable auth = m_auth[toke_n_munchie]; if (null == auth) { errors.Add("Null session."); m_log.InfoFormat("[WEB SERVICE]: Null session {0} - {1}.", toke_n_munchie, doit); } else { // TODO - maybe check if session has expired, but how long should they last? if (auth["IP"].ToString() != headers["remote_addr"].ToString()) errors.Add("Wrong IP for session."); else { auth["time"] = DateTime.Now; m_auth[toke_n_munchie] = auth; m_log.InfoFormat("[WEB SERVICE]: New timestamp for session {0} - {1}.", toke_n_munchie, doit); } } } if (0 != errors.Count) deleteSession(toke_n_munchie, doit, headers, ref fields, ref replyHeaders); if (("https://" + m_domain + ":" + m_https_port.ToString() + "/web/account.html") != headers["referer"].ToString()) errors.Add("Invalid referer."); // Include a check for god names if we are creating a new account. string[] names = validateName(("create" == doit) || ("confirm" == doit), fields, ref errors); if ("logout" == doit) { deleteSession(toke_n_munchie, doit, headers, ref fields, ref replyHeaders); errors.Add("Logged out."); } else if (("create" == doit) || ("confirm" == doit)) { validateEmail(fields, ref errors); if ("confirm" == doit) validatePassword(fields, ref errors); if (0 == errors.Count) { // Check the account name doesn't exist yet. // Which might be tricky, apparently names are not case sensitive on login, but stored with case in the database. // I confirmed that, can log in no matter what case you use. // UserAccounts FirstName and LastName fields are both varchar(64) utf8_general_ci. // The MySQL docs say that the "_ci" bit means comparisons will be case insensitive. So that should work fine. // No need for prepared SQL here, the names have already been checked. if (0 != m_database.Count("UserAccounts", "FirstName = '" + names[0] + "' AND LastName = '" + names[1] + "'")) errors.Add("Pick a different name."); else if (("create" == doit)) reply["str_response_string"] = accountCreationPage(fields, body); else { var fromAddress = new MailAddress(m_email_from, (string) ssi["grid"]); var toAddress = new MailAddress((string) fields["email"], (string) fields["name"]); using (var message = new MailMessage(fromAddress, toAddress) { Subject = "validation email", Body = "Should be a linky around here somewhere." }) { m_smtp.Send(message); } reply["str_response_string"] = loggedOnPage(fields, body); } } else deleteSession(toke_n_munchie.ToString(), doit, headers, ref fields, ref replyHeaders); } else if ("cancel" == doit) { deleteSession(toke_n_munchie.ToString(), doit, headers, ref fields, ref replyHeaders); errors.Add("Cancelled."); } else if ("list" == doit) { // TODO - should check if the user is a god before allowing this. List< Hashtable > rows = m_database.Select("UserAccounts", "CONCAT(FirstName,' ',LastName) as Name,UserTitle as Title,UserLevel as Level,UserFlags as Flags,PrincipalID as UUID", "", "Name"); reply["str_response_string"] = "member accounts" + table(rows, new string[5] {"Name", "Title", "Level", "Flags", "UUID"}, "member accounts", "account.html?doit=edit&token=" + fields["toke_n_munchie"].ToString(), "UUID") + "

" + button("my account") + "

"; } else if ("login" == doit) { if (0 != errors.Count) deleteSession(toke_n_munchie.ToString(), doit, headers, ref fields, ref replyHeaders); else reply["str_response_string"] = loggedOnPage(fields, body); } else reply["str_response_string"] = loggedOnPage(fields, body); */ // } // else // Not one of our dynamic pages. // { m_log.ErrorFormat("[WEB SERVICE]: No such POST target {0}.", path); reply["int_response_code"] = 404; reply["content_type"] = "text/html"; reply["str_response_string"] = "404 Unknown page" + "404 error, can't find the " + reqpath + " page.

 

"; // } } else // Not one of our handled methods. { m_log.ErrorFormat("[WEB SERVICE]: UNKNOWN method {0} path {1}.", method, reqpath); reply["int_response_code"] = 404; reply["content_type"] = "text/html"; reply["str_response_string"] = "Unknown method" + "HUH! For " + reqpath + " page.

 

"; } reply["headers"] = replyHeaders; if (0 != errors.Count) { string b = ""; foreach (string e in errors) b = b + "

" + e + "

"; reply["str_response_string"] = loginPage(fields, b); } if ("HEAD" == method) { reply.Remove("bin_response_data"); reply.Remove("str_response_string"); } return reply; } // Poor mans Bobby Tables protection. private string bobbyTables(string n, string v) { if ((0 != String.Compare("password", n)) && (0 != String.Compare("psswrd", n))) { v = v.Replace("'", "_"); v = v.Replace("\"", "_"); v = v.Replace(";", "_"); v = v.Replace("(", "_"); v = v.Replace(")", "_"); } return v; } private string newSession(string doit, Hashtable headers, ref Hashtable fields, ref Hashtable replyHeaders) { // This is a little over the top, but apparently best practices. string toke_n_munchie = Guid.NewGuid().ToString(); string salt = Util.Md5Hash(UUID.Random().ToString()); string hash = Util.Md5Hash(Util.Md5Hash(toke_n_munchie) + ":" + salt); Hashtable auth = new Hashtable(); auth["toke_n_munchie"] = toke_n_munchie; auth["salt"] = salt; auth["hash"] = hash; auth["IP"] = headers["remote_addr"].ToString(); auth["time"] = DateTime.Now; auth["UUID"] = UUID.Zero; m_auth[hash] = auth; // For some odd reason, __Host- only works if "Path=/", and if no "Domain=", per the spec. //replyHeaders["Set-Cookie"] = "__Host-toke_n_munchie=" + toke_n_munchie + "; HttpOnly; Path=/web/; SameSite=Strict; Secure;"; replyHeaders["Set-Cookie"] = "toke_n_munchie=" + hash + "; HttpOnly; Path=/web/; SameSite=Strict; Secure;"; fields["toke_n_munchie"] = hash; toke_n_munchie = hash; m_log.InfoFormat("[WEB SERVICE]: {0} New session {1} - {2}.", headers["remote_addr"].ToString(), toke_n_munchie, doit); return toke_n_munchie; } private void deleteSession(string session, string doit, Hashtable headers, ref Hashtable fields, ref Hashtable replyHeaders) { m_log.InfoFormat("[WEB SERVICE]: {0} Deleted session {1} - {2}.", headers["remote_addr"].ToString(), session, doit); m_auth.Remove(session); fields.Remove("toke_n_munchie"); replyHeaders["Set-Cookie"] = "toke_n_munchie=\"\"; HttpOnly; Path=/web/; SameSite=Strict; Secure; expires=Thu, 01 Jan 1970 00:00:00 GMT;"; } private void validateEmail(Hashtable fields, ref List errors) { Regex rgxEmail = new Regex("^.+@.+\\..+$"); if ((null == fields["email"]) || ("" == fields["email"].ToString())) errors.Add("Please supply an email address."); else if (!rgxEmail.IsMatch(fields["email"].ToString())) errors.Add("Please supply a proper email address."); else if (!Uri.IsWellFormedUriString("mailto:" + fields["email"].ToString(), System.UriKind.Absolute)) errors.Add("Please supply a valid email address."); // Actually lookup the domain name, looking for any sort of record. string e = fields["email"].ToString().Split('@')[1]; IPHostEntry ip = null; try { ip = Dns.GetHostEntry(e); } catch(Exception) { } if (null == ip) errors.Add("Can't find that email server, try a different email address."); } private string[] validateName(bool godCheck, Hashtable fields, ref List errors) { Regex rgxName = new Regex("^[a-zA-Z0-9]+$"); string[] names = {"", ""}; if ((null == fields["name"]) || ("" == fields["name"].ToString())) errors.Add("Please supply an account name."); else { names = fields["name"].ToString().Split(' '); if (2 != names.Length) errors.Add("Names have to be two words."); // SL docs say 31 characters each for first and last name. UserAccounts table is varchar(64) each. userinfo has varchar(50) for the combined name. // The userinfo table seems to be obsolete. // Singularity at least limits the total name to 64. // I can't find any limitations on characters allowed, but I only ever see letters and digits used. Case is stored, but not significant. // OpenSims "create user" console command doesn't sanitize it at all, even crashing on some names. else { if ((31 < names[0].Length) || (31 < names[1].Length)) errors.Add("First and last names are limited to 31 letters each."); if ((!rgxName.IsMatch(names[0])) || (!rgxName.IsMatch(names[1]))) errors.Add("First and last names are limited to letters and digits."); if (godCheck) { // Check and disallow god names, those are done in the console. bool f = false; try { if (null != m_fullNames[names[0] + " " + names[1]]) { f = true; errors.Add("Pick another name."); } } catch (Exception) { } if (!f) { try { if (null != m_firstNames[names[0]]) errors.Add("Pick another first name."); } catch (Exception) { } try { if (null != m_firstNames[names[1]]) errors.Add("Pick another last name."); } catch (Exception) { } } } } } return names; } private void validatePassword(Hashtable fields, ref List errors) { if ((null == fields["password"]) || ("" == fields["password"].ToString())) errors.Add("Please supply a password."); else if ((null == fields["psswrd"]) || ("" == fields["psswrd"].ToString())) errors.Add("Please supply a password."); else if (0 != String.Compare(fields["psswrd"].ToString(), fields["password"].ToString())) errors.Add("Passwords are not the same."); } private string loginPage(Hashtable fields, string message) { string n = ""; string e = ""; if (null != fields) { n = fields["name"].ToString(); e = fields["email"].ToString(); } return header(ssi["grid"] + " account") + form("account.html", "", text("text", "name", "name", n, 63, true) + text("email", "email", "email", e, 254, false) + text("password", "password", "password", "", 0, true) + "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters." + button("create") + button("login") ) + "

" + message + "

" + footer(); } private string accountCreationPage(Hashtable fields, string message) { return header(ssi["grid"] + " account") + "

Creating " + ssi["grid"] + " account for " + fields["name"].ToString() + "

" + form("account.html", fields["toke_n_munchie"].ToString(), hidden("name", fields["name"].ToString()) + hidden("psswrd", fields["password"].ToString()) + text("email", "An email will be sent to", "email", fields["email"].ToString(), 254, false) + " to validate it, please double check this." + text("password", "Re-enter your password", "password", "", 0, false) + "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters." + button("confirm") + button("cancel") ) + message + footer(); } private string loggedOnPage(Hashtable fields, string message) { return header(ssi["grid"] + " account") + "

" + ssi["grid"] + " account for " + fields["name"].ToString() + "

" + form("account.html", fields["toke_n_munchie"].ToString(), hidden("name", fields["name"].ToString()) // + hidden("UUID", fields["UUID"].ToString()) + text("email", "email", "email", fields["email"].ToString(), 254, true) + text("password", "password", "password", "", 0, false) + "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters." // + text("title", "text", "title", fields["title"].ToString(), 64, false) + select("type", "type", option("", false) + option("approved", true) + option("disabled", false) + option("god", false) ) + button("delete") + button("list") + button("logout") // + button("read") + button("update") ) + message + footer(); } private string header(string title) { return "\n \n " + title + "\n \n \n"; } private string table(List< Hashtable > rows, string[] fields, string caption, string URL, string id) { string tbl = ""; bool head = true; string address = ""; string addrend = ""; foreach (Hashtable row in rows) { if (0 == fields.Length) { int c = 0; foreach (DictionaryEntry r in row) c++; fields = new string[c]; c = 0; foreach (DictionaryEntry r in row) fields[c++] = (string) r.Key; } string line = ""; address = ""; if ("" != URL) { address = ""; if (head) { foreach (string s in fields) line = line + ""; tbl = tbl + line + "\n"; head = false; } line = ""; foreach (string s in fields) { if (s == id) line = line + ""; else line = line + ""; } tbl = tbl + line + "\n"; } return tbl + "
" + caption + "
" + s + "
" + address + row[s] + addrend + "" + row[s] + "
"; } private string form(string action, string token, string form) { return "
\n" + hidden("toke_n_munchie", token) + form + "
\n"; } private string hidden(string name, string val) { return " \n"; } private string text(string type, string title, string name, string val, int max, bool required) { string extra = ""; if (0 < max) extra = extra + " maxlength=\"" + max.ToString() + "\""; if (required) extra = extra + " required"; if ("" != val) val = "value=\"" + val + "\""; return "

" + title + " :

\n"; } private string select(string title, string name, string options) { return "

" + title + " : \n \n

\n"; } private string option(string title, bool selected) { string sel = ""; if (selected) sel = " selected"; return " \n"; } private string button(string title) { return " \n"; } private string footer() { return " \n\n"; } } }