540 lines
21 KiB
C#
540 lines
21 KiB
C#
using System;
|
|
using System.Collections.Specialized;
|
|
using System.Configuration;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Mail;
|
|
using System.Text.RegularExpressions;
|
|
using System.Web;
|
|
using System.Web.Security;
|
|
using RestSharp;
|
|
using RestSharp.Deserializers;
|
|
using umbraco;
|
|
using umbraco.BusinessLogic;
|
|
using umbraco.editorControls.MultiNodeTreePicker;
|
|
using umbraco.NodeFactory;
|
|
using Umbraco.Core.Logging;
|
|
using Umbraco.Core.Models;
|
|
using Umbraco.Core.Services;
|
|
using Umbraco.Web;
|
|
using Member = umbraco.cms.businesslogic.member.Member;
|
|
using MemberGroup = umbraco.cms.businesslogic.member.MemberGroup;
|
|
|
|
namespace uForum.Library
|
|
{
|
|
public class Utils
|
|
{
|
|
private const string SpamMemberGroupName = "potentialspam";
|
|
private static readonly int PotentialSpammerThreshold = int.Parse(ConfigurationManager.AppSettings["PotentialSpammerThreshold"]);
|
|
private static readonly int SpamBlockThreshold = int.Parse(ConfigurationManager.AppSettings["SpamBlockThreshold"]);
|
|
private const int ReputationThreshold = 30;
|
|
|
|
/// <summary>
|
|
/// sanitize any potentially dangerous tags from the provided raw HTML input using
|
|
/// a whitelist based approach, leaving the "safe" HTML tags
|
|
/// </summary>
|
|
public static string Sanitize(string html)
|
|
{
|
|
html = html.Replace("[code]", "<pre>");
|
|
html = html.Replace("[/code]", "</pre>");
|
|
|
|
var cleanHtml = CleanInvalidXmlChars(html);
|
|
|
|
// Add links to URLs that aren't "properly" linked in a markdown way
|
|
var regex = new Regex(@"(^|\s|>|;)(https?|ftp)(:\/\/[-A-Z0-9+&@#\/%?=~_|\[\]\(\)!:,\.;]*[-A-Z0-9+&@#\/%=~_|\[\]])($|\W)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
|
|
|
var linkedHtml = regex.Replace(cleanHtml, "$1<a href=\"$2$3\">$2$3</a>$4").Replace("href=\"www", "href=\"http://www");
|
|
|
|
var scriptRegex = new Regex("<script.*?</script>", RegexOptions.Singleline | RegexOptions.IgnoreCase);
|
|
var scriptRegexMatches = scriptRegex.Matches(linkedHtml);
|
|
for (var i = 0; i < scriptRegexMatches.Count; i++)
|
|
{
|
|
linkedHtml = linkedHtml.Replace(scriptRegexMatches[i].Value, string.Format("<pre>{0}</pre>", HttpContext.Current.Server.HtmlEncode(scriptRegexMatches[i].Value)));
|
|
}
|
|
|
|
var iframeRegex = new Regex("<iframe.*?</iframe>", RegexOptions.Singleline | RegexOptions.IgnoreCase);
|
|
var iframeMatches = iframeRegex.Matches(linkedHtml);
|
|
for (var i = 0; i < iframeMatches.Count; i++)
|
|
{
|
|
linkedHtml = linkedHtml.Replace(iframeMatches[i].Value, string.Format("<pre>{0}</pre>", HttpContext.Current.Server.HtmlEncode(iframeMatches[i].Value)));
|
|
}
|
|
|
|
return linkedHtml;
|
|
}
|
|
|
|
public static string CleanInvalidXmlChars(string text)
|
|
{
|
|
// From xml spec valid chars:
|
|
// #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
|
|
// any Unicode character, excluding the surrogate blocks, FFFE, and FFFF.
|
|
const string re = @"[^\x09\x0A\x0D\x20-\xD7FF\xE000-\xFFFD\x10000-x10FFFF]";
|
|
return Regex.Replace(text, re, "");
|
|
}
|
|
|
|
public static Member GetMember(int id)
|
|
{
|
|
try
|
|
{
|
|
return Member.GetMemberFromCache(id) ?? new Member(id);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Log.Add(LogTypes.Error, 0, string.Format("Could not get member {0} from the cache nor from the database - Exception: {1} {2} {3}", id, exception.Message, exception.StackTrace, exception.InnerException));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static bool IsModerator()
|
|
{
|
|
var isModerator = false;
|
|
|
|
var currentMemberId = HttpContext.Current.User.Identity.IsAuthenticated ? (int)Membership.GetUser().ProviderUserKey : 0;
|
|
|
|
if (currentMemberId != 0)
|
|
{
|
|
var moderatorRoles = new[] { "admin", "HQ", "Core", "MVP" };
|
|
|
|
isModerator = moderatorRoles.Any(moderatorRole => IsMemberInGroup(moderatorRole, currentMemberId));
|
|
}
|
|
|
|
return isModerator;
|
|
}
|
|
|
|
public static bool IsMemberInGroup(string GroupName, int memberid)
|
|
{
|
|
Member m;
|
|
try
|
|
{
|
|
m = Utils.GetMember(memberid);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Add(LogTypes.Error, new User(0), -1, string.Format("Utills.GetMember({0}) failed - {1} {2} {3}", memberid, ex.Message, ex.StackTrace, ex.InnerException));
|
|
return false;
|
|
}
|
|
|
|
foreach (MemberGroup mg in m.Groups.Values)
|
|
{
|
|
if (mg.Text == GroupName)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static bool IsInGroup(string GroupName)
|
|
{
|
|
|
|
if (umbraco.library.IsLoggedOn())
|
|
return IsMemberInGroup(GroupName, Member.CurrentMemberId());
|
|
else
|
|
return false;
|
|
}
|
|
|
|
public static void AddMemberToPotentialSpamGroup(Member member)
|
|
{
|
|
var memberGroup = MemberGroup.GetByName(SpamMemberGroupName);
|
|
if (memberGroup == null)
|
|
MemberGroup.MakeNew(SpamMemberGroupName, new User(0));
|
|
|
|
memberGroup = MemberGroup.GetByName(SpamMemberGroupName);
|
|
member.AddGroup(memberGroup.Id);
|
|
}
|
|
|
|
public static void RemoveMemberFromPotentialSpamGroup(Member member)
|
|
{
|
|
var memberGroup = MemberGroup.GetByName(SpamMemberGroupName);
|
|
if (memberGroup == null)
|
|
MemberGroup.MakeNew(SpamMemberGroupName, new User(0));
|
|
|
|
memberGroup = MemberGroup.GetByName(SpamMemberGroupName);
|
|
member.RemoveGroup(memberGroup.Id);
|
|
}
|
|
|
|
public static SpamResult CheckForSpam(IMember member)
|
|
{
|
|
try
|
|
{
|
|
// Already blocked, nothing left to do here
|
|
if (member.GetValue<bool>("blocked"))
|
|
{
|
|
return new SpamResult
|
|
{
|
|
MemberId = member.Id,
|
|
Name = member.Name,
|
|
Blocked = true
|
|
};
|
|
}
|
|
|
|
// If reputation is > ReputationThreshold they've got enough karma, spammers never get that far
|
|
var reputation = member.GetValue<int>("reputationTotal");
|
|
if (reputation > ReputationThreshold)
|
|
return null;
|
|
|
|
// If they're already marked as suspicious then no need to process again
|
|
if (Roles.IsUserInRole(member.Username, SpamMemberGroupName))
|
|
{
|
|
return new SpamResult
|
|
{
|
|
MemberId = member.Id,
|
|
Name = member.Name,
|
|
AlreadyInSpamRole = true
|
|
};
|
|
}
|
|
|
|
var spammer = CheckForSpam(member.Email, member.Name, false);
|
|
|
|
if (spammer != null && spammer.TotalScore > PotentialSpammerThreshold)
|
|
{
|
|
var memberService = UmbracoContext.Current.Application.Services.MemberService;
|
|
memberService.AssignRole(member.Id, SpamMemberGroupName);
|
|
|
|
spammer.MemberId = member.Id;
|
|
|
|
SendPotentialSpamMemberMail(spammer);
|
|
|
|
if (spammer.Blocked)
|
|
{
|
|
member.SetValue("blocked", true);
|
|
memberService.Save(member);
|
|
|
|
// If blocked, just redirect them to the home page where they'll get a message saying they're blocked
|
|
HttpContext.Current.Response.Redirect("/");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogHelper.Error<Utils>(string.Format("Error trying to CheckForSpam for member {0}", member.Email), ex);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static SpamResult CheckForSpam(string email, string name, bool sendMail)
|
|
{
|
|
try
|
|
{
|
|
var ipAddress = GetIpAddress();
|
|
var client = new RestClient("http://api.stopforumspam.org");
|
|
var request = new RestRequest(string.Format("api?ip={0}&email={1}&f=json", ipAddress, HttpUtility.UrlEncode(email)), Method.GET);
|
|
var response = client.Execute(request);
|
|
var jsonResult = new JsonDeserializer();
|
|
var spamCheckResult = jsonResult.Deserialize<SpamCheckResult>(response);
|
|
|
|
if (spamCheckResult.Success == 1)
|
|
{
|
|
var score = spamCheckResult.Ip.Confidence + spamCheckResult.Email.Confidence;
|
|
|
|
var blocked = score > SpamBlockThreshold;
|
|
if (score > PotentialSpammerThreshold)
|
|
{
|
|
var spammer = new SpamResult
|
|
{
|
|
Name = name,
|
|
Email = email,
|
|
Ip = ipAddress,
|
|
ScoreEmail = spamCheckResult.Email.Confidence.ToString(CultureInfo.InvariantCulture),
|
|
FrequencyEmail = spamCheckResult.Email.Frequency.ToString(CultureInfo.InvariantCulture),
|
|
ScoreIp = spamCheckResult.Ip.Confidence.ToString(CultureInfo.InvariantCulture),
|
|
FrequencyIp = spamCheckResult.Ip.Frequency.ToString(CultureInfo.InvariantCulture),
|
|
Blocked = blocked,
|
|
TotalScore = score
|
|
};
|
|
|
|
if (sendMail)
|
|
SendPotentialSpamMemberMail(spammer);
|
|
|
|
return spammer;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Log.Add(LogTypes.Error, -1, string.Format("Error checking stopforumspam.org {0}", spamCheckResult.Error));
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Add(LogTypes.Error, -1, string.Format("Error checking stopforumspam.org {0}", ex.Message + ex.StackTrace));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static void SendMemberSignupMail(IMember member)
|
|
{
|
|
try
|
|
{
|
|
var ipAddress = GetIpAddress();
|
|
var client = new RestClient("http://api.stopforumspam.org");
|
|
var request = new RestRequest(string.Format("api?ip={0}&email={1}&f=json", ipAddress, HttpUtility.UrlEncode(member.Email)), Method.GET);
|
|
var response = client.Execute(request);
|
|
var jsonResult = new JsonDeserializer();
|
|
var spamCheckResult = jsonResult.Deserialize<SpamCheckResult>(response);
|
|
|
|
var spammer = new SpamResult
|
|
{
|
|
MemberId = member.Id,
|
|
Name = member.Name,
|
|
Company = member.GetValue<string>("company"),
|
|
Bio = member.GetValue<string>("profileText"),
|
|
Email = member.Email,
|
|
Ip = ipAddress
|
|
};
|
|
|
|
if (spamCheckResult.Success == 1)
|
|
{
|
|
var score = spamCheckResult.Ip.Confidence + spamCheckResult.Email.Confidence;
|
|
var blocked = score > SpamBlockThreshold;
|
|
|
|
spammer.ScoreEmail = spamCheckResult.Email.Confidence.ToString(CultureInfo.InvariantCulture);
|
|
spammer.FrequencyEmail = spamCheckResult.Email.Frequency.ToString(CultureInfo.InvariantCulture);
|
|
spammer.ScoreIp = spamCheckResult.Ip.Confidence.ToString(CultureInfo.InvariantCulture);
|
|
spammer.FrequencyIp = spamCheckResult.Ip.Frequency.ToString(CultureInfo.InvariantCulture);
|
|
spammer.Blocked = blocked;
|
|
spammer.TotalScore = score;
|
|
|
|
SendNewMemberMail(spammer);
|
|
}
|
|
else
|
|
{
|
|
LogHelper.Warn<Utils>(string.Format("Error checking stopforumspam.org {0}", spamCheckResult.Error));
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogHelper.Error<Utils>("Error checking stopforumspam.org", ex);
|
|
}
|
|
}
|
|
|
|
public static string GetIpAddress()
|
|
{
|
|
var context = HttpContext.Current;
|
|
var ipAddress = context.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
|
|
|
|
if (string.IsNullOrEmpty(ipAddress))
|
|
return context.Request.ServerVariables["REMOTE_ADDR"];
|
|
|
|
var addresses = ipAddress.Split(',');
|
|
return addresses.Length != 0 ? addresses[0] : context.Request.ServerVariables["REMOTE_ADDR"];
|
|
}
|
|
|
|
public static void SendPotentialSpamMemberMail(SpamResult spammer)
|
|
{
|
|
try
|
|
{
|
|
var notify = ConfigurationManager.AppSettings["uForumSpamNotify"];
|
|
|
|
var body = GetSpamResultBody(spammer);
|
|
|
|
var subject = string.Format("Umbraco community: member {0}", spammer.Blocked ? "blocked" : "flagged as potential spammer");
|
|
|
|
var mailMessage = new MailMessage
|
|
{
|
|
Subject = subject,
|
|
Body = body,
|
|
IsBodyHtml = true
|
|
};
|
|
|
|
foreach (var email in notify.Split(','))
|
|
mailMessage.To.Add(email);
|
|
|
|
mailMessage.From = new MailAddress("our@umbraco.org");
|
|
|
|
var smtpClient = new SmtpClient();
|
|
smtpClient.Send(mailMessage);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Add(LogTypes.Error, new User(0), -1, string.Format("Error sending potential spam member notification: {0} {1} {2}",
|
|
ex.Message, ex.StackTrace, ex.InnerException));
|
|
}
|
|
}
|
|
|
|
public static void SendNewMemberMail(SpamResult spamResult)
|
|
{
|
|
try
|
|
{
|
|
var notify = ConfigurationManager.AppSettings["uForumSpamNotify"];
|
|
var body = GetSpamResultBody(spamResult);
|
|
|
|
var subject = string.Format("Umbraco community: new member signed up");
|
|
|
|
var mailMessage = new MailMessage
|
|
{
|
|
Subject = subject,
|
|
Body = body,
|
|
IsBodyHtml = true
|
|
};
|
|
|
|
foreach (var email in notify.Split(','))
|
|
mailMessage.To.Add(email);
|
|
|
|
mailMessage.From = new MailAddress("our@umbraco.org");
|
|
|
|
var smtpClient = new SmtpClient();
|
|
smtpClient.Send(mailMessage);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Add(LogTypes.Error, new User(0), -1, string.Format("Error sending new member notification: {0} {1} {2}",
|
|
ex.Message, ex.StackTrace, ex.InnerException));
|
|
}
|
|
}
|
|
|
|
public static void SendActivationMail(IMember member)
|
|
{
|
|
try
|
|
{
|
|
var subject = "Activate your account on our.umbraco.org";
|
|
var body =
|
|
string.Format("Hi {0},<br /><br /> Thanks for signing up for the Umbraco community site. In order to be able to log in please click on the link below to activate your account: <br /><a href=\"https://our.umbraco.org/member/activate/?id={1}\">https://our.umbraco.org/member/activate/?id={1}</a><br /><br />Best regards,<br />The Umbraco Community robot.", member.Name, member.ProviderUserKey);
|
|
|
|
var mailMessage = new MailMessage
|
|
{
|
|
Subject = subject,
|
|
Body = body,
|
|
IsBodyHtml = true
|
|
};
|
|
|
|
|
|
mailMessage.To.Add(member.Email);
|
|
|
|
mailMessage.From = new MailAddress("robot@umbraco.org");
|
|
|
|
var smtpClient = new SmtpClient();
|
|
smtpClient.Send(mailMessage);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var error = string.Format("*ERROR* sending activation mail for member {0} - {1} {2} {3}", member.Email, ex.Message, ex.StackTrace, ex.InnerException);
|
|
SendSlackNotification(error);
|
|
LogHelper.Error<Utils>(error, ex);
|
|
}
|
|
}
|
|
|
|
private static void SendSlackNotification(string body)
|
|
{
|
|
using (var client = new WebClient())
|
|
{
|
|
var values = new NameValueCollection
|
|
{
|
|
{"channel", ConfigurationManager.AppSettings["SlackChannel"]},
|
|
{"token", ConfigurationManager.AppSettings["SlackToken"]},
|
|
{"username", ConfigurationManager.AppSettings["SlackUsername"]},
|
|
{"icon_url", ConfigurationManager.AppSettings["SlackIconUrl"]},
|
|
{"text", body}
|
|
};
|
|
|
|
try
|
|
{
|
|
var data = client.UploadValues("https://slack.com/api/chat.postMessage", "POST", values);
|
|
var response = client.Encoding.GetString(data);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogHelper.Error<Utils>("Posting update to Slack failed", ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static string GetForumName(IPublishedContent forum)
|
|
{
|
|
var forumName = forum.Name;
|
|
var isProjectForum = string.Equals(forum.Parent.ContentType.Alias, "Project", StringComparison.InvariantCultureIgnoreCase);
|
|
if (isProjectForum && forum.Name.ToLowerInvariant().Contains(forum.Parent.Name.ToLowerInvariant()) == false)
|
|
{
|
|
forumName = forum.Parent.Name + " - " + forumName;
|
|
}
|
|
|
|
return forumName;
|
|
}
|
|
|
|
private static string GetSpamResultBody(SpamResult spammer)
|
|
{
|
|
var body = string.Empty;
|
|
|
|
if (spammer.MemberId != 0)
|
|
{
|
|
body = body + string.Format(
|
|
"<a href=\"http://our.umbraco.org/umbraco/members/editMember.aspx?id={0}\">Edit Member</a><br /><br />",
|
|
spammer.MemberId);
|
|
|
|
body = body + string.Format("<a href=\"http://our.umbraco.org/member/{0}\">Go to member</a><br />", spammer.MemberId);
|
|
}
|
|
else if (spammer.Blocked)
|
|
{
|
|
body = body + string.Format("Member was never created.<br />");
|
|
}
|
|
|
|
body = body + string.Format(
|
|
"Blocked: {0}<br />Name: {1}<br />Company: {2}<br />Bio: {3}<br />Email: {4}<br />IP: {5}<br />" +
|
|
"Score IP: {6}<br />Frequency IP: {7}<br />Score e-mail: {8}<br />Frequency e-mail: {9}<br />Total score: {10}<br />Member Id: {11}",
|
|
spammer.Blocked,
|
|
spammer.Name ?? string.Empty,
|
|
spammer.Company ?? string.Empty,
|
|
spammer.Bio == null ? string.Empty : spammer.Bio.Replace("\n", "<br />"),
|
|
spammer.Email ?? string.Empty,
|
|
spammer.Ip ?? string.Empty,
|
|
spammer.ScoreIp ?? string.Empty,
|
|
spammer.FrequencyIp ?? string.Empty,
|
|
spammer.ScoreEmail ?? string.Empty,
|
|
spammer.FrequencyEmail ?? string.Empty,
|
|
spammer.TotalScore,
|
|
spammer.MemberId);
|
|
|
|
var querystring = string.Format("api?ip={0}&email={1}&f=json", spammer.Ip, HttpUtility.UrlEncode(spammer.Email));
|
|
|
|
body = body + string.Format("<hr /><a href=\"http://api.stopforumspam.org/{0}\">Check full StopForumSpam response</a>", querystring);
|
|
return body;
|
|
}
|
|
}
|
|
|
|
|
|
public class SpamCheckResult
|
|
{
|
|
public int Success { get; set; }
|
|
public string Error { get; set; }
|
|
public SpamCheckProperty Username { get; set; }
|
|
public SpamCheckProperty Email { get; set; }
|
|
public SpamCheckProperty Ip { get; set; }
|
|
}
|
|
|
|
public class SpamCheckProperty
|
|
{
|
|
public string LastSeen { get; set; }
|
|
public int Frequency { get; set; }
|
|
public int Appears { get; set; }
|
|
public float Confidence { get; set; }
|
|
}
|
|
|
|
public class SpamResult
|
|
{
|
|
public int MemberId { get; set; }
|
|
public string Name { get; set; }
|
|
public string Company { get; set; }
|
|
public string Bio { get; set; }
|
|
public string Ip { get; set; }
|
|
public string Email { get; set; }
|
|
public string FrequencyIp { get; set; }
|
|
public string ScoreIp { get; set; }
|
|
public string ScoreEmail { get; set; }
|
|
public string FrequencyEmail { get; set; }
|
|
public bool AlreadyInSpamRole { get; set; }
|
|
public bool Blocked { get; set; }
|
|
public float TotalScore { get; set; }
|
|
}
|
|
|
|
public struct ReplacePoint
|
|
{
|
|
public int open, close;
|
|
|
|
public ReplacePoint(int open, int close)
|
|
{
|
|
this.open = open;
|
|
this.close = close;
|
|
}
|
|
}
|
|
}
|