2014-01-10 17:03:00 +11:00
using System ;
using System.Collections.Generic ;
2014-01-13 13:05:25 +11:00
using System.Globalization ;
using System.Linq ;
using System.Net.Mail ;
using System.Text ;
2014-01-13 13:50:30 +11:00
using System.Threading ;
2014-01-13 13:05:25 +11:00
using System.Web ;
using Umbraco.Core.Configuration ;
using Umbraco.Core.IO ;
using Umbraco.Core.Logging ;
2014-01-10 17:03:00 +11:00
using Umbraco.Core.Models ;
using Umbraco.Core.Models.EntityBase ;
using Umbraco.Core.Models.Membership ;
using Umbraco.Core.Persistence.Repositories ;
using Umbraco.Core.Persistence.UnitOfWork ;
2014-01-13 13:05:25 +11:00
using Umbraco.Core.Strings ;
2014-01-10 17:03:00 +11:00
using umbraco.interfaces ;
namespace Umbraco.Core.Services
{
2014-05-08 11:38:05 +10:00
public class NotificationService : INotificationService
2014-01-10 17:03:00 +11:00
{
private readonly IDatabaseUnitOfWorkProvider _uowProvider ;
2014-01-13 13:05:25 +11:00
private readonly IUserService _userService ;
private readonly IContentService _contentService ;
2015-01-07 17:23:24 +11:00
private readonly ILogger _logger ;
2014-01-10 17:03:00 +11:00
2015-01-07 17:23:24 +11:00
public NotificationService ( IDatabaseUnitOfWorkProvider provider , IUserService userService , IContentService contentService , ILogger logger )
2014-01-10 17:03:00 +11:00
{
2015-01-07 17:23:24 +11:00
if ( provider = = null ) throw new ArgumentNullException ( "provider" ) ;
if ( userService = = null ) throw new ArgumentNullException ( "userService" ) ;
if ( contentService = = null ) throw new ArgumentNullException ( "contentService" ) ;
if ( logger = = null ) throw new ArgumentNullException ( "logger" ) ;
2014-01-10 17:03:00 +11:00
_uowProvider = provider ;
2014-01-13 13:05:25 +11:00
_userService = userService ;
_contentService = contentService ;
2015-01-07 17:23:24 +11:00
_logger = logger ;
2014-01-10 17:03:00 +11:00
}
/// <summary>
/// Sends the notifications for the specified user regarding the specified node and action.
/// </summary>
/// <param name="entity"></param>
2014-01-13 13:05:25 +11:00
/// <param name="operatingUser"></param>
2014-01-10 17:03:00 +11:00
/// <param name="action"></param>
2014-01-13 13:05:25 +11:00
/// <param name="actionName"></param>
/// <param name="http"></param>
/// <param name="createSubject"></param>
/// <param name="createBody"></param>
/// <remarks>
/// Currently this will only work for Content entities!
/// </remarks>
public void SendNotifications ( IUser operatingUser , IUmbracoEntity entity , string action , string actionName , HttpContextBase http ,
Func < IUser , string [ ] , string > createSubject ,
Func < IUser , string [ ] , string > createBody )
2014-01-10 17:03:00 +11:00
{
2014-01-13 13:05:25 +11:00
if ( ( entity is IContent ) = = false )
{
throw new NotSupportedException ( ) ;
}
var content = ( IContent ) entity ;
2014-01-13 13:50:30 +11:00
//we'll lazily get these if we need to send notifications
2015-08-11 12:12:34 +02:00
IEnumerable < IContent > allVersions = null ;
2014-01-13 13:05:25 +11:00
int totalUsers ;
2014-02-21 16:03:32 +11:00
var allUsers = _userService . GetAll ( 0 , int . MaxValue , out totalUsers ) ;
2014-01-13 13:05:25 +11:00
foreach ( var u in allUsers )
{
if ( u . IsApproved = = false ) continue ;
var userNotifications = GetUserNotifications ( u , content . Path ) . ToArray ( ) ;
var notificationForAction = userNotifications . FirstOrDefault ( x = > x . Action = = action ) ;
if ( notificationForAction ! = null )
{
2014-01-13 13:50:30 +11:00
//lazy load versions if notifications are required
if ( allVersions = = null )
{
2015-08-11 12:12:34 +02:00
allVersions = _contentService . GetVersions ( entity . Id ) ;
2014-01-13 13:50:30 +11:00
}
2014-01-13 13:05:25 +11:00
try
{
2015-08-11 11:52:06 +02:00
SendNotification (
2015-08-11 12:12:34 +02:00
operatingUser , u , content ,
allVersions ,
2015-08-11 11:52:06 +02:00
actionName , http , createSubject , createBody ) ;
2015-01-07 17:23:24 +11:00
_logger . Debug < NotificationService > ( string . Format ( "Notification type: {0} sent to {1} ({2})" , action , u . Name , u . Email ) ) ;
2014-01-13 13:05:25 +11:00
}
catch ( Exception ex )
{
2015-01-07 17:23:24 +11:00
_logger . Error < NotificationService > ( "An error occurred sending notification" , ex ) ;
2014-01-13 13:05:25 +11:00
}
}
}
2014-01-10 17:03:00 +11:00
}
/// <summary>
/// Gets the notifications for the user
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public IEnumerable < Notification > GetUserNotifications ( IUser user )
{
var uow = _uowProvider . GetUnitOfWork ( ) ;
var repository = new NotificationsRepository ( uow ) ;
return repository . GetUserNotifications ( user ) ;
}
2014-01-13 13:05:25 +11:00
/// <summary>
/// Gets the notifications for the user based on the specified node path
/// </summary>
/// <param name="user"></param>
/// <param name="path"></param>
/// <returns></returns>
/// <remarks>
/// Notifications are inherited from the parent so any child node will also have notifications assigned based on it's parent (ancestors)
/// </remarks>
public IEnumerable < Notification > GetUserNotifications ( IUser user , string path )
{
var userNotifications = GetUserNotifications ( user ) . ToArray ( ) ;
var pathParts = path . Split ( new [ ] { ',' } , StringSplitOptions . RemoveEmptyEntries ) ;
2014-01-13 13:50:30 +11:00
var result = userNotifications . Where ( r = > pathParts . InvariantContains ( r . EntityId . ToString ( CultureInfo . InvariantCulture ) ) ) . ToList ( ) ;
2014-01-13 13:05:25 +11:00
return result ;
}
2014-01-10 17:03:00 +11:00
/// <summary>
/// Deletes notifications by entity
/// </summary>
/// <param name="entity"></param>
public IEnumerable < Notification > GetEntityNotifications ( IEntity entity )
{
var uow = _uowProvider . GetUnitOfWork ( ) ;
var repository = new NotificationsRepository ( uow ) ;
return repository . GetEntityNotifications ( entity ) ;
}
/// <summary>
/// Deletes notifications by entity
/// </summary>
/// <param name="entity"></param>
public void DeleteNotifications ( IEntity entity )
{
var uow = _uowProvider . GetUnitOfWork ( ) ;
var repository = new NotificationsRepository ( uow ) ;
repository . DeleteNotifications ( entity ) ;
}
/// <summary>
/// Deletes notifications by user
/// </summary>
/// <param name="user"></param>
public void DeleteNotifications ( IUser user )
{
var uow = _uowProvider . GetUnitOfWork ( ) ;
var repository = new NotificationsRepository ( uow ) ;
repository . DeleteNotifications ( user ) ;
}
/// <summary>
/// Delete notifications by user and entity
/// </summary>
/// <param name="user"></param>
/// <param name="entity"></param>
public void DeleteNotifications ( IUser user , IEntity entity )
{
var uow = _uowProvider . GetUnitOfWork ( ) ;
var repository = new NotificationsRepository ( uow ) ;
repository . DeleteNotifications ( user , entity ) ;
}
/// <summary>
/// Creates a new notification
/// </summary>
/// <param name="user"></param>
/// <param name="entity"></param>
/// <param name="action">The action letter - note: this is a string for future compatibility</param>
/// <returns></returns>
public Notification CreateNotification ( IUser user , IEntity entity , string action )
{
var uow = _uowProvider . GetUnitOfWork ( ) ;
var repository = new NotificationsRepository ( uow ) ;
return repository . CreateNotification ( user , entity , action ) ;
}
2014-01-13 13:05:25 +11:00
#region private methods
/// <summary>
/// Sends the notification
/// </summary>
/// <param name="performingUser"></param>
/// <param name="mailingUser"></param>
/// <param name="content"></param>
2014-01-13 13:50:30 +11:00
/// <param name="allVersions"></param>
2014-01-13 13:05:25 +11:00
/// <param name="actionName">The action readable name - currently an action is just a single letter, this is the name associated with the letter </param>
/// <param name="http"></param>
/// <param name="createSubject">Callback to create the mail subject</param>
/// <param name="createBody">Callback to create the mail body</param>
2015-08-11 12:12:34 +02:00
private void SendNotification ( IUser performingUser , IUser mailingUser , IContent content , IEnumerable < IContent > allVersions , string actionName , HttpContextBase http ,
2014-01-13 13:05:25 +11:00
Func < IUser , string [ ] , string > createSubject ,
Func < IUser , string [ ] , string > createBody )
{
if ( performingUser = = null ) throw new ArgumentNullException ( "performingUser" ) ;
if ( mailingUser = = null ) throw new ArgumentNullException ( "mailingUser" ) ;
if ( content = = null ) throw new ArgumentNullException ( "content" ) ;
2014-01-13 13:50:30 +11:00
if ( allVersions = = null ) throw new ArgumentNullException ( "allVersions" ) ;
2014-01-13 13:05:25 +11:00
if ( http = = null ) throw new ArgumentNullException ( "http" ) ;
if ( createSubject = = null ) throw new ArgumentNullException ( "createSubject" ) ;
if ( createBody = = null ) throw new ArgumentNullException ( "createBody" ) ;
2015-08-11 12:12:34 +02:00
//Ensure they are sorted: http://issues.umbraco.org/issue/U4-5180
var allVersionsAsArray = allVersions . OrderBy ( x = > x . UpdateDate ) . ToArray ( ) ;
int versionCount = ( allVersionsAsArray . Length > 1 ) ? ( allVersionsAsArray . Length - 2 ) : ( allVersionsAsArray . Length - 1 ) ;
var oldDoc = _contentService . GetByVersion ( allVersionsAsArray [ versionCount ] . Version ) ;
2014-01-13 13:05:25 +11:00
// build summary
var summary = new StringBuilder ( ) ;
var props = content . Properties . ToArray ( ) ;
foreach ( var p in props )
{
var newText = p . Value ! = null ? p . Value . ToString ( ) : "" ;
var oldText = newText ;
// check if something was changed and display the changes otherwise display the fields
if ( oldDoc . Properties . Contains ( p . PropertyType . Alias ) )
{
var oldProperty = oldDoc . Properties [ p . PropertyType . Alias ] ;
oldText = oldProperty . Value ! = null ? oldProperty . Value . ToString ( ) : "" ;
// replace html with char equivalent
ReplaceHtmlSymbols ( ref oldText ) ;
ReplaceHtmlSymbols ( ref newText ) ;
}
// make sure to only highlight changes done using TinyMCE editor... other changes will be displayed using default summary
// TODO: We should probably allow more than just tinymce??
2014-01-13 14:54:15 +11:00
if ( ( p . PropertyType . PropertyEditorAlias = = Constants . PropertyEditors . TinyMCEAlias )
2014-01-13 13:05:25 +11:00
& & string . CompareOrdinal ( oldText , newText ) ! = 0 )
{
summary . Append ( "<tr>" ) ;
summary . Append ( "<th style='text-align: left; vertical-align: top; width: 25%;'> Note: </th>" ) ;
summary . Append (
"<td style='text-align: left; vertical-align: top;'> <span style='background-color:red;'>Red for deleted characters</span> <span style='background-color:yellow;'>Yellow for inserted characters</span></td>" ) ;
summary . Append ( "</tr>" ) ;
summary . Append ( "<tr>" ) ;
summary . Append ( "<th style='text-align: left; vertical-align: top; width: 25%;'> New " +
p . PropertyType . Name + "</th>" ) ;
summary . Append ( "<td style='text-align: left; vertical-align: top;'>" +
ReplaceLinks ( CompareText ( oldText , newText , true , false , "<span style='background-color:yellow;'>" , string . Empty ) , http . Request ) +
"</td>" ) ;
summary . Append ( "</tr>" ) ;
summary . Append ( "<tr>" ) ;
summary . Append ( "<th style='text-align: left; vertical-align: top; width: 25%;'> Old " +
p . PropertyType . Name + "</th>" ) ;
summary . Append ( "<td style='text-align: left; vertical-align: top;'>" +
ReplaceLinks ( CompareText ( newText , oldText , true , false , "<span style='background-color:red;'>" , string . Empty ) , http . Request ) +
"</td>" ) ;
summary . Append ( "</tr>" ) ;
}
else
{
summary . Append ( "<tr>" ) ;
summary . Append ( "<th style='text-align: left; vertical-align: top; width: 25%;'>" +
p . PropertyType . Name + "</th>" ) ;
summary . Append ( "<td style='text-align: left; vertical-align: top;'>" + newText + "</td>" ) ;
summary . Append ( "</tr>" ) ;
}
summary . Append (
"<tr><td colspan=\"2\" style=\"border-bottom: 1px solid #CCC; font-size: 2px;\"> </td></tr>" ) ;
}
string protocol = GlobalSettings . UseSSL ? "https" : "http" ;
string [ ] subjectVars = {
http . Request . ServerVariables [ "SERVER_NAME" ] + ":" +
http . Request . Url . Port +
IOHelper . ResolveUrl ( SystemDirectories . Umbraco ) ,
actionName ,
content . Name
} ;
string [ ] bodyVars = {
mailingUser . Name ,
actionName ,
content . Name ,
performingUser . Name ,
http . Request . ServerVariables [ "SERVER_NAME" ] + ":" + http . Request . Url . Port + IOHelper . ResolveUrl ( SystemDirectories . Umbraco ) ,
content . Id . ToString ( CultureInfo . InvariantCulture ) , summary . ToString ( ) ,
string . Format ( "{2}://{0}/{1}" ,
http . Request . ServerVariables [ "SERVER_NAME" ] + ":" + http . Request . Url . Port ,
//TODO: RE-enable this so we can have a nice url
/*umbraco.library.NiceUrl(documentObject.Id))*/
content . Id + ".aspx" ,
protocol )
} ;
// create the mail message
2014-01-13 14:54:15 +11:00
var mail = new MailMessage ( UmbracoConfig . For . UmbracoSettings ( ) . Content . NotificationEmailAddress , mailingUser . Email ) ;
2014-01-13 13:05:25 +11:00
// populate the message
mail . Subject = createSubject ( mailingUser , subjectVars ) ;
2014-01-13 14:54:15 +11:00
if ( UmbracoConfig . For . UmbracoSettings ( ) . Content . DisableHtmlEmail )
2014-01-13 13:05:25 +11:00
{
mail . IsBodyHtml = false ;
mail . Body = createBody ( mailingUser , bodyVars ) ;
}
else
{
mail . IsBodyHtml = true ;
mail . Body =
@ "<html><head>
< / head >
< body style = ' font - family : Trebuchet MS , arial , sans - serif ; font - color : black ; ' >
" + createBody(mailingUser, bodyVars);
}
// nh, issue 30724. Due to hardcoded http strings in resource files, we need to check for https replacements here
// adding the server name to make sure we don't replace external links
if ( GlobalSettings . UseSSL & & string . IsNullOrEmpty ( mail . Body ) = = false )
{
string serverName = http . Request . ServerVariables [ "SERVER_NAME" ] ;
mail . Body = mail . Body . Replace (
string . Format ( "http://{0}" , serverName ) ,
string . Format ( "https://{0}" , serverName ) ) ;
}
2014-01-13 13:50:30 +11:00
// send it asynchronously, we don't want to got up all of the request time to send emails!
ThreadPool . QueueUserWorkItem ( state = >
{
try
{
2014-01-13 16:13:53 +11:00
using ( mail )
{
using ( var sender = new SmtpClient ( ) )
{
sender . Send ( mail ) ;
}
}
2014-01-13 17:28:49 +11:00
2014-01-13 13:50:30 +11:00
}
catch ( Exception ex )
{
2015-01-07 17:23:24 +11:00
_logger . Error < NotificationService > ( "An error occurred sending notification" , ex ) ;
2014-01-13 13:50:30 +11:00
}
} ) ;
2014-01-13 13:05:25 +11:00
}
private static string ReplaceLinks ( string text , HttpRequestBase request )
{
string domain = GlobalSettings . UseSSL ? "https://" : "http://" ;
domain + = request . ServerVariables [ "SERVER_NAME" ] + ":" + request . Url . Port + "/" ;
text = text . Replace ( "href=\"/" , "href=\"" + domain ) ;
text = text . Replace ( "src=\"/" , "src=\"" + domain ) ;
return text ;
}
/// <summary>
/// Replaces the HTML symbols with the character equivalent.
/// </summary>
/// <param name="oldString">The old string.</param>
private static void ReplaceHtmlSymbols ( ref string oldString )
{
oldString = oldString . Replace ( " " , " " ) ;
oldString = oldString . Replace ( "’" , "'" ) ;
oldString = oldString . Replace ( "&" , "&" ) ;
oldString = oldString . Replace ( "“" , "“" ) ;
oldString = oldString . Replace ( "”" , "”" ) ;
oldString = oldString . Replace ( """ , "\"" ) ;
}
/// <summary>
/// Compares the text.
/// </summary>
/// <param name="oldText">The old text.</param>
/// <param name="newText">The new text.</param>
/// <param name="displayInsertedText">if set to <c>true</c> [display inserted text].</param>
/// <param name="displayDeletedText">if set to <c>true</c> [display deleted text].</param>
/// <param name="insertedStyle">The inserted style.</param>
/// <param name="deletedStyle">The deleted style.</param>
/// <returns></returns>
private static string CompareText ( string oldText , string newText , bool displayInsertedText ,
bool displayDeletedText , string insertedStyle , string deletedStyle )
{
var sb = new StringBuilder ( ) ;
var diffs = Diff . DiffText1 ( oldText , newText ) ;
int pos = 0 ;
for ( int n = 0 ; n < diffs . Length ; n + + )
{
Diff . Item it = diffs [ n ] ;
// write unchanged chars
while ( ( pos < it . StartB ) & & ( pos < newText . Length ) )
{
sb . Append ( newText [ pos ] ) ;
pos + + ;
} // while
// write deleted chars
if ( displayDeletedText & & it . DeletedA > 0 )
{
sb . Append ( deletedStyle ) ;
for ( int m = 0 ; m < it . DeletedA ; m + + )
{
sb . Append ( oldText [ it . StartA + m ] ) ;
} // for
sb . Append ( "</span>" ) ;
}
// write inserted chars
if ( displayInsertedText & & pos < it . StartB + it . InsertedB )
{
sb . Append ( insertedStyle ) ;
while ( pos < it . StartB + it . InsertedB )
{
sb . Append ( newText [ pos ] ) ;
pos + + ;
} // while
sb . Append ( "</span>" ) ;
} // if
} // while
// write rest of unchanged chars
while ( pos < newText . Length )
{
sb . Append ( newText [ pos ] ) ;
pos + + ;
} // while
return sb . ToString ( ) ;
}
#endregion
2014-01-10 17:03:00 +11:00
}
}