using System; using System.Collections.Generic; using System.Net; using System.Text; using System.Text.RegularExpressions; using ScrewTurn.Wiki.PluginFramework; using System.Web; using System.Globalization; namespace ScrewTurn.Wiki { /// /// Allows access to the Pages. /// public static class Pages { #region Namespaces /// /// Gets all the namespaces, sorted. /// /// The namespaces, sorted. public static List GetNamespaces() { List result = new List(10); int count = 0; foreach(IPagesStorageProviderV30 provider in Collectors.PagesProviderCollector.AllProviders) { count++; result.AddRange(provider.GetNamespaces()); } if(count > 1) { result.Sort(new NamespaceComparer()); } return result; } /// /// Finds a namespace. /// /// The name of the namespace to find. /// The namespace, or null if no namespace is found. public static NamespaceInfo FindNamespace(string name) { if(string.IsNullOrEmpty(name)) return null; IPagesStorageProviderV30 defProv = Collectors.PagesProviderCollector.GetProvider(Settings.DefaultPagesProvider); NamespaceInfo nspace = defProv.GetNamespace(name); if(nspace != null) return nspace; foreach(IPagesStorageProviderV30 prov in Collectors.PagesProviderCollector.AllProviders) { if(prov != defProv) { nspace = prov.GetNamespace(name); if(nspace != null) return nspace; } } return null; } /// /// Finds a namespace. /// /// The name of the namespace to find. /// The provider to look into. /// The namespace, or null if the namespace is not found. public static NamespaceInfo FindNamespace(string name, IPagesStorageProviderV30 provider) { if(string.IsNullOrEmpty(name)) return null; return provider.GetNamespace(name); } /// /// Creates a new namespace in the default pages storage provider. /// /// The name of the namespace to add. /// true if the namespace is created, false otherwise. public static bool CreateNamespace(string name) { return CreateNamespace(name, Collectors.PagesProviderCollector.GetProvider(Settings.DefaultPagesProvider)); } /// /// Creates a new namespace. /// /// The name of the namespace to add. /// The provider to create the namespace into. /// true if the namespace is created, false otherwise. public static bool CreateNamespace(string name, IPagesStorageProviderV30 provider) { if(provider.ReadOnly) return false; if(FindNamespace(name) != null) return false; NamespaceInfo result = provider.AddNamespace(name); if(result != null) { InitMetaDataItems(name); AuthWriter.ClearEntriesForNamespace(name, new List()); Cache.ClearPseudoCache(); Cache.ClearPageCache(); Host.Instance.OnNamespaceActivity(result, null, NamespaceActivity.NamespaceAdded); Log.LogEntry("Namespace " + name + " created", EntryType.General, Log.SystemUsername); return true; } else { Log.LogEntry("Namespace creation failed for " + name, EntryType.Error, Log.SystemUsername); return false; } } /// /// Removes a namespace. /// /// The namespace to remove. /// true if the namespace is removed, false otherwise. public static bool RemoveNamespace(NamespaceInfo nspace) { if(nspace.Provider.ReadOnly) return false; NamespaceInfo realNspace = FindNamespace(nspace.Name); if(realNspace == null) return false; List pages = GetPages(realNspace); bool done = realNspace.Provider.RemoveNamespace(realNspace); if(done) { DeleteAllAttachments(pages); ResetMetaDataItems(nspace.Name); AuthWriter.ClearEntriesForNamespace(nspace.Name, pages.ConvertAll((p) => { return NameTools.GetLocalName(p.FullName); })); Cache.ClearPseudoCache(); Cache.ClearPageCache(); Host.Instance.OnNamespaceActivity(realNspace, null, NamespaceActivity.NamespaceRemoved); Log.LogEntry("Namespace " + realNspace.Name + " removed", EntryType.General, Log.SystemUsername); return true; } else { Log.LogEntry("Namespace deletion failed for " + realNspace.Name, EntryType.General, Log.SystemUsername); return false; } } /// /// Deletes all page attachments for a whole namespace. /// /// The pages in the namespace. private static void DeleteAllAttachments(List pages) { foreach(IFilesStorageProviderV30 prov in Collectors.FilesProviderCollector.AllProviders) { foreach(PageInfo page in pages) { string[] attachments = prov.ListPageAttachments(page); foreach(string attachment in attachments) { prov.DeletePageAttachment(page, attachment); } } } } /// /// Renames a namespace. /// /// The namespace to rename. /// The new name. /// true if the namespace is removed, false otherwise. public static bool RenameNamespace(NamespaceInfo nspace, string newName) { if(nspace.Provider.ReadOnly) return false; NamespaceInfo realNspace = FindNamespace(nspace.Name); if(realNspace == null) return false; if(FindNamespace(newName) != null) return false; List pages = GetPages(nspace); List pageNames = new List(pages.Count); foreach(PageInfo page in pages) pageNames.Add(NameTools.GetLocalName(page.FullName)); pages = null; string oldName = nspace.Name; NamespaceInfo newNspace = realNspace.Provider.RenameNamespace(realNspace, newName); if(newNspace != null) { NotifyFilesProvidersForNamespaceRename(pageNames, oldName, newName); UpdateMetaDataItems(oldName, newName); AuthWriter.ClearEntriesForNamespace(newName, new List()); AuthWriter.ProcessNamespaceRenaming(oldName, pageNames, newName); Cache.ClearPseudoCache(); Cache.ClearPageCache(); Host.Instance.OnNamespaceActivity(newNspace, oldName, NamespaceActivity.NamespaceRenamed); Log.LogEntry("Namespace " + nspace.Name + " renamed to " + newName, EntryType.General, Log.SystemUsername); return true; } else { Log.LogEntry("Namespace rename failed for " + nspace.Name, EntryType.General, Log.SystemUsername); return false; } } /// /// Notifies all files providers that a namespace was renamed. /// /// The pages in the renamed namespace. /// The name of the renamed namespace. /// The new name of the namespace. private static void NotifyFilesProvidersForNamespaceRename(List pages, string nspace, string newName) { foreach(IFilesStorageProviderV30 prov in Collectors.FilesProviderCollector.AllProviders) { foreach(string page in pages) { PageInfo pageToNotify = new PageInfo(NameTools.GetFullName(nspace, page), null, DateTime.Now); PageInfo newPage = new PageInfo(NameTools.GetFullName(newName, page), null, DateTime.Now); prov.NotifyPageRenaming(pageToNotify, newPage); } } } /// /// Initializes the namespace-specific meta-data items for a namespace. /// /// The namespace to initialize meta-data items for. private static void InitMetaDataItems(string nspace) { // Footer, Header, HtmlHead, PageFooter, PageHeader, Sidebar Settings.Provider.SetMetaDataItem(MetaDataItem.EditNotice, nspace, Defaults.EditNoticeContent); Settings.Provider.SetMetaDataItem(MetaDataItem.Footer, nspace, Defaults.FooterContent); Settings.Provider.SetMetaDataItem(MetaDataItem.Header, nspace, Defaults.HeaderContent); Settings.Provider.SetMetaDataItem(MetaDataItem.HtmlHead, nspace, ""); Settings.Provider.SetMetaDataItem(MetaDataItem.PageFooter, nspace, ""); Settings.Provider.SetMetaDataItem(MetaDataItem.PageHeader, nspace, ""); Settings.Provider.SetMetaDataItem(MetaDataItem.Sidebar, nspace, Defaults.SidebarContentForSubNamespace); } /// /// Resets the namespace-specific meta-data items for a namespace. /// /// The namespace to reset meta-data items for. private static void ResetMetaDataItems(string nspace) { // Footer, Header, HtmlHead, PageFooter, PageHeader, Sidebar Settings.Provider.SetMetaDataItem(MetaDataItem.EditNotice, nspace, ""); Settings.Provider.SetMetaDataItem(MetaDataItem.Footer, nspace, ""); Settings.Provider.SetMetaDataItem(MetaDataItem.Header, nspace, ""); Settings.Provider.SetMetaDataItem(MetaDataItem.HtmlHead, nspace, ""); Settings.Provider.SetMetaDataItem(MetaDataItem.PageFooter, nspace, ""); Settings.Provider.SetMetaDataItem(MetaDataItem.PageHeader, nspace, ""); Settings.Provider.SetMetaDataItem(MetaDataItem.Sidebar, nspace, ""); } /// /// Updates the namespace-specific meta-data items for a namespace when it is renamed. /// /// The renamed namespace to update the meta-data items for. /// The new name of the namespace. private static void UpdateMetaDataItems(string nspace, string newName) { // Footer, Header, HtmlHead, PageFooter, PageHeader, Sidebar Settings.Provider.SetMetaDataItem(MetaDataItem.EditNotice, newName, Settings.Provider.GetMetaDataItem(MetaDataItem.EditNotice, nspace)); Settings.Provider.SetMetaDataItem(MetaDataItem.Footer, newName, Settings.Provider.GetMetaDataItem(MetaDataItem.Footer, nspace)); Settings.Provider.SetMetaDataItem(MetaDataItem.Header, newName, Settings.Provider.GetMetaDataItem(MetaDataItem.Header, nspace)); Settings.Provider.SetMetaDataItem(MetaDataItem.HtmlHead, newName, Settings.Provider.GetMetaDataItem(MetaDataItem.HtmlHead, nspace)); Settings.Provider.SetMetaDataItem(MetaDataItem.PageFooter, newName, Settings.Provider.GetMetaDataItem(MetaDataItem.PageFooter, nspace)); Settings.Provider.SetMetaDataItem(MetaDataItem.PageHeader, newName, Settings.Provider.GetMetaDataItem(MetaDataItem.PageHeader, nspace)); Settings.Provider.SetMetaDataItem(MetaDataItem.Sidebar, newName, Settings.Provider.GetMetaDataItem(MetaDataItem.Sidebar, nspace)); ResetMetaDataItems(nspace); } /// /// Sets the default page of a namespace. /// /// The namespace (null for the root). /// The page. /// true if the default page is set, false otherwise. public static bool SetNamespaceDefaultPage(NamespaceInfo nspace, PageInfo page) { if(nspace == null) { // Root namespace, default to classic settings storage Settings.DefaultPage = page.FullName; return true; } if(nspace.Provider.ReadOnly) return false; NamespaceInfo pageNamespace = FindNamespace(NameTools.GetNamespace(page.FullName), page.Provider); if(pageNamespace == null) return false; NamespaceComparer comp = new NamespaceComparer(); if(comp.Compare(pageNamespace, nspace) != 0) return false; NamespaceInfo result = pageNamespace.Provider.SetNamespaceDefaultPage(nspace, page); if(result != null) { Host.Instance.OnNamespaceActivity(result, null, NamespaceActivity.NamespaceModified); Log.LogEntry("Default Page set for " + nspace.Name + " (" + page.FullName + ")", EntryType.General, Log.SystemUsername); return true; } else { Log.LogEntry("Default Page setting failed for " + nspace.Name + " (" + page.FullName + ")", EntryType.Error, Log.SystemUsername); return false; } } #endregion #region Pages /// /// Finds a Page. /// /// The full name of the page to find (case unsensitive). /// The correct object, if any, null otherwise. public static PageInfo FindPage(string fullName) { if(string.IsNullOrEmpty(fullName)) return null; IPagesStorageProviderV30 defProv = Collectors.PagesProviderCollector.GetProvider(Settings.DefaultPagesProvider); PageInfo page = defProv.GetPage(fullName); if(page != null) return page; foreach(IPagesStorageProviderV30 prov in Collectors.PagesProviderCollector.AllProviders) { if(prov != defProv) { page = prov.GetPage(fullName); if(page != null) return page; } } return null; } /// /// Finds a Page in a specific Provider. /// /// The full name of the page to find (case unsensitive). /// The Provider. /// The correct object, if any, null otherwise. public static PageInfo FindPage(string fullName, IPagesStorageProviderV30 provider) { if(string.IsNullOrEmpty(fullName)) return null; return provider.GetPage(fullName); } /// /// Gets a page draft, if any. /// /// The draft content, or null if no draft exists. public static PageContent GetDraft(PageInfo page) { if(page == null) return null; return page.Provider.GetDraft(page); } /// /// Deletes the draft of a page (if any). /// /// The page of which to delete the draft. public static void DeleteDraft(PageInfo page) { if(page == null) return; if(page.Provider.GetDraft(page) != null) { page.Provider.DeleteDraft(page); } } /// /// Gets the Backups/Revisions of a Page. /// /// The Page. /// The list of available Backups/Revision numbers. public static List GetBackups(PageInfo page) { int[] temp = page.Provider.GetBackups(page); if(temp == null) return null; else return new List(temp); } /// /// Gets the Content of a Page Backup. /// /// The Page. /// The Backup/Revision number. /// The Content of the Backup. public static PageContent GetBackupContent(PageInfo page, int revision) { return page.Provider.GetBackupContent(page, revision); } /// /// Deletes all the backups of a page. /// /// The Page. public static bool DeleteBackups(PageInfo page) { return DeleteBackups(page, -1); } /// /// Deletes a subset of the backups of a page. /// /// The Page. /// The first backup to be deleted (this backup and older backups are deleted). public static bool DeleteBackups(PageInfo page, int firstToDelete) { if(page.Provider.ReadOnly) return false; bool done = page.Provider.DeleteBackups(page, firstToDelete); if(done) { Log.LogEntry("Backups (0-" + firstToDelete.ToString() + ") deleted for " + page.FullName, EntryType.General, Log.SystemUsername); Host.Instance.OnPageActivity(page, null, SessionFacade.GetCurrentUsername(), PageActivity.PageBackupsDeleted); } else { Log.LogEntry("Backups (0-" + firstToDelete.ToString() + ") deletion failed for " + page.FullName, EntryType.Error, Log.SystemUsername); } return done; } /// /// Performs the rollpack of a Page. /// /// The Page. /// The revision to rollback the Page to. public static bool Rollback(PageInfo page, int version) { if(page.Provider.ReadOnly) return false; bool done = page.Provider.RollbackPage(page, version); if(done) { Content.InvalidatePage(page); PageContent pageContent = Content.GetPageContent(page, false); // Update page's outgoing links string[] linkedPages; Formatter.Format(pageContent.Content, false, FormattingContext.PageContent, page, out linkedPages); string[] outgoingLinks = new string[linkedPages.Length]; for(int i = 0; i < outgoingLinks.Length; i++) { outgoingLinks[i] = linkedPages[i]; } Settings.Provider.StoreOutgoingLinks(page.FullName, outgoingLinks); Log.LogEntry("Rollback executed for " + page.FullName + " at revision " + version.ToString(), EntryType.General, Log.SystemUsername); RecentChanges.AddChange(page.FullName, pageContent.Title, null, DateTime.Now, SessionFacade.GetCurrentUsername(), Change.PageRolledBack, ""); Host.Instance.OnPageActivity(page, null, SessionFacade.GetCurrentUsername(), PageActivity.PageRolledBack); return true; } else { Log.LogEntry("Rollback failed for " + page.FullName + " at revision " + version.ToString(), EntryType.Error, Log.SystemUsername); return false; } } /// /// Creates a new Page. /// /// The target namespace (null for the root). /// The Page name. /// true if the Page is created, false otherwise. public static bool CreatePage(NamespaceInfo nspace, string name) { string namespaceName = nspace != null ? nspace.Name : null; return CreatePage(namespaceName, name, nspace != null ? nspace.Provider : null); } /// /// Creates a new Page. /// /// The target namespace (null for the root). /// The Page name. /// true if the Page is created, false otherwise. public static bool CreatePage(string nspace, string name) { return CreatePage(nspace, name, Collectors.PagesProviderCollector.GetProvider(Settings.DefaultPagesProvider)); } /// /// Creates a new Page. /// /// The target namespace (null for the root). /// The Page name. /// The destination provider. /// true if the Page is created, false otherwise. public static bool CreatePage(NamespaceInfo nspace, string name, IPagesStorageProviderV30 provider) { string namespaceName = nspace != null ? nspace.Name : null; return CreatePage(namespaceName, name, provider); } /// /// Creates a new Page. /// /// The target namespace (null for the root). /// The Page name. /// The destination provider. /// true if the Page is created, false otherwise. public static bool CreatePage(string nspace, string name, IPagesStorageProviderV30 provider) { if(provider.ReadOnly) return false; string fullName = NameTools.GetFullName(nspace, name); if(FindPage(fullName) != null) return false; PageInfo newPage = provider.AddPage(nspace, name, DateTime.Now); if(newPage != null) { AuthWriter.ClearEntriesForPage(fullName); Content.InvalidateAllPages(); Content.ClearPseudoCache(); Log.LogEntry("Page " + fullName + " created", EntryType.General, Log.SystemUsername); Host.Instance.OnPageActivity(newPage, null, SessionFacade.GetCurrentUsername(), PageActivity.PageCreated); return true; } else { Log.LogEntry("Page creation failed for " + fullName, EntryType.Error, Log.SystemUsername); return false; } } /// /// Deletes a Page. /// /// The Page to delete. public static bool DeletePage(PageInfo page) { if(page.Provider.ReadOnly) return false; string title = Content.GetPageContent(page, false).Title; bool done = page.Provider.RemovePage(page); if(done) { AuthWriter.ClearEntriesForPage(page.FullName); // Remove the deleted page from the Breadcrumbs Trail and Redirections list SessionFacade.Breadcrumbs.RemovePage(page); Redirections.WipePageOut(page); // Cleanup Cache Content.InvalidatePage(page); Content.ClearPseudoCache(); // Remove outgoing links Settings.Provider.DeleteOutgoingLinks(page.FullName); Log.LogEntry("Page " + page.FullName + " deleted", EntryType.General, Log.SystemUsername); RecentChanges.AddChange(page.FullName, title, null, DateTime.Now, SessionFacade.GetCurrentUsername(), Change.PageDeleted, ""); Host.Instance.OnPageActivity(page, null, SessionFacade.GetCurrentUsername(), PageActivity.PageDeleted); return true; } else { Log.LogEntry("Page deletion failed for " + page.FullName, EntryType.Error, Log.SystemUsername); return false; } } /// /// Renames a Page. /// /// The Page to rename. /// The new name. public static bool RenamePage(PageInfo page, string name) { if(page.Provider.ReadOnly) return false; string newFullName = NameTools.GetFullName(NameTools.GetNamespace(page.FullName), NameTools.GetLocalName(name)); if(FindPage(newFullName) != null) return false; string oldName = page.FullName; string title = Content.GetPageContent(page, false).Title; PageInfo pg = page.Provider.RenamePage(page, name); if(pg != null) { AuthWriter.ClearEntriesForPage(newFullName); AuthWriter.ProcessPageRenaming(oldName, newFullName); SessionFacade.Breadcrumbs.RemovePage(page); Redirections.Clear(); Content.InvalidateAllPages(); Content.ClearPseudoCache(); // Page redirect is implemented directly in AdminPages.aspx.cs Log.LogEntry("Page " + page.FullName + " renamed to " + name, EntryType.General, Log.SystemUsername); RecentChanges.AddChange(page.FullName, title, null, DateTime.Now, SessionFacade.GetCurrentUsername(), Change.PageRenamed, ""); Host.Instance.OnPageActivity(page, oldName, SessionFacade.GetCurrentUsername(), PageActivity.PageRenamed); return true; } else { Log.LogEntry("Page rename failed for " + page.FullName + " (" + name + ")", EntryType.Error, Log.SystemUsername); return false; } } /// /// Migrates a page. /// /// The page to migrate. /// The target namespace. /// A value indicating whether to copy the page categories to the target namespace. /// true if the page is migrated, false otherwise. public static bool MigratePage(PageInfo page, NamespaceInfo targetNamespace, bool copyCategories) { PageInfo result = page.Provider.MovePage(page, targetNamespace, copyCategories); if(result != null) { Settings.Provider.StoreOutgoingLinks(page.FullName, new string[0]); PageContent content = Content.GetPageContent(result, false); StorePageOutgoingLinks(result, content.Content); } return result != null; } /// /// Modifies a Page. /// /// The Page to modify. /// The Title of the Page. /// The Username of the user who modified the Page. /// The Date/Time of the modification. /// The comment of the editor, about this revision. /// The Content. /// The keywords, usually used for SEO. /// The description, usually used for SEO. /// The save mode. /// True if the Page has been modified successfully. public static bool ModifyPage(PageInfo page, string title, string username, DateTime dateTime, string comment, string content, string[] keywords, string description, SaveMode saveMode) { if(page.Provider.ReadOnly) return false; StringBuilder sb = new StringBuilder(content); sb.Replace("~~~~", "§§(" + username + "," + dateTime.ToString("yyyy'/'MM'/'dd' 'HH':'mm':'ss") + ")§§"); content = sb.ToString(); // Because of transclusion and other page-linking features, it is necessary to clear the whole cache Content.ClearPseudoCache(); Content.InvalidateAllPages(); bool done = page.Provider.ModifyPage(page, title, username, dateTime, comment, content, keywords, description, saveMode); if(done) { Log.LogEntry("Page Content updated for " + page.FullName, EntryType.General, Log.SystemUsername); StorePageOutgoingLinks(page, content); if(saveMode != SaveMode.Draft) { RecentChanges.AddChange(page.FullName, title, null, dateTime, username, Change.PageUpdated, comment); Host.Instance.OnPageActivity(page, null, username, PageActivity.PageModified); SendEmailNotificationForPage(page, Users.FindUser(username)); } else { Host.Instance.OnPageActivity(page, null, username, PageActivity.PageDraftSaved); } if(saveMode == SaveMode.Backup) { // Delete old backups, if needed DeleteOldBackupsIfNeeded(page); } } else Log.LogEntry("Page Content update failed for " + page.FullName, EntryType.Error, Log.SystemUsername); return done; } /// /// Stores outgoing links for a page. /// /// The page. /// The raw content. public static void StorePageOutgoingLinks(PageInfo page, string content) { string[] linkedPages; Formatter.Format(content, false, FormattingContext.PageContent, page, out linkedPages); string lowercaseName = page.FullName.ToLowerInvariant(); // Avoid self-references List cleanLinkedPages = new List(linkedPages); for(int i = cleanLinkedPages.Count - 1; i >= 0; i--) { if(cleanLinkedPages[i] == null || cleanLinkedPages[i].Length == 0) { cleanLinkedPages.RemoveAt(i); } else if(cleanLinkedPages[i].ToLowerInvariant() == lowercaseName) { cleanLinkedPages.RemoveAt(i); } } bool doneLinks = Settings.Provider.StoreOutgoingLinks(page.FullName, cleanLinkedPages.ToArray()); if(!doneLinks) { Log.LogEntry("Could not store outgoing links for page " + page.FullName, EntryType.Error, Log.SystemUsername); } } /// /// Deletes the old backups if the current number of backups exceeds the limit. /// /// The page. private static void DeleteOldBackupsIfNeeded(PageInfo page) { int maxBackups = Settings.KeptBackupNumber; if(maxBackups == -1) return; // Oldest to newest: 0, 1, 2, 3 List backups = GetBackups(page); if(backups.Count > maxBackups) { backups.Reverse(); DeleteBackups(page, backups[maxBackups]); } } /// /// Removes a user from an array. /// /// The array of users. /// The user to remove. /// The resulting array without the specified user. private static UserInfo[] RemoveUserFromArray(UserInfo[] users, UserInfo userToRemove) { if(userToRemove == null) return users; List temp = new List(users); UsernameComparer comp = new UsernameComparer(); temp.RemoveAll(delegate(UserInfo elem) { return comp.Compare(elem, userToRemove) == 0; }); return temp.ToArray(); } /// /// Sends the email notification for a page change. /// /// The page that was modified. /// The author of the modification. private static void SendEmailNotificationForPage(PageInfo page, UserInfo author) { if(page == null) return; PageContent content = Content.GetPageContent(page, false); UserInfo[] usersToNotify = Users.GetUsersToNotifyForPageChange(page); usersToNotify = RemoveUserFromArray(usersToNotify, author); string[] recipients = EmailTools.GetRecipients(usersToNotify); string body = Settings.Provider.GetMetaDataItem(MetaDataItem.PageChangeMessage, null); string title = FormattingPipeline.PrepareTitle(content.Title, false, FormattingContext.Other, page); EmailTools.AsyncSendMassEmail(recipients, Settings.SenderEmail, Settings.WikiTitle + " - " + title, body.Replace("##PAGE##", title).Replace("##USER##", content.User).Replace("##DATETIME##", Preferences.AlignWithServerTimezone(content.LastModified).ToString(Settings.DateTimeFormat)).Replace("##COMMENT##", (string.IsNullOrEmpty(content.Comment) ? Exchanger.ResourceExchanger.GetResource("None") : content.Comment)).Replace("##LINK##", Settings.MainUrl + Tools.UrlEncode(page.FullName) + Settings.PageExtension).Replace("##WIKITITLE##", Settings.WikiTitle), false); } /// /// Determines whether a user can edit a page. /// /// The page. /// The username. /// The groups. /// A value indicating whether the user can edit the page. /// A value indicating whether the user can edit the page with subsequent approval. public static void CanEditPage(PageInfo page, string username, string[] groups, out bool canEdit, out bool canEditWithApproval) { canEdit = false; canEditWithApproval = false; switch(Settings.ChangeModerationMode) { case ChangeModerationMode.RequirePageEditingPermissions: canEdit = AuthChecker.CheckActionForPage(page, Actions.ForPages.ManagePage, username, groups); canEditWithApproval = AuthChecker.CheckActionForPage(page, Actions.ForPages.ModifyPage, username, groups); break; case ChangeModerationMode.RequirePageViewingPermissions: canEdit = AuthChecker.CheckActionForPage(page, Actions.ForPages.ModifyPage, username, groups); canEditWithApproval = AuthChecker.CheckActionForPage(page, Actions.ForPages.ReadPage, username, groups); break; case ChangeModerationMode.None: canEdit = AuthChecker.CheckActionForPage(page, Actions.ForPages.ModifyPage, username, groups); canEditWithApproval = false; break; } if(canEditWithApproval && canEdit) canEditWithApproval = false; if(canEdit && !string.IsNullOrEmpty(Settings.IpHostFilter)) canEdit = VerifyIpHostFilter(); } /// /// Verifies whether or not the current user's ip address is in the host filter or not. /// /// private static bool VerifyIpHostFilter() { const RegexOptions options = RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace; var hostAddress = HttpContext.Current.Request.UserHostAddress; var ips = Settings.IpHostFilter.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); // For each IP in the host filter setting foreach(var ip in ips) { // Split each by the . var digits = ip.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries); var regExpression = string.Empty; foreach(var digit in digits) { // Build a regex to check against the host ip. if(regExpression != string.Empty) regExpression += "\\."; if(digit == "*") regExpression += "\\d{1,3}"; else regExpression += digit; } // If we match, then the user is in the filter, return false. var regex = new Regex(regExpression, options); if(regex.IsMatch(hostAddress)) return false; } return true; // Made it here, then all is well, return true. } /// /// Determines whether a user can approve/reject a draft of a page. /// /// The page. /// The username. /// The groups. /// true if the user can approve/reject a draft of the page, false otherwise. public static bool CanApproveDraft(PageInfo page, string username, string[] groups) { string requiredAction = Actions.ForPages.ManagePage; // TODO: decide whether it is incorrect to require only ModifyPage permission /*switch(Settings.ChangeModerationMode) { case ChangeModerationMode.None: return; case ChangeModerationMode.RequirePageViewingPermissions: requiredAction = Actions.ForPages.ModifyPage; break; case ChangeModerationMode.RequirePageEditingPermissions: requiredAction = Actions.ForPages.ManagePage; break; default: throw new NotSupportedException(); }*/ return AuthChecker.CheckActionForPage(page, requiredAction, username, groups); } /// /// Sends a draft notification to "administrators". /// /// The edited page. /// The title. /// The comment. /// The author. public static void SendEmailNotificationForDraft(PageInfo currentPage, string title, string comment, string author) { // Decide the users to notify based on the ChangeModerationMode // Retrieve the list of matching users // Asynchronously send the notification // Retrieve all the users that have a grant on requiredAction for the current page // TODO: make this work when Users.GetUsers does not return all existing users but only a sub-set List usersToNotify = new List(10); foreach(UserInfo user in Users.GetUsers()) { if(CanApproveDraft(currentPage, user.Username, user.Groups)) { usersToNotify.Add(user); } } usersToNotify.Add(new UserInfo("admin", "Administrator", Settings.ContactEmail, true, DateTime.Now, null)); string subject = Settings.WikiTitle + " - " + Exchanger.ResourceExchanger.GetResource("ApproveRejectDraft") + ": " + title; string body = Settings.Provider.GetMetaDataItem(MetaDataItem.ApproveDraftMessage, null); body = body.Replace("##PAGE##", title).Replace("##USER##", author).Replace("##DATETIME##", Preferences.AlignWithServerTimezone(DateTime.Now).ToString(Settings.DateTimeFormat)).Replace("##COMMENT##", string.IsNullOrEmpty(comment) ? Exchanger.ResourceExchanger.GetResource("None") : comment).Replace("##LINK##", Settings.MainUrl + UrlTools.BuildUrl("Edit.aspx?Page=", Tools.UrlEncode(currentPage.FullName))).Replace("##LINK2##", Settings.MainUrl + "AdminPages.aspx?Admin=" + Tools.UrlEncode(currentPage.FullName)).Replace("##WIKITITLE##", Settings.WikiTitle); EmailTools.AsyncSendMassEmail(EmailTools.GetRecipients(usersToNotify.ToArray()), Settings.SenderEmail, subject, body, false); } /// /// Gets the list of all the Pages of a namespace. /// /// The namespace (null for the root). /// The pages. public static List GetPages(NamespaceInfo nspace) { List allPages = new List(10000); // Retrieve all pages from Pages Providers int count = 0; foreach(IPagesStorageProviderV30 provider in Collectors.PagesProviderCollector.AllProviders) { count++; allPages.AddRange(provider.GetPages(nspace)); } if(count > 1) { allPages.Sort(new PageNameComparer()); } return allPages; } /// /// Gets the global number of pages. /// /// The number of pages. public static int GetGlobalPageCount() { int count = 0; foreach(IPagesStorageProviderV30 prov in Collectors.PagesProviderCollector.AllProviders) { count += prov.GetPages(null).Length; foreach(NamespaceInfo nspace in prov.GetNamespaces()) { count += prov.GetPages(nspace).Length; } } return count; } /// /// Gets the incoming links for a page. /// /// The page. /// The incoming links. public static string[] GetPageIncomingLinks(PageInfo page) { if(page == null) return null; IDictionary allLinks = Settings.Provider.GetAllOutgoingLinks(); string[] knownPages = new string[allLinks.Count]; allLinks.Keys.CopyTo(knownPages, 0); List result = new List(20); foreach(string key in knownPages) { if(Contains(allLinks[key], page.FullName)) { // result is likely to be very small, so a linear search is fine if(!result.Contains(key)) result.Add(key); } } return result.ToArray(); } /// /// Gets the outgoing links of a page. /// /// The page. /// The outgoing links. public static string[] GetPageOutgoingLinks(PageInfo page) { if(page == null) return null; return Settings.Provider.GetOutgoingLinks(page.FullName); } /// /// Gets all the pages in a namespace without incoming links. /// /// The namespace (null for the root). /// The orphaned pages. public static PageInfo[] GetOrphanedPages(NamespaceInfo nspace) { List pages = GetPages(nspace); IDictionary allLinks = Settings.Provider.GetAllOutgoingLinks(); string[] knownPages = new string[allLinks.Count]; allLinks.Keys.CopyTo(knownPages, 0); Dictionary result = new Dictionary(pages.Count); foreach(PageInfo page in pages) { result.Add(page, false); foreach(string key in knownPages) { if(Contains(allLinks[key], page.FullName)) { // page has incoming links result[page] = true; } } } return ExtractNegativeKeys(result); } /// /// Gets the wanted/inexistent pages in all namespaces. /// /// The namespace (null for the root). /// The wanted/inexistent pages (dictionary wanted_page->linking_pages). public static Dictionary> GetWantedPages(string nspace) { if(string.IsNullOrEmpty(nspace)) nspace = null; IDictionary allLinks = Settings.Provider.GetAllOutgoingLinks(); string[] knownPages = new string[allLinks.Count]; allLinks.Keys.CopyTo(knownPages, 0); Dictionary> result = new Dictionary>(100); foreach(string key in knownPages) { foreach(string link in allLinks[key]) { string linkNamespace = NameTools.GetNamespace(link); if(linkNamespace == nspace) { PageInfo tempPage = FindPage(link); if(tempPage == null) { if(!result.ContainsKey(link)) result.Add(link, new List(3)); result[link].Add(key); } } } } return result; } /// /// Determines whether an array contains a value. /// /// The type of the elements in the array. /// The array. /// The value. /// true if the array contains the value, false otherwise. private static bool Contains(T[] array, T value) { return Array.IndexOf(array, value) >= 0; } /// /// Extracts the negative keys from a dictionary. /// /// The type of the key. /// The dictionary. /// The negative keys. private static T[] ExtractNegativeKeys(Dictionary data) { List result = new List(data.Count); foreach(KeyValuePair pair in data) { if(!pair.Value) result.Add(pair.Key); } return result.ToArray(); } #endregion #region Categories /// /// Finds a Category. /// /// The full name of the Category to Find (case unsensitive). /// The correct object or null if no category is found. public static CategoryInfo FindCategory(string fullName) { if(string.IsNullOrEmpty(fullName)) return null; IPagesStorageProviderV30 defProv = Collectors.PagesProviderCollector.GetProvider(Settings.DefaultPagesProvider); CategoryInfo category = defProv.GetCategory(fullName); if(category != null) return category; foreach(IPagesStorageProviderV30 prov in Collectors.PagesProviderCollector.AllProviders) { if(prov != defProv) { category = prov.GetCategory(fullName); if(category != null) return category; } } return null; } /// /// Creates a new Category. /// /// The target namespace (null for the root). /// The Name of the Category. /// true if the category is created, false otherwise. public static bool CreateCategory(NamespaceInfo nspace, string name) { string namespaceName = nspace != null ? nspace.Name : null; return CreateCategory(namespaceName, name); } /// /// Creates a new Category. /// /// The target namespace (null for the root). /// The Name of the Category. /// true if the category is created, false otherwise. public static bool CreateCategory(string nspace, string name) { return CreateCategory(nspace, name, Collectors.PagesProviderCollector.GetProvider(Settings.DefaultPagesProvider)); } /// /// Creates a new Category in the specifued Provider. /// /// The target namespace (null for the root). /// The Name of the Category. /// The Provider. /// true if the category is created, false otherwise. public static bool CreateCategory(NamespaceInfo nspace, string name, IPagesStorageProviderV30 provider) { string namespaceName = nspace != null ? nspace.Name : null; return CreateCategory(namespaceName, name, provider); } /// /// Creates a new Category in the specifued Provider. /// /// The target namespace (null for the root). /// The Name of the Category. /// The Provider. /// true if the category is created, false otherwise. public static bool CreateCategory(string nspace, string name, IPagesStorageProviderV30 provider) { if(provider == null) provider = Collectors.PagesProviderCollector.GetProvider(Settings.DefaultPagesProvider); if(provider.ReadOnly) return false; string fullName = NameTools.GetFullName(nspace, name); if(FindCategory(fullName) != null) return false; CategoryInfo newCat = provider.AddCategory(nspace, name); if(newCat != null) { Log.LogEntry("Category " + fullName + " created", EntryType.General, Log.SystemUsername); // Because of transclusion and other page-linking features, it is necessary to clear the whole cache Content.ClearPseudoCache(); Content.InvalidateAllPages(); return true; } else { Log.LogEntry("Category creation failed for " + fullName, EntryType.Error, Log.SystemUsername); return false; } } /// /// Removes a Category. /// /// The Category to remove. /// True if the Category has been removed successfully. public static bool RemoveCategory(CategoryInfo category) { if(category.Provider.ReadOnly) return false; bool done = category.Provider.RemoveCategory(category); if(done) Log.LogEntry("Category " + category.FullName + " removed", EntryType.General, Log.SystemUsername); else Log.LogEntry("Category deletion failed for " + category.FullName, EntryType.Error, Log.SystemUsername); // Because of transclusion and other page-linking features, it is necessary to clear the whole cache Content.ClearPseudoCache(); Content.InvalidateAllPages(); return done; } /// /// Renames a Category. /// /// The Category to rename. /// The new Name of the Category. /// True if the Category has been renamed successfully. public static bool RenameCategory(CategoryInfo category, string newName) { if(category.Provider.ReadOnly) return false; string newFullName = NameTools.GetFullName(NameTools.GetNamespace(category.FullName), newName); if(FindCategory(newFullName) != null) return false; string oldName = category.FullName; CategoryInfo newCat = category.Provider.RenameCategory(category, newName); if(newCat != null) Log.LogEntry("Category " + oldName + " renamed to " + newFullName, EntryType.General, Log.SystemUsername); else Log.LogEntry("Category rename failed for " + oldName + " (" + newFullName + ")", EntryType.Error, Log.SystemUsername); // Because of transclusion and other page-linking features, it is necessary to clear the whole cache Content.ClearPseudoCache(); Content.InvalidateAllPages(); return newCat != null; } /// /// Gets the Categories of a Page. /// /// The Page. /// The Categories of the Page. public static CategoryInfo[] GetCategoriesForPage(PageInfo page) { if(page == null) return new CategoryInfo[0]; CategoryInfo[] categories = page.Provider.GetCategoriesForPage(page); return categories; } /// /// Gets all the Uncategorized Pages. /// /// The namespace. /// The Uncategorized Pages. public static PageInfo[] GetUncategorizedPages(NamespaceInfo nspace) { if(nspace == null) { List pages = new List(1000); int count = 0; foreach(IPagesStorageProviderV30 prov in Collectors.PagesProviderCollector.AllProviders) { count++; pages.AddRange(prov.GetUncategorizedPages(null)); } if(count > 1) { pages.Sort(new PageNameComparer()); } return pages.ToArray(); } else { PageInfo[] pages = nspace.Provider.GetUncategorizedPages(nspace); return pages; } } /// /// Gets the valid Categories for a Page, i.e. the Categories managed by the Page's Provider and in the same namespace as the page. /// /// The Page, or null to use the default provider. /// The valid Categories. public static CategoryInfo[] GetAvailableCategories(PageInfo page) { NamespaceInfo pageNamespace = FindNamespace(NameTools.GetNamespace(page.FullName)); if(page != null) { return page.Provider.GetCategories(pageNamespace); } else { return Collectors.PagesProviderCollector.GetProvider(Settings.DefaultPagesProvider).GetCategories(pageNamespace); } } /// /// Gets the other Categories of the Provider and Namespace of the specified Category. /// /// The Category. /// The matching Categories. public static CategoryInfo[] GetMatchingCategories(CategoryInfo category) { NamespaceInfo nspace = FindNamespace(NameTools.GetNamespace(category.FullName)); List allCategories = GetCategories(nspace); List result = new List(10); for(int i = 0; i < allCategories.Count; i++) { if(allCategories[i].Provider == category.Provider && allCategories[i] != category) { result.Add(allCategories[i]); } } return result.ToArray(); } /// /// Binds a Page with some Categories. /// /// The Page to rebind. /// The Categories to bind the Page with. /// /// The specified Categories must be managed by the same Provider that manages the Page. /// The operation removes all the previous bindings. /// /// True if the binding succeeded. public static bool Rebind(PageInfo page, CategoryInfo[] cats) { if(page.Provider.ReadOnly) return false; string[] names = new string[cats.Length]; for(int i = 0; i < cats.Length; i++) { if(cats[i].Provider != page.Provider) return false; names[i] = cats[i].FullName; // Saves one cycle } bool done = page.Provider.RebindPage(page, names); if(done) Log.LogEntry("Page " + page.FullName + " rebound", EntryType.General, Log.SystemUsername); else Log.LogEntry("Page rebind failed for " + page.FullName, EntryType.Error, Log.SystemUsername); // Because of transclusion and other page-linking features, it is necessary to clear the whole cache Content.ClearPseudoCache(); Content.InvalidateAllPages(); return done; } /// /// Merges two Categories. /// /// The source Category. /// The destination Category. /// True if the Categories have been merged successfully. /// The destination Category remains, while the source Category is deleted, and all its Pages re-binded in the destination Category. /// The two Categories must have the same provider. public static bool MergeCategories(CategoryInfo source, CategoryInfo destination) { if(source.Provider != destination.Provider) return false; if(source.Provider.ReadOnly) return false; CategoryInfo newCat = source.Provider.MergeCategories(source, destination); if(newCat != null) Log.LogEntry("Category " + source.FullName + " merged into " + destination.FullName, EntryType.General, Log.SystemUsername); else Log.LogEntry("Categories merging failed for " + source.FullName + " into " + destination.FullName, EntryType.Error, Log.SystemUsername); // Because of transclusion and other page-linking features, it is necessary to clear the whole cache Content.ClearPseudoCache(); Content.InvalidateAllPages(); return newCat != null; } /// /// Gets the list of all the Categories of a namespace. The list shouldn't be modified. /// /// The namespace (null for the root). /// The categories, sorted by name. public static List GetCategories(NamespaceInfo nspace) { List allCategories = new List(50); // Retrieve all the categories from Pages Provider int count = 0; foreach(IPagesStorageProviderV30 provider in Collectors.PagesProviderCollector.AllProviders) { count++; allCategories.AddRange(provider.GetCategories(nspace)); } if(count > 1) { allCategories.Sort(new CategoryNameComparer()); } return allCategories; } #endregion #region Page Discussion /// /// Gets the Page Messages. /// /// The Page. /// The list of the first-level Messages, containing the replies properly nested. public static Message[] GetPageMessages(PageInfo page) { return page.Provider.GetMessages(page); } /// /// Gets the total number of Messages in a Page Discussion. /// /// The Page. /// The number of messages. public static int GetMessageCount(PageInfo page) { return page.Provider.GetMessageCount(page); } /// /// Finds a Message. /// /// The Messages. /// The Message ID. /// The Message or null. public static Message FindMessage(Message[] messages, int id) { Message result = null; for(int i = 0; i < messages.Length; i++) { if(messages[i].ID == id) { result = messages[i]; } if(result == null) { result = FindMessage(messages[i].Replies, id); } if(result != null) break; } return result; } /// /// Adds a new Message to a Page. /// /// The Page. /// The Username. /// The Subject. /// The Date/Time. /// The Body. /// The Parent Message ID, or -1. /// True if the Message has been added successfully. public static bool AddMessage(PageInfo page, string username, string subject, DateTime dateTime, string body, int parent) { if(page.Provider.ReadOnly) return false; bool done = page.Provider.AddMessage(page, username, subject, dateTime, body, parent); if(done) { SendEmailNotificationForMessage(page, Users.FindUser(username), Tools.GetMessageIdForAnchor(dateTime), subject); PageContent content = Content.GetPageContent(page, false); RecentChanges.AddChange(page.FullName, content.Title, subject, dateTime, username, Change.MessagePosted, ""); Host.Instance.OnPageActivity(page, null, username, PageActivity.MessagePosted); } return done; } /// /// Sends the email notification for a new message. /// /// The page the message was posted to. /// The author of the message. /// The message ID to be used for anchors. /// The message subject. private static void SendEmailNotificationForMessage(PageInfo page, UserInfo author, string id, string subject) { if(page == null) return; PageContent content = Content.GetPageContent(page, false); UserInfo[] usersToNotify = Users.GetUsersToNotifyForDiscussionMessages(page); usersToNotify = RemoveUserFromArray(usersToNotify, author); string[] recipients = EmailTools.GetRecipients(usersToNotify); string body = Settings.Provider.GetMetaDataItem(MetaDataItem.DiscussionChangeMessage, null); string title = FormattingPipeline.PrepareTitle(content.Title, false, FormattingContext.Other, page); EmailTools.AsyncSendMassEmail(recipients, Settings.SenderEmail, Settings.WikiTitle + " - " + title, body.Replace("##PAGE##", title).Replace("##USER##", author != null ? Users.GetDisplayName(author) : "anonymous").Replace("##DATETIME##", Preferences.AlignWithServerTimezone(content.LastModified).ToString(Settings.DateTimeFormat)).Replace("##SUBJECT##", subject).Replace("##LINK##", Settings.MainUrl + Tools.UrlEncode(page.FullName) + Settings.PageExtension + "?Discuss=1#" + id).Replace("##WIKITITLE##", Settings.WikiTitle), false); } /// /// Removes a Message. /// /// The Page. /// The ID of the Message to remove. /// A value specifying whether or not to remove the replies. /// True if the Message has been removed successfully. public static bool RemoveMessage(PageInfo page, int id, bool removeReplies) { if(page.Provider.ReadOnly) return false; Message[] messages = page.Provider.GetMessages(page); Message msg = FindMessage(messages, id); bool done = page.Provider.RemoveMessage(page, id, removeReplies); if(done) { PageContent content = Content.GetPageContent(page, false); RecentChanges.AddChange(page.FullName, content.Title, msg.Subject, DateTime.Now, msg.Username, Change.MessageDeleted, ""); Host.Instance.OnPageActivity(page, null, null, PageActivity.MessageDeleted); } return done; } /// /// Removes all messages of a Page. /// /// The Page. /// true if the messages are removed, false otherwise. public static bool RemoveAllMessages(PageInfo page) { if(page.Provider.ReadOnly) return false; Message[] messages = GetPageMessages(page); bool done = true; foreach(Message msg in messages) { done &= page.Provider.RemoveMessage(page, msg.ID, true); } return done; } /// /// Modifies a Message. /// /// The Page. /// The ID of the Message to modify. /// The Username. /// The Subject. /// The Date/Time. /// The Body. /// True if the Message has been modified successfully. public static bool ModifyMessage(PageInfo page, int id, string username, string subject, DateTime dateTime, string body) { if(page.Provider.ReadOnly) return false; bool done = page.Provider.ModifyMessage(page, id, username, subject, dateTime, body); if(done) { PageContent content = Content.GetPageContent(page, false); RecentChanges.AddChange(page.FullName, content.Title, subject, dateTime, username, Change.MessageEdited, ""); Host.Instance.OnPageActivity(page, null, username, PageActivity.MessageModified); } return done; } #endregion /// /// Checks the validity of a Page name. /// /// The Page name. /// True if the name is valid. public static bool IsValidName(string name) { if(name == null) return false; if(name.Replace(" ", "").Length == 0 || name.Length > 100 || name.Contains("?") || name.Contains("<") || name.Contains(">") || name.Contains("|") || name.Contains(":") || name.Contains("*") || name.Contains("\"") || name.Contains("/") || name.Contains("\\") || name.Contains("&") || name.Contains("%") || name.Contains("'") || name.Contains("\"") || name.Contains("+") || name.Contains(".") || name.Contains("#") || name.Contains("[") || name.Contains("]")) { return false; } else return true; } } /// /// Compares PageContent objects. /// public class PageContentDateComparer : IComparer { /// /// Compares two PageContent objects, using the DateTime as parameter. /// /// The first object. /// The second object. /// The result of the comparison (1, 0 or -1). public int Compare(PageContent x, PageContent y) { return x.LastModified.CompareTo(y.LastModified); } } }