/*
* Box Socialâ„¢
* http://boxsocial.net/
* Copyright © 2007, David Lachlan Smith
*
* $Id:$
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using BoxSocial.IO;
namespace BoxSocial.Internals
{
[DataTable("comments")]
public sealed class Comment : NumberedItem
{
// TODO: 1023 max length
public const int COMMENT_MAX_LENGTH = 1023;
[DataField("comment_id", DataFieldKeys.Primary)]
private long commentId;
[DataField("user_id")]
private long userId;
/*[DataField("comment_item_id")]
private long itemId;
[DataField("comment_item_type", NAMESPACE)]
private string itemType;*/
[DataField("comment_item", DataFieldKeys.Index)]
private ItemKey itemKey;
[DataField("comment_spam_score")]
private byte spamScore;
[DataField("comment_time_ut")]
private long timeRaw;
[DataField("comment_ip", IP)]
private string commentIp;
[DataField("comment_text", COMMENT_MAX_LENGTH)]
private string body;
[DataField("comment_hash", 128)]
private string commentHash;
[DataField("comment_deleted")]
private bool deleted;
public long CommentId
{
get
{
return commentId;
}
}
public long UserId
{
get
{
return userId;
}
}
public ItemKey ItemKey
{
get
{
return itemKey;
}
}
public long ItemId
{
get
{
return itemKey.Id;
}
}
public string ItemType
{
get
{
return itemKey.Type;
}
}
public byte SpamScore
{
get
{
return spamScore;
}
}
public string Body
{
get
{
return body;
}
}
public DateTime GetTime(UnixTime tz)
{
return tz.DateTimeFromMysql(timeRaw);
}
public Comment(Core core, long commentId)
: base(core)
{
ItemLoad += new ItemLoadHandler(Comment_ItemLoad);
try
{
LoadItem(commentId);
}
catch (InvalidItemException)
{
throw new InvalidCommentException();
}
}
public Comment(Core core, DataRow commentRow)
: base(core)
{
ItemLoad += new ItemLoadHandler(Comment_ItemLoad);
try
{
loadItemInfo(commentRow);
}
catch (InvalidItemException)
{
throw new InvalidCommentException();
}
}
void Comment_ItemLoad()
{
}
public static Comment Create(Core core, ItemKey itemKey, string comment)
{
if (!core.Session.IsLoggedIn)
{
throw new NotLoggedInException();
}
if (core.Db.Query(string.Format("SELECT user_id FROM comments WHERE (user_id = {0} OR comment_ip = '{1}') AND (UNIX_TIMESTAMP() - comment_time_ut) < 20",
core.LoggedInMemberId, core.Session.IPAddress.ToString())).Rows.Count > 0)
{
throw new CommentFloodException();
}
if (comment.Length > COMMENT_MAX_LENGTH)
{
throw new CommentTooLongException();
}
if (comment.Length < 2)
{
throw new CommentTooShortException();
}
Relation relations = Relation.None;
// A little bit of hard coding we can't avoid
if (itemKey.Type == typeof(User).FullName)
{
core.LoadUserProfile(itemKey.Id);
relations = core.PrimitiveCache[itemKey.Id].GetRelations(core.Session.LoggedInMember);
}
core.Db.BeginTransaction();
long commentId = core.Db.UpdateQuery(string.Format("INSERT INTO comments (comment_item_id, comment_item_type_id, user_id, comment_time_ut, comment_text, comment_ip, comment_spam_score, comment_hash) VALUES ({0}, {1}, {2}, UNIX_TIMESTAMP(), '{3}', '{4}', {5}, '{6}');",
itemKey.Id, itemKey.TypeId, core.LoggedInMemberId, Mysql.Escape(comment), core.Session.IPAddress.ToString(), CalculateSpamScore(core, comment, relations), MessageMd5(comment)));
return new Comment(core, commentId);
}
public static List GetComments(Core core, ItemKey itemKey, SortOrder commentSortOrder, int currentPage, int perPage, List commenters)
{
Mysql db = core.Db;
List comments = new List();
string sort = (commentSortOrder == SortOrder.Ascending) ? "ASC" : "DESC";
SelectQuery query = Comment.GetSelectQueryStub(typeof(Comment));
query.AddCondition("comment_deleted", false);
query.AddSort(commentSortOrder, "comment_time_ut");
query.LimitStart = (currentPage - 1) * perPage;
query.LimitCount = perPage;
if (commenters != null)
{
if (commenters.Count == 2)
{
if (itemKey.Type == typeof(User).FullName)
{
QueryCondition qc1 = query.AddCondition("comment_item_id", commenters[0].Id);
qc1.AddCondition("user_id", commenters[1].Id);
QueryCondition qc2 = query.AddCondition(ConditionRelations.Or, "comment_item_id", commenters[1].Id);
qc2.AddCondition("user_id", commenters[0].Id);
query.AddCondition("comment_item_type_id", itemKey.TypeId);
}
else
{
query.AddCondition("comment_item_id", itemKey.Id);
query.AddCondition("comment_item_type_id", itemKey.TypeId);
}
}
else
{
query.AddCondition("comment_item_id", itemKey.Id);
query.AddCondition("comment_item_type_id", itemKey.TypeId);
}
}
else
{
query.AddCondition("comment_item_id", itemKey.Id);
query.AddCondition("comment_item_type_id", itemKey.TypeId);
}
DataTable commentsTable = db.Query(query);
foreach (DataRow dr in commentsTable.Rows)
{
comments.Add(new Comment(core, dr));
}
return comments;
}
public static void LoadUserInfoCache(Core core, List comments)
{
List userIds = GetUserIds(comments);
core.LoadUserProfiles(userIds);
}
private static List GetUserIds(List comments)
{
List userIds = new List();
foreach (Comment comment in comments)
{
if (!userIds.Contains(comment.UserId))
{
userIds.Add(comment.UserId);
}
}
return userIds;
}
private static int CalculateSpamScore(Core core, string message, Relation relations)
{
double spamScore = 0;
bool hasMatchingHash = false;
string messageMd5Hash = MessageMd5(message);
TimeSpan ts = DateTime.Now - core.Session.LoggedInMember.Info.RegistrationDate;
// registered last ...
if (ts.TotalMinutes <= 10) // first 10 minutes
{
spamScore += 6;
}
else if (ts.TotalDays <= 1) // first day
{
spamScore += 4;
}
else if (ts.TotalDays <= 7) // first week
{
spamScore += 1;
}
// hyperlinks
MatchCollection httpMatches = Regex.Matches(message, @"(([a-z]+?://){1}|)([a-z0-9\-\.,\?!%\*_\#:;~\\&$@\/=\+\(\)]+)", RegexOptions.IgnoreCase);
spamScore += Math.Sqrt(httpMatches.Count * 1.0);
// embedded images
int lastImgIndex = 0;
int imageScore = 0;
do
{
lastImgIndex = Math.Max(message.IndexOf("[img]", lastImgIndex + 1), message.IndexOf("[IMG]", lastImgIndex + 1));
if (lastImgIndex >= 0)
{
imageScore += 1;
}
}
while (lastImgIndex >= 0);
spamScore += Math.Sqrt((double)imageScore);
// common spam words that on their own aren't spam, but if used alot might indicate the presence of spam
string[] spamWords = { "porn", "poker", "ringtone", "casino", "viagra", "blackjack", "gambling", "beastiality", "insurance", "phentermine", "incest", "anal", "lesbian", "gay", "porno", "fisting", "sex", "dildo", "fuck", "cum", "milf", "insurence", "ephedrine", "prescription", "cialis", "heroin" };
Array.Sort(spamWords);
string[] messageWords = message.Split(new char[] { ' ', '\t', '\'', '\n', '?', ',', '.', '(', ')', '[', ']' });
int wordScore = 0;
for (int i = 0; i < messageWords.Length; i++)
{
if (Array.IndexOf(spamWords, messageWords[i].ToLower()) >= 0)
{
wordScore += 1;
}
}
spamScore += Math.Sqrt((double)wordScore);
// if less than 5 words
if (messageWords.Length < 5)
{
spamScore += 2;
}
//
// select all threads that are related spam
//
DataTable spamCommentsTable = core.Db.Query(string.Format("SELECT comment_ip, comment_hash FROM comments WHERE ((comment_ip = '{0}' AND comment_time_ut + 86400 > UNIX_TIMESTAMP()) OR comment_hash = '{1}') AND comment_spam_score >= 10 GROUP BY comment_ip, comment_hash;",
core.Session.IPAddress.ToString(), messageMd5Hash));
// known spam IPs
for (int i = 0; i < spamCommentsTable.Rows.Count; i++)
{
if ((string)spamCommentsTable.Rows[i]["comment_ip"] == core.Session.IPAddress.ToString())
{
spamScore += 5;
break;
}
}
// known spam hash
for (int i = 0; i < spamCommentsTable.Rows.Count; i++)
{
if ((string)spamCommentsTable.Rows[i]["comment_hash"] == messageMd5Hash)
{
spamScore += 8;
hasMatchingHash = true;
break;
}
}
// friend
if ((relations & Relation.Owner) == Relation.Owner)
{
spamScore = 0;
}
else if ((relations & Relation.Family) == Relation.Family)
{
spamScore -= 5;
}
else if ((relations & Relation.Friend) == Relation.Friend)
{
spamScore -= 3;
}
// Group: spamScore -= 1
int returnScore = (int)Math.Ceiling(spamScore);
returnScore = Math.Min(returnScore, byte.MaxValue); // prevent overflow
returnScore = Math.Max(returnScore, byte.MinValue); // prevent underflow
// update existing comments with matching MD5 string
if (returnScore >= 10)
{
if (hasMatchingHash)
{
core.Db.UpdateQuery(string.Format("UPDATE comments SET comment_spam_score = {0} WHERE comment_hash = '{1}'",
returnScore, messageMd5Hash));
}
}
return returnScore;
}
public static string MessageMd5(string input)
{
return System.Web.Security.FormsAuthentication.HashPasswordForStoringInConfigFile(input, "MD5").ToLower();
}
public override long Id
{
get
{
return commentId;
}
}
public string BuildUri(ICommentableItem item)
{
return core.Uri.AppendSid(string.Format("{0}?c={2}c{1}",
core.Uri.StripSid(item.Uri), commentId, commentId));
}
public override string Uri
{
get
{
throw new NotImplementedException();
}
}
}
public class CommentPostedEventArgs : EventArgs
{
private Comment comment;
private ItemKey itemKey;
private User poster;
public Comment Comment
{
get
{
return comment;
}
}
public ItemKey ItemKey
{
get
{
return itemKey;
}
}
public string ItemType
{
get
{
return itemKey.Type;
}
}
public long ItemId
{
get
{
return itemKey.Id;
}
}
public User Poster
{
get
{
return poster;
}
}
public CommentPostedEventArgs(Comment comment, User poster, ItemKey itemKey)
{
this.comment = comment;
this.poster = poster;
this.itemKey = itemKey;
}
}
public class InvalidCommentException : Exception
{
}
public class CommentTooLongException : Exception
{
}
public class CommentTooShortException : Exception
{
}
public class CommentFloodException : Exception
{
}
public class NotLoggedInException : Exception
{
}
}