screwturn-4/Core/Tools.cs

577 lines
22 KiB
C#

using System;
using System.Configuration;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Web;
using System.Web.Security;
using System.Globalization;
using ScrewTurn.Wiki.PluginFramework;
using System.Reflection;
using System.Net;
namespace ScrewTurn.Wiki {
/// <summary>
/// Contains useful Tools.
/// </summary>
public static class Tools {
/// <summary>
/// Gets all the included files for the HTML Head, such as CSS, JavaScript and Icon pluginAssemblies, for a namespace.
/// </summary>
/// <param name="nspace">The namespace (<c>null</c> for the root).</param>
/// <returns>The includes.</returns>
public static string GetIncludes(string nspace) {
string theme = Settings.GetTheme(nspace);
string themePath = Settings.GetThemePath(nspace);
StringBuilder result = new StringBuilder(300);
string[] css = Directory.GetFiles(Settings.ThemesDirectory + theme, "*.css");
string firstChunk;
for(int i = 0; i < css.Length; i++) {
if(Path.GetFileName(css[i]).IndexOf("_") != -1) {
firstChunk = Path.GetFileName(css[i]).Substring(0, Path.GetFileName(css[i]).IndexOf("_")).ToLower(CultureInfo.CurrentCulture);
if(firstChunk.Equals("screen") || firstChunk.Equals("print") || firstChunk.Equals("all") ||
firstChunk.Equals("aural") || firstChunk.Equals("braille") || firstChunk.Equals("embossed") ||
firstChunk.Equals("handheld") || firstChunk.Equals("projection") || firstChunk.Equals("tty") || firstChunk.Equals("tv")) {
result.Append(@"<link rel=""stylesheet"" media=""" + firstChunk + @""" href=""" + themePath + Path.GetFileName(css[i]) + @""" type=""text/css"" />" + "\n");
}
else {
result.Append(@"<link rel=""stylesheet"" href=""" + themePath + Path.GetFileName(css[i]) + @""" type=""text/css"" />" + "\n");
}
}
else {
result.Append(@"<link rel=""stylesheet"" href=""" + themePath + Path.GetFileName(css[i]) + @""" type=""text/css"" />" + "\n");
}
}
string customEditorCss = Path.Combine(Settings.ThemesDirectory, theme);
customEditorCss = Path.Combine(customEditorCss, "Editor.css");
if(File.Exists(customEditorCss)) result.AppendFormat(@"<link rel=""stylesheet"" href=""Themes/{0}/Editor.css"" type=""text/css"" />" + "\n", theme);
else result.Append(@"<link rel=""stylesheet"" href=""Themes/Editor.css"" type=""text/css"" />" + "\n");
// OpenSearch
result.AppendFormat(@"<link rel=""search"" href=""Search.aspx?OpenSearch=1"" type=""application/opensearchdescription+xml"" title=""{1}"" />",
Settings.MainUrl, Settings.WikiTitle + " - Search");
string[] js = Directory.GetFiles(Settings.ThemesDirectory + theme, "*.js");
for(int i = 0; i < js.Length; i++) {
result.Append(@"<script src=""" + themePath + Path.GetFileName(js[i]) + @""" type=""text/javascript""></script>" + "\n");
}
string[] icons = Directory.GetFiles(Settings.ThemesDirectory + theme, "Icon.*");
if(icons.Length > 0) {
result.Append(@"<link rel=""shortcut icon"" href=""" + themePath + Path.GetFileName(icons[0]) + @""" type=""");
switch(Path.GetExtension(icons[0]).ToLowerInvariant()) {
case ".ico":
result.Append("image/x-icon");
break;
case ".gif":
result.Append("image/gif");
break;
case ".png":
result.Append("image/png");
break;
}
result.Append(@""" />" + "\n");
}
result.Append(GetJavaScriptIncludes());
// Include HTML Head
result.Append(Settings.Provider.GetMetaDataItem(MetaDataItem.HtmlHead, nspace));
return result.ToString();
}
/// <summary>
/// Gets all the JavaScript files to include.
/// </summary>
/// <returns>The JS files.</returns>
public static string GetJavaScriptIncludes() {
StringBuilder buffer = new StringBuilder(100);
foreach(string js in Directory.GetFiles(Settings.JsDirectory, "*.js")) {
buffer.Append(@"<script type=""text/javascript"" src=""" + Settings.JsDirectoryName + "/" + Path.GetFileName(js) + @"""></script>" + "\n");
}
return buffer.ToString();
}
/// <summary>
/// Converts a byte number into a string, formatted using KB, MB or GB.
/// </summary>
/// <param name="bytes">The # of bytes.</param>
/// <returns>The formatted string.</returns>
public static string BytesToString(long bytes) {
if(bytes < 1024) return bytes.ToString() + " B";
else if(bytes < 1048576) return string.Format("{0:N2} KB", (float)bytes / 1024F);
else if(bytes < 1073741824) return string.Format("{0:N2} MB", (float)bytes / 1048576F);
else return string.Format("{0:N2} GB", (float)bytes / 1073741824F);
}
/// <summary>
/// Computes the Disk Space Usage of a directory.
/// </summary>
/// <param name="dir">The directory.</param>
/// <returns>The used Disk Space, in bytes.</returns>
public static long DiskUsage(string dir) {
string[] files = Directory.GetFiles(dir);
string[] directories = Directory.GetDirectories(dir);
long result = 0;
FileInfo file;
for(int i = 0; i < files.Length; i++) {
file = new FileInfo(files[i]);
result += file.Length;
}
for(int i = 0; i < directories.Length; i++) {
result += DiskUsage(directories[i]);
}
return result;
}
/// <summary>
/// Generates the standard 5-digit Page Version string.
/// </summary>
/// <param name="version">The Page version.</param>
/// <returns>The 5-digit Version string.</returns>
public static string GetVersionString(int version) {
string result = version.ToString();
int len = result.Length;
for(int i = 0; i < 5 - len; i++) {
result = "0" + result;
}
return result;
}
/// <summary>
/// Gets the available Themes.
/// </summary>
public static string[] AvailableThemes {
get {
string[] dirs = Directory.GetDirectories(Settings.ThemesDirectory);
string[] res = new string[dirs.Length];
for(int i = 0; i < dirs.Length; i++) {
//if(dirs[i].EndsWith("\\")) dirs[i] = dirs[i].Substring(0, dirs[i].Length - 1);
dirs[i] = dirs[i].TrimEnd(Path.DirectorySeparatorChar);
res[i] = dirs[i].Substring(dirs[i].LastIndexOf(Path.DirectorySeparatorChar) + 1);
}
return res;
}
}
/// <summary>
/// Gets the available Cultures.
/// </summary>
public static string[] AvailableCultures {
get {
// It seems, at least in VS 2008, that for Precompiled Web Sites, the GlobalResources pluginAssemblies that are not the
// default resource (Culture=neutral), get sorted into subdirectories named by the Culture Info name. Every
// assembly in these directories is called "ScrewTurn.Wiki.resources.dll"
// I'm sure it's possible to just use the subdirectory names in the bin directory to get the culture info names,
// however, I'm not sure what other things might get tossed in there by the compiler now or in the future.
// That's why I'm specifically going for the App_GlobalResources.resources.dlls.
// So, get all of the App_GlobalResources.resources.dll pluginAssemblies from bin and recurse subdirectories
string[] dllFiles = Directory.GetFiles(Path.Combine(Settings.RootDirectory, "bin"), "ScrewTurn.Wiki.resources.dll", SearchOption.AllDirectories);
// List to collect constructed culture names
List<string> cultureNames = new List<string>();
// Manually add en-US culture
CultureInfo enCI = new CultureInfo("en-US");
cultureNames.Add(enCI.Name + "|" + UppercaseInitial(enCI.NativeName) + " - " + enCI.EnglishName);
// For every file we find
// List format: xx-ZZ|Native name (English name)
foreach(string s in dllFiles) {
try {
// Load a reflection only assembly from the filename
Assembly asm = Assembly.ReflectionOnlyLoadFrom(s);
// string for destructive parsing of the assembly's full name
// Which, btw, looks something like this
// App_GlobalResources.resources, Version=0.0.0.0, Culture=zh-cn, PublicKeyToken=null
string fullName = asm.FullName;
// Find the Culture= attribute
int find = fullName.IndexOf("Culture=");
// Remove it and everything prior
fullName = fullName.Substring(find + 8);
// Find the trailing comma
find = fullName.IndexOf(',');
// Remove it and everything after
fullName = fullName.Substring(0, find);
// Fullname should now be the culture info name and we can instantiate the CultureInfo class from it
CultureInfo ci = new CultureInfo(fullName);
// StringBuilders
StringBuilder sb = new StringBuilder();
sb.Append(ci.Name);
sb.Append("|");
sb.Append(UppercaseInitial(ci.NativeName));
sb.Append(" - ");
sb.Append(ci.EnglishName);
// Add the newly constructed Culture string
cultureNames.Add(sb.ToString());
}
catch(Exception ex) {
Log.LogEntry("Error parsing culture info from " + s + Environment.NewLine + ex.Message, EntryType.Error, Log.SystemUsername);
}
}
// If for whatever reason every one fails, this will return a 1 element array with the en-US info.
cultureNames.Sort();
return cultureNames.ToArray();
}
}
private static string UppercaseInitial(string value) {
if(value.Length > 0) {
return value[0].ToString().ToUpper(CultureInfo.CurrentCulture) + value.Substring(1);
}
else return "";
}
/// <summary>
/// Computes the Hash of a Username, mixing it with other data, in order to avoid illegal Account activations.
/// </summary>
/// <param name="username">The Username.</param>
/// <param name="email">The email.</param>
/// <param name="dateTime">The date/time.</param>
/// <returns>The secured Hash of the Username.</returns>
public static string ComputeSecurityHash(string username, string email, DateTime dateTime) {
return Hash.ComputeSecurityHash(username, email, dateTime, Settings.MasterPassword);
}
/// <summary>
/// Escapes bad characters in a string (pipes and \n).
/// </summary>
/// <param name="input">The input string.</param>
/// <returns>The escaped string.</returns>
public static string EscapeString(string input) {
StringBuilder sb = new StringBuilder(input);
sb.Replace("\r", "");
sb.Replace("\n", "%0A");
sb.Replace("|", "%7C");
return sb.ToString();
}
/// <summary>
/// Unescapes bad characters in a string (pipes and \n).
/// </summary>
/// <param name="input">The input string.</param>
/// <returns>The unescaped string.</returns>
public static string UnescapeString(string input) {
StringBuilder sb = new StringBuilder(input);
sb.Replace("%7C", "|");
sb.Replace("%0A", "\n");
return sb.ToString();
}
/// <summary>
/// Generates a random 10-char Password.
/// </summary>
/// <returns>The Password.</returns>
public static string GenerateRandomPassword() {
Random r = new Random();
string password = "";
for(int i = 0; i < 10; i++) {
if(i % 2 == 0)
password += ((char)r.Next(65, 91)).ToString(); // Uppercase letter
else password += ((char)r.Next(97, 123)).ToString(); // Lowercase letter
}
return password;
}
/// <summary>
/// Gets the approximate System Uptime.
/// </summary>
public static TimeSpan SystemUptime {
get {
int t = Environment.TickCount;
if(t < 0) t = t + int.MaxValue;
t = t / 1000;
return TimeSpan.FromSeconds(t);
}
}
/// <summary>
/// Converts a Time Span to string.
/// </summary>
/// <param name="span">The Time Span.</param>
/// <returns>The string.</returns>
public static string TimeSpanToString(TimeSpan span) {
string result = span.Days.ToString() + "d ";
result += span.Hours.ToString() + "h ";
result += span.Minutes.ToString() + "m ";
result += span.Seconds.ToString() + "s";
return result;
}
/// <summary>
/// Executes URL-encoding, avoiding to use '+' for spaces.
/// </summary>
/// <param name="input">The input string.</param>
/// <returns>The encoded string.</returns>
public static string UrlEncode(string input) {
if(HttpContext.Current != null && HttpContext.Current.Server != null) return HttpContext.Current.Server.UrlEncode(input).Replace("+", "%20");
else {
Log.LogEntry("HttpContext.Current or HttpContext.Current.Server were null (Tools.UrlEncode)", EntryType.Warning, Log.SystemUsername);
return input;
}
}
/// <summary>
/// Executes URL-decoding, replacing spaces as processed by UrlEncode.
/// </summary>
/// <param name="input">The input string.</param>
/// <returns>The decoded string.</returns>
public static string UrlDecode(string input) {
return HttpContext.Current.Server.UrlDecode(input.Replace("%20", " "));
}
/// <summary>
/// Removes all HTML tags from a text.
/// </summary>
/// <param name="html">The input HTML.</param>
/// <returns>The extracted plain text.</returns>
public static string RemoveHtmlMarkup(string html) {
StringBuilder sb = new StringBuilder(System.Text.RegularExpressions.Regex.Replace(html, "<[^>]*>", " "));
sb.Replace("&nbsp;", " ");
sb.Replace(" ", " ");
return sb.ToString();
}
/// <summary>
/// Extracts the directory name from a path used in the Files Storage Providers.
/// </summary>
/// <param name="path">The path, for example '/folder/blah/'.</param>
/// <returns>The directory name, for example 'blah'.</returns>
public static string ExtractDirectoryName(string path) {
path = path.Trim('/');
int idx = path.LastIndexOf("/");
return idx != -1 ? path.Substring(idx + 1) : path;
}
/// <summary>
/// Detects the correct <see cref="T:PageInfo" /> object associated to the current page using the <b>Page</b> and <b>NS</b> parameters in the query string.
/// </summary>
/// <param name="loadDefault"><c>true</c> to load the default page of the specified namespace when <b>Page</b> is not specified, <c>false</c> otherwise.</param>
/// <returns>If <b>Page</b> is specified and exists, the correct <see cref="T:PageInfo" />, otherwise <c>null</c> if <b>loadDefault</b> is <c>false</c>,
/// or the <see cref="T:PageInfo" /> object representing the default page of the specified namespace if <b>loadDefault</b> is <c>true</c>.</returns>
public static PageInfo DetectCurrentPageInfo(bool loadDefault) {
string nspace = HttpContext.Current.Request["NS"];
NamespaceInfo nsinfo = nspace != null ? Pages.FindNamespace(nspace) : null;
string page = HttpContext.Current.Request["Page"];
if(string.IsNullOrEmpty(page)) {
if(loadDefault) {
if(nsinfo == null) page = Settings.DefaultPage;
else page = nsinfo.DefaultPage != null ? nsinfo.DefaultPage.FullName : "";
}
else return null;
}
string fullName = null;
if(!page.StartsWith(nspace + ".")) fullName = nspace + "." + page;
else fullName = page;
fullName = fullName.Trim('.');
return Pages.FindPage(fullName);
}
/// <summary>
/// Detects the full name of the current page using the <b>Page</b> and <b>NS</b> parameters in the query string.
/// </summary>
/// <returns>The full name of the page, regardless of the existence of the page.</returns>
public static string DetectCurrentFullName() {
string nspace = HttpContext.Current.Request["NS"] != null ? HttpContext.Current.Request["NS"] : "";
string page = HttpContext.Current.Request["Page"] != null ? HttpContext.Current.Request["Page"] : "";
string fullName = null;
if(!page.StartsWith(nspace + ".")) fullName = nspace + "." + page;
else fullName = page;
return fullName.Trim('.');
}
/// <summary>
/// Detects the correct <see cref="T:NamespaceInfo" /> object associated to the current namespace using the <b>NS</b> parameter in the query string.
/// </summary>
/// <returns>The correct <see cref="T:NamespaceInfo" /> object, or <c>null</c>.</returns>
public static NamespaceInfo DetectCurrentNamespaceInfo() {
string nspace = HttpContext.Current.Request["NS"];
NamespaceInfo nsinfo = nspace != null ? Pages.FindNamespace(nspace) : null;
return nsinfo;
}
/// <summary>
/// Detects the name of the current namespace using the <b>NS</b> parameter in the query string.
/// </summary>
/// <returns>The name of the namespace, or an empty string.</returns>
public static string DetectCurrentNamespace() {
return HttpContext.Current.Request["NS"] != null ? HttpContext.Current.Request["NS"] : "";
}
/// <summary>
/// Gets the message ID for HTML anchors.
/// </summary>
/// <param name="messageDateTime">The message date/time.</param>
/// <returns>The ID.</returns>
public static string GetMessageIdForAnchor(DateTime messageDateTime) {
return "MSG_" + messageDateTime.ToString("yyyyMMddHHmmss");
}
/// <summary>
/// Gets the name of a file's directory.
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>The name of the item.</returns>
public static string GetDirectoryName(string filename) {
if(filename != null) {
int index = filename.LastIndexOf("/");
if(index > 0) {
string directoryName = filename.Substring(0, index + 1);
if(!directoryName.StartsWith("/")) directoryName = "/" + directoryName;
return directoryName;
}
}
// Assume to navigate in the root directory
return "/";
}
/// <summary>
/// Gets the update status of a component.
/// </summary>
/// <param name="url">The version file URL.</param>
/// <param name="currentVersion">The current version.</param>
/// <param name="newVersion">The new version, if any.</param>
/// <param name="newAssemblyUrl">The URL of the new assembly, if applicable and available.</param>
/// <returns>The update status.</returns>
/// <remarks>This method only works in Full Trust.</remarks>
public static UpdateStatus GetUpdateStatus(string url, string currentVersion, out string newVersion, out string newAssemblyUrl) {
// TODO: Verify usage of WebPermission class
// http://msdn.microsoft.com/en-us/library/system.net.webpermission.aspx
string urlHash = "UpdUrlCache-" + url.GetHashCode().ToString();
try {
string ver = null;
if(HttpContext.Current != null) {
ver = HttpContext.Current.Cache[urlHash] as string;
}
if(ver == null) {
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
req.AllowAutoRedirect = true;
HttpWebResponse res = (HttpWebResponse)req.GetResponse();
if(res.StatusCode != HttpStatusCode.OK) {
newVersion = null;
newAssemblyUrl = null;
return UpdateStatus.Error;
}
StreamReader sr = new StreamReader(res.GetResponseStream());
ver = sr.ReadToEnd();
sr.Close();
if(HttpContext.Current != null) {
HttpContext.Current.Cache.Add(urlHash, ver, null, DateTime.Now.AddMinutes(5),
System.Web.Caching.Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.Normal, null);
}
}
string[] lines = ver.Replace("\r", "").Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
if(lines.Length == 0) {
newVersion = null;
newAssemblyUrl = null;
return UpdateStatus.Error;
}
string[] versions = lines[0].Split('|');
bool upToDate = false;
for(int i = 0; i < versions.Length; i++) {
ver = versions[i];
if(versions[i].Equals(currentVersion)) {
if(i == versions.Length - 1) upToDate = true;
else upToDate = false;
ver = versions[versions.Length - 1];
break;
}
}
if(upToDate) {
newVersion = null;
newAssemblyUrl = null;
return UpdateStatus.UpToDate;
}
else {
newVersion = ver;
if(lines.Length == 2) newAssemblyUrl = lines[1];
else newAssemblyUrl = null;
return UpdateStatus.NewVersionFound;
}
}
catch(Exception) {
if(HttpContext.Current != null) {
HttpContext.Current.Cache.Add(urlHash, "", null, DateTime.Now.AddMinutes(5),
System.Web.Caching.Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.Normal, null);
}
newVersion = null;
newAssemblyUrl = null;
return UpdateStatus.Error;
}
}
/// <summary>
/// Computes the hash value of a string that is value across application instances and versions.
/// </summary>
/// <param name="value">The string to compute the hash of.</param>
/// <returns>The hash value.</returns>
public static uint HashDocumentNameForTemporaryIndex(string value) {
if(value == null) throw new ArgumentNullException("value");
// sdbm algorithm, borrowed from http://www.cse.yorku.ca/~oz/hash.html
uint hash = 0;
foreach(char c in value) {
// hash(i) = hash(i - 1) * 65599 + str[i]
hash = c + (hash << 6) + (hash << 16) - hash;
}
return hash;
}
}
/// <summary>
/// Lists legal update statuses.
/// </summary>
public enum UpdateStatus {
/// <summary>
/// Error while retrieving version information.
/// </summary>
Error,
/// <summary>
/// The component is up-to-date.
/// </summary>
UpToDate,
/// <summary>
/// A new version was found.
/// </summary>
NewVersionFound
}
}