using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Mail; using System.Text; using System.Threading; using System.Web; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Strings; using umbraco.interfaces; namespace Umbraco.Core.Services { public class NotificationService : INotificationService { private readonly IDatabaseUnitOfWorkProvider _uowProvider; private readonly IUserService _userService; private readonly IContentService _contentService; private readonly ILogger _logger; public NotificationService(IDatabaseUnitOfWorkProvider provider, IUserService userService, IContentService contentService, ILogger logger) { 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"); _uowProvider = provider; _userService = userService; _contentService = contentService; _logger = logger; } /// /// Sends the notifications for the specified user regarding the specified node and action. /// /// /// /// /// /// /// /// /// /// Currently this will only work for Content entities! /// public void SendNotifications(IUser operatingUser, IUmbracoEntity entity, string action, string actionName, HttpContextBase http, Func createSubject, Func createBody) { if ((entity is IContent) == false) { throw new NotSupportedException(); } var content = (IContent) entity; //we'll lazily get these if we need to send notifications IEnumerable allVersions = null; int totalUsers; var allUsers = _userService.GetAll(0, int.MaxValue, out totalUsers); 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) { //lazy load versions if notifications are required if (allVersions == null) { allVersions = _contentService.GetVersions(entity.Id); } try { SendNotification( operatingUser, u, content, allVersions, actionName, http, createSubject, createBody); _logger.Debug(string.Format("Notification type: {0} sent to {1} ({2})", action, u.Name, u.Email)); } catch (Exception ex) { _logger.Error("An error occurred sending notification", ex); } } } } /// /// Gets the notifications for the user /// /// /// public IEnumerable GetUserNotifications(IUser user) { var uow = _uowProvider.GetUnitOfWork(); var repository = new NotificationsRepository(uow); return repository.GetUserNotifications(user); } /// /// Gets the notifications for the user based on the specified node path /// /// /// /// /// /// Notifications are inherited from the parent so any child node will also have notifications assigned based on it's parent (ancestors) /// public IEnumerable GetUserNotifications(IUser user, string path) { var userNotifications = GetUserNotifications(user).ToArray(); var pathParts = path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); var result = userNotifications.Where(r => pathParts.InvariantContains(r.EntityId.ToString(CultureInfo.InvariantCulture))).ToList(); return result; } /// /// Deletes notifications by entity /// /// public IEnumerable GetEntityNotifications(IEntity entity) { var uow = _uowProvider.GetUnitOfWork(); var repository = new NotificationsRepository(uow); return repository.GetEntityNotifications(entity); } /// /// Deletes notifications by entity /// /// public void DeleteNotifications(IEntity entity) { var uow = _uowProvider.GetUnitOfWork(); var repository = new NotificationsRepository(uow); repository.DeleteNotifications(entity); } /// /// Deletes notifications by user /// /// public void DeleteNotifications(IUser user) { var uow = _uowProvider.GetUnitOfWork(); var repository = new NotificationsRepository(uow); repository.DeleteNotifications(user); } /// /// Delete notifications by user and entity /// /// /// public void DeleteNotifications(IUser user, IEntity entity) { var uow = _uowProvider.GetUnitOfWork(); var repository = new NotificationsRepository(uow); repository.DeleteNotifications(user, entity); } /// /// Creates a new notification /// /// /// /// The action letter - note: this is a string for future compatibility /// public Notification CreateNotification(IUser user, IEntity entity, string action) { var uow = _uowProvider.GetUnitOfWork(); var repository = new NotificationsRepository(uow); return repository.CreateNotification(user, entity, action); } #region private methods /// /// Sends the notification /// /// /// /// /// /// The action readable name - currently an action is just a single letter, this is the name associated with the letter /// /// Callback to create the mail subject /// Callback to create the mail body private void SendNotification(IUser performingUser, IUser mailingUser, IContent content, IEnumerable allVersions, string actionName, HttpContextBase http, Func createSubject, Func createBody) { if (performingUser == null) throw new ArgumentNullException("performingUser"); if (mailingUser == null) throw new ArgumentNullException("mailingUser"); if (content == null) throw new ArgumentNullException("content"); if (allVersions == null) throw new ArgumentNullException("allVersions"); if (http == null) throw new ArgumentNullException("http"); if (createSubject == null) throw new ArgumentNullException("createSubject"); if (createBody == null) throw new ArgumentNullException("createBody"); //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); // 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?? if ((p.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.TinyMCEAlias) && string.CompareOrdinal(oldText, newText) != 0) { summary.Append(""); summary.Append(" Note: "); summary.Append( " Red for deleted characters Yellow for inserted characters"); summary.Append(""); summary.Append(""); summary.Append(" New " + p.PropertyType.Name + ""); summary.Append("" + ReplaceLinks(CompareText(oldText, newText, true, false, "", string.Empty), http.Request) + ""); summary.Append(""); summary.Append(""); summary.Append(" Old " + p.PropertyType.Name + ""); summary.Append("" + ReplaceLinks(CompareText(newText, oldText, true, false, "", string.Empty), http.Request) + ""); summary.Append(""); } else { summary.Append(""); summary.Append("" + p.PropertyType.Name + ""); summary.Append("" + newText + ""); summary.Append(""); } summary.Append( " "); } 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 var mail = new MailMessage(UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, mailingUser.Email); // populate the message mail.Subject = createSubject(mailingUser, subjectVars); if (UmbracoConfig.For.UmbracoSettings().Content.DisableHtmlEmail) { mail.IsBodyHtml = false; mail.Body = createBody(mailingUser, bodyVars); } else { mail.IsBodyHtml = true; mail.Body = @" " + 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)); } // send it asynchronously, we don't want to got up all of the request time to send emails! ThreadPool.QueueUserWorkItem(state => { try { using (mail) { using (var sender = new SmtpClient()) { sender.Send(mail); } } } catch (Exception ex) { _logger.Error("An error occurred sending notification", ex); } }); } 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; } /// /// Replaces the HTML symbols with the character equivalent. /// /// The old string. 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(""", "\""); } /// /// Compares the text. /// /// The old text. /// The new text. /// if set to true [display inserted text]. /// if set to true [display deleted text]. /// The inserted style. /// The deleted style. /// 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(""); } // 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(""); } // if } // while // write rest of unchanged chars while (pos < newText.Length) { sb.Append(newText[pos]); pos++; } // while return sb.ToString(); } #endregion } }