1527 lines
59 KiB
C#
1527 lines
59 KiB
C#
|
||
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 {
|
||
|
||
/// <summary>
|
||
/// Allows access to the Pages.
|
||
/// </summary>
|
||
public static class Pages {
|
||
|
||
#region Namespaces
|
||
|
||
/// <summary>
|
||
/// Gets all the namespaces, sorted.
|
||
/// </summary>
|
||
/// <returns>The namespaces, sorted.</returns>
|
||
public static List<NamespaceInfo> GetNamespaces() {
|
||
List<NamespaceInfo> result = new List<NamespaceInfo>(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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Finds a namespace.
|
||
/// </summary>
|
||
/// <param name="name">The name of the namespace to find.</param>
|
||
/// <returns>The namespace, or <c>null</c> if no namespace is found.</returns>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Finds a namespace.
|
||
/// </summary>
|
||
/// <param name="name">The name of the namespace to find.</param>
|
||
/// <param name="provider">The provider to look into.</param>
|
||
/// <returns>The namespace, or <c>null</c> if the namespace is not found.</returns>
|
||
public static NamespaceInfo FindNamespace(string name, IPagesStorageProviderV30 provider) {
|
||
if(string.IsNullOrEmpty(name)) return null;
|
||
|
||
return provider.GetNamespace(name);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates a new namespace in the default pages storage provider.
|
||
/// </summary>
|
||
/// <param name="name">The name of the namespace to add.</param>
|
||
/// <returns><c>true</c> if the namespace is created, <c>false</c> otherwise.</returns>
|
||
public static bool CreateNamespace(string name) {
|
||
return CreateNamespace(name, Collectors.PagesProviderCollector.GetProvider(Settings.DefaultPagesProvider));
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates a new namespace.
|
||
/// </summary>
|
||
/// <param name="name">The name of the namespace to add.</param>
|
||
/// <param name="provider">The provider to create the namespace into.</param>
|
||
/// <returns><c>true</c> if the namespace is created, <c>false</c> otherwise.</returns>
|
||
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<string>());
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Removes a namespace.
|
||
/// </summary>
|
||
/// <param name="nspace">The namespace to remove.</param>
|
||
/// <returns><c>true</c> if the namespace is removed, <c>false</c> otherwise.</returns>
|
||
public static bool RemoveNamespace(NamespaceInfo nspace) {
|
||
if(nspace.Provider.ReadOnly) return false;
|
||
|
||
NamespaceInfo realNspace = FindNamespace(nspace.Name);
|
||
if(realNspace == null) return false;
|
||
|
||
List<PageInfo> 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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Deletes all page attachments for a whole namespace.
|
||
/// </summary>
|
||
/// <param name="pages">The pages in the namespace.</param>
|
||
private static void DeleteAllAttachments(List<PageInfo> 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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Renames a namespace.
|
||
/// </summary>
|
||
/// <param name="nspace">The namespace to rename.</param>
|
||
/// <param name="newName">The new name.</param>
|
||
/// <returns><c>true</c> if the namespace is removed, <c>false</c> otherwise.</returns>
|
||
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<PageInfo> pages = GetPages(nspace);
|
||
List<string> pageNames = new List<string>(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<string>());
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Notifies all files providers that a namespace was renamed.
|
||
/// </summary>
|
||
/// <param name="pages">The pages in the renamed namespace.</param>
|
||
/// <param name="nspace">The name of the renamed namespace.</param>
|
||
/// <param name="newName">The new name of the namespace.</param>
|
||
private static void NotifyFilesProvidersForNamespaceRename(List<string> 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);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Initializes the namespace-specific meta-data items for a namespace.
|
||
/// </summary>
|
||
/// <param name="nspace">The namespace to initialize meta-data items for.</param>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Resets the namespace-specific meta-data items for a namespace.
|
||
/// </summary>
|
||
/// <param name="nspace">The namespace to reset meta-data items for.</param>
|
||
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, "");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Updates the namespace-specific meta-data items for a namespace when it is renamed.
|
||
/// </summary>
|
||
/// <param name="nspace">The renamed namespace to update the meta-data items for.</param>
|
||
/// <param name="newName">The new name of the namespace.</param>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Sets the default page of a namespace.
|
||
/// </summary>
|
||
/// <param name="nspace">The namespace (<c>null</c> for the root).</param>
|
||
/// <param name="page">The page.</param>
|
||
/// <returns><c>true</c> if the default page is set, <c>false</c> otherwise.</returns>
|
||
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
|
||
|
||
/// <summary>
|
||
/// Finds a Page.
|
||
/// </summary>
|
||
/// <param name="fullName">The full name of the page to find (case <b>unsensitive</b>).</param>
|
||
/// <returns>The correct <see cref="T:PageInfo" /> object, if any, <c>null</c> otherwise.</returns>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Finds a Page in a specific Provider.
|
||
/// </summary>
|
||
/// <param name="fullName">The full name of the page to find (case <b>unsensitive</b>).</param>
|
||
/// <param name="provider">The Provider.</param>
|
||
/// <returns>The correct <see cref="T:PageInfo" /> object, if any, <c>null</c> otherwise.</returns>
|
||
public static PageInfo FindPage(string fullName, IPagesStorageProviderV30 provider) {
|
||
if(string.IsNullOrEmpty(fullName)) return null;
|
||
|
||
return provider.GetPage(fullName);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets a page draft, if any.
|
||
/// </summary>
|
||
/// <param name="page">The draft content, or <c>null</c> if no draft exists.</param>
|
||
public static PageContent GetDraft(PageInfo page) {
|
||
if(page == null) return null;
|
||
|
||
return page.Provider.GetDraft(page);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Deletes the draft of a page (if any).
|
||
/// </summary>
|
||
/// <param name="page">The page of which to delete the draft.</param>
|
||
public static void DeleteDraft(PageInfo page) {
|
||
if(page == null) return;
|
||
|
||
if(page.Provider.GetDraft(page) != null) {
|
||
page.Provider.DeleteDraft(page);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets the Backups/Revisions of a Page.
|
||
/// </summary>
|
||
/// <param name="page">The Page.</param>
|
||
/// <returns>The list of available Backups/Revision numbers.</returns>
|
||
public static List<int> GetBackups(PageInfo page) {
|
||
int[] temp = page.Provider.GetBackups(page);
|
||
if(temp == null) return null;
|
||
else return new List<int>(temp);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets the Content of a Page Backup.
|
||
/// </summary>
|
||
/// <param name="page">The Page.</param>
|
||
/// <param name="revision">The Backup/Revision number.</param>
|
||
/// <returns>The Content of the Backup.</returns>
|
||
public static PageContent GetBackupContent(PageInfo page, int revision) {
|
||
return page.Provider.GetBackupContent(page, revision);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Deletes all the backups of a page.
|
||
/// </summary>
|
||
/// <param name="page">The Page.</param>
|
||
public static bool DeleteBackups(PageInfo page) {
|
||
return DeleteBackups(page, -1);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Deletes a subset of the backups of a page.
|
||
/// </summary>
|
||
/// <param name="page">The Page.</param>
|
||
/// <param name="firstToDelete">The first backup to be deleted (this backup and older backups are deleted).</param>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Performs the rollpack of a Page.
|
||
/// </summary>
|
||
/// <param name="page">The Page.</param>
|
||
/// <param name="version">The revision to rollback the Page to.</param>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates a new Page.
|
||
/// </summary>
|
||
/// <param name="nspace">The target namespace (<c>null</c> for the root).</param>
|
||
/// <param name="name">The Page name.</param>
|
||
/// <returns><c>true</c> if the Page is created, <c>false</c> otherwise.</returns>
|
||
public static bool CreatePage(NamespaceInfo nspace, string name) {
|
||
string namespaceName = nspace != null ? nspace.Name : null;
|
||
return CreatePage(namespaceName, name, nspace != null ? nspace.Provider : null);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates a new Page.
|
||
/// </summary>
|
||
/// <param name="nspace">The target namespace (<c>null</c> for the root).</param>
|
||
/// <param name="name">The Page name.</param>
|
||
/// <returns><c>true</c> if the Page is created, <c>false</c> otherwise.</returns>
|
||
public static bool CreatePage(string nspace, string name) {
|
||
return CreatePage(nspace, name, Collectors.PagesProviderCollector.GetProvider(Settings.DefaultPagesProvider));
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates a new Page.
|
||
/// </summary>
|
||
/// <param name="nspace">The target namespace (<c>null</c> for the root).</param>
|
||
/// <param name="name">The Page name.</param>
|
||
/// <param name="provider">The destination provider.</param>
|
||
/// <returns><c>true</c> if the Page is created, <c>false</c> otherwise.</returns>
|
||
public static bool CreatePage(NamespaceInfo nspace, string name, IPagesStorageProviderV30 provider) {
|
||
string namespaceName = nspace != null ? nspace.Name : null;
|
||
return CreatePage(namespaceName, name, provider);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates a new Page.
|
||
/// </summary>
|
||
/// <param name="nspace">The target namespace (<c>null</c> for the root).</param>
|
||
/// <param name="name">The Page name.</param>
|
||
/// <param name="provider">The destination provider.</param>
|
||
/// <returns><c>true</c> if the Page is created, <c>false</c> otherwise.</returns>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Deletes a Page.
|
||
/// </summary>
|
||
/// <param name="page">The Page to delete.</param>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Renames a Page.
|
||
/// </summary>
|
||
/// <param name="page">The Page to rename.</param>
|
||
/// <param name="name">The new name.</param>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Migrates a page.
|
||
/// </summary>
|
||
/// <param name="page">The page to migrate.</param>
|
||
/// <param name="targetNamespace">The target namespace.</param>
|
||
/// <param name="copyCategories">A value indicating whether to copy the page categories to the target namespace.</param>
|
||
/// <returns><c>true</c> if the page is migrated, <c>false</c> otherwise.</returns>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Modifies a Page.
|
||
/// </summary>
|
||
/// <param name="page">The Page to modify.</param>
|
||
/// <param name="title">The Title of the Page.</param>
|
||
/// <param name="username">The Username of the user who modified the Page.</param>
|
||
/// <param name="dateTime">The Date/Time of the modification.</param>
|
||
/// <param name="comment">The comment of the editor, about this revision.</param>
|
||
/// <param name="content">The Content.</param>
|
||
/// <param name="keywords">The keywords, usually used for SEO.</param>
|
||
/// <param name="description">The description, usually used for SEO.</param>
|
||
/// <param name="saveMode">The save mode.</param>
|
||
/// <returns>True if the Page has been modified successfully.</returns>
|
||
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("~~~~", "<22><>(" + username + "," + dateTime.ToString("yyyy'/'MM'/'dd' 'HH':'mm':'ss") + ")<29><>");
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Stores outgoing links for a page.
|
||
/// </summary>
|
||
/// <param name="page">The page.</param>
|
||
/// <param name="content">The raw content.</param>
|
||
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<string> cleanLinkedPages = new List<string>(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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Deletes the old backups if the current number of backups exceeds the limit.
|
||
/// </summary>
|
||
/// <param name="page">The page.</param>
|
||
private static void DeleteOldBackupsIfNeeded(PageInfo page) {
|
||
int maxBackups = Settings.KeptBackupNumber;
|
||
if(maxBackups == -1) return;
|
||
|
||
// Oldest to newest: 0, 1, 2, 3
|
||
List<int> backups = GetBackups(page);
|
||
if(backups.Count > maxBackups) {
|
||
backups.Reverse();
|
||
DeleteBackups(page, backups[maxBackups]);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Removes a user from an array.
|
||
/// </summary>
|
||
/// <param name="users">The array of users.</param>
|
||
/// <param name="userToRemove">The user to remove.</param>
|
||
/// <returns>The resulting array without the specified user.</returns>
|
||
private static UserInfo[] RemoveUserFromArray(UserInfo[] users, UserInfo userToRemove) {
|
||
if(userToRemove == null) return users;
|
||
|
||
List<UserInfo> temp = new List<UserInfo>(users);
|
||
UsernameComparer comp = new UsernameComparer();
|
||
temp.RemoveAll(delegate(UserInfo elem) { return comp.Compare(elem, userToRemove) == 0; });
|
||
|
||
return temp.ToArray();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Sends the email notification for a page change.
|
||
/// </summary>
|
||
/// <param name="page">The page that was modified.</param>
|
||
/// <param name="author">The author of the modification.</param>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Determines whether a user can edit a page.
|
||
/// </summary>
|
||
/// <param name="page">The page.</param>
|
||
/// <param name="username">The username.</param>
|
||
/// <param name="groups">The groups.</param>
|
||
/// <param name="canEdit">A value indicating whether the user can edit the page.</param>
|
||
/// <param name="canEditWithApproval">A value indicating whether the user can edit the page with subsequent approval.</param>
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Verifies whether or not the current user's ip address is in the host filter or not.
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
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.
|
||
}
|
||
|
||
/// <summary>
|
||
/// Determines whether a user can approve/reject a draft of a page.
|
||
/// </summary>
|
||
/// <param name="page">The page.</param>
|
||
/// <param name="username">The username.</param>
|
||
/// <param name="groups">The groups.</param>
|
||
/// <returns><c>true</c> if the user can approve/reject a draft of the page, <c>false</c> otherwise.</returns>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Sends a draft notification to "administrators".
|
||
/// </summary>
|
||
/// <param name="currentPage">The edited page.</param>
|
||
/// <param name="title">The title.</param>
|
||
/// <param name="comment">The comment.</param>
|
||
/// <param name="author">The author.</param>
|
||
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<UserInfo> usersToNotify = new List<UserInfo>(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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets the list of all the Pages of a namespace.
|
||
/// </summary>
|
||
/// <param name="nspace">The namespace (<c>null</c> for the root).</param>
|
||
/// <returns>The pages.</returns>
|
||
public static List<PageInfo> GetPages(NamespaceInfo nspace) {
|
||
List<PageInfo> allPages = new List<PageInfo>(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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets the global number of pages.
|
||
/// </summary>
|
||
/// <returns>The number of pages.</returns>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets the incoming links for a page.
|
||
/// </summary>
|
||
/// <param name="page">The page.</param>
|
||
/// <returns>The incoming links.</returns>
|
||
public static string[] GetPageIncomingLinks(PageInfo page) {
|
||
if(page == null) return null;
|
||
|
||
IDictionary<string, string[]> allLinks = Settings.Provider.GetAllOutgoingLinks();
|
||
string[] knownPages = new string[allLinks.Count];
|
||
allLinks.Keys.CopyTo(knownPages, 0);
|
||
|
||
List<string> result = new List<string>(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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets the outgoing links of a page.
|
||
/// </summary>
|
||
/// <param name="page">The page.</param>
|
||
/// <returns>The outgoing links.</returns>
|
||
public static string[] GetPageOutgoingLinks(PageInfo page) {
|
||
if(page == null) return null;
|
||
return Settings.Provider.GetOutgoingLinks(page.FullName);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets all the pages in a namespace without incoming links.
|
||
/// </summary>
|
||
/// <param name="nspace">The namespace (<c>null</c> for the root).</param>
|
||
/// <returns>The orphaned pages.</returns>
|
||
public static PageInfo[] GetOrphanedPages(NamespaceInfo nspace) {
|
||
List<PageInfo> pages = GetPages(nspace);
|
||
IDictionary<string, string[]> allLinks = Settings.Provider.GetAllOutgoingLinks();
|
||
string[] knownPages = new string[allLinks.Count];
|
||
allLinks.Keys.CopyTo(knownPages, 0);
|
||
|
||
Dictionary<PageInfo, bool> result = new Dictionary<PageInfo, bool>(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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets the wanted/inexistent pages in all namespaces.
|
||
/// </summary>
|
||
/// <param name="nspace">The namespace (<c>null</c> for the root).</param>
|
||
/// <returns>The wanted/inexistent pages (dictionary wanted_page->linking_pages).</returns>
|
||
public static Dictionary<string, List<string>> GetWantedPages(string nspace) {
|
||
if(string.IsNullOrEmpty(nspace)) nspace = null;
|
||
|
||
IDictionary<string, string[]> allLinks = Settings.Provider.GetAllOutgoingLinks();
|
||
string[] knownPages = new string[allLinks.Count];
|
||
allLinks.Keys.CopyTo(knownPages, 0);
|
||
|
||
Dictionary<string, List<string>> result = new Dictionary<string, List<string>>(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<string>(3));
|
||
result[link].Add(key);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Determines whether an array contains a value.
|
||
/// </summary>
|
||
/// <typeparam name="T">The type of the elements in the array.</typeparam>
|
||
/// <param name="array">The array.</param>
|
||
/// <param name="value">The value.</param>
|
||
/// <returns><c>true</c> if the array contains the value, <c>false</c> otherwise.</returns>
|
||
private static bool Contains<T>(T[] array, T value) {
|
||
return Array.IndexOf(array, value) >= 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Extracts the negative keys from a dictionary.
|
||
/// </summary>
|
||
/// <typeparam name="T">The type of the key.</typeparam>
|
||
/// <param name="data">The dictionary.</param>
|
||
/// <returns>The negative keys.</returns>
|
||
private static T[] ExtractNegativeKeys<T>(Dictionary<T, bool> data) {
|
||
List<T> result = new List<T>(data.Count);
|
||
|
||
foreach(KeyValuePair<T, bool> pair in data) {
|
||
if(!pair.Value) result.Add(pair.Key);
|
||
}
|
||
|
||
return result.ToArray();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Categories
|
||
|
||
/// <summary>
|
||
/// Finds a Category.
|
||
/// </summary>
|
||
/// <param name="fullName">The full name of the Category to Find (case <b>unsensitive</b>).</param>
|
||
/// <returns>The correct <see cref="T:CategoryInfo" /> object or <c>null</c> if no category is found.</returns>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates a new Category.
|
||
/// </summary>
|
||
/// <param name="nspace">The target namespace (<c>null</c> for the root).</param>
|
||
/// <param name="name">The Name of the Category.</param>
|
||
/// <returns><c>true</c> if the category is created, <c>false</c> otherwise.</returns>
|
||
public static bool CreateCategory(NamespaceInfo nspace, string name) {
|
||
string namespaceName = nspace != null ? nspace.Name : null;
|
||
return CreateCategory(namespaceName, name);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates a new Category.
|
||
/// </summary>
|
||
/// <param name="nspace">The target namespace (<c>null</c> for the root).</param>
|
||
/// <param name="name">The Name of the Category.</param>
|
||
/// <returns><c>true</c> if the category is created, <c>false</c> otherwise.</returns>
|
||
public static bool CreateCategory(string nspace, string name) {
|
||
return CreateCategory(nspace, name, Collectors.PagesProviderCollector.GetProvider(Settings.DefaultPagesProvider));
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates a new Category in the specifued Provider.
|
||
/// </summary>
|
||
/// <param name="nspace">The target namespace (<c>null</c> for the root).</param>
|
||
/// <param name="name">The Name of the Category.</param>
|
||
/// <param name="provider">The Provider.</param>
|
||
/// <returns><c>true</c> if the category is created, <c>false</c> otherwise.</returns>
|
||
public static bool CreateCategory(NamespaceInfo nspace, string name, IPagesStorageProviderV30 provider) {
|
||
string namespaceName = nspace != null ? nspace.Name : null;
|
||
return CreateCategory(namespaceName, name, provider);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates a new Category in the specifued Provider.
|
||
/// </summary>
|
||
/// <param name="nspace">The target namespace (<c>null</c> for the root).</param>
|
||
/// <param name="name">The Name of the Category.</param>
|
||
/// <param name="provider">The Provider.</param>
|
||
/// <returns><c>true</c> if the category is created, <c>false</c> otherwise.</returns>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Removes a Category.
|
||
/// </summary>
|
||
/// <param name="category">The Category to remove.</param>
|
||
/// <returns>True if the Category has been removed successfully.</returns>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Renames a Category.
|
||
/// </summary>
|
||
/// <param name="category">The Category to rename.</param>
|
||
/// <param name="newName">The new Name of the Category.</param>
|
||
/// <returns>True if the Category has been renamed successfully.</returns>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets the Categories of a Page.
|
||
/// </summary>
|
||
/// <param name="page">The Page.</param>
|
||
/// <returns>The Categories of the Page.</returns>
|
||
public static CategoryInfo[] GetCategoriesForPage(PageInfo page) {
|
||
if(page == null) return new CategoryInfo[0];
|
||
|
||
CategoryInfo[] categories = page.Provider.GetCategoriesForPage(page);
|
||
|
||
return categories;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets all the Uncategorized Pages.
|
||
/// </summary>
|
||
/// <param name="nspace">The namespace.</param>
|
||
/// <returns>The Uncategorized Pages.</returns>
|
||
public static PageInfo[] GetUncategorizedPages(NamespaceInfo nspace) {
|
||
if(nspace == null) {
|
||
List<PageInfo> pages = new List<PageInfo>(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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
/// <param name="page">The Page, or <c>null</c> to use the default provider.</param>
|
||
/// <returns>The valid Categories.</returns>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets the other Categories of the Provider and Namespace of the specified Category.
|
||
/// </summary>
|
||
/// <param name="category">The Category.</param>
|
||
/// <returns>The matching Categories.</returns>
|
||
public static CategoryInfo[] GetMatchingCategories(CategoryInfo category) {
|
||
NamespaceInfo nspace = FindNamespace(NameTools.GetNamespace(category.FullName));
|
||
|
||
List<CategoryInfo> allCategories = GetCategories(nspace);
|
||
List<CategoryInfo> result = new List<CategoryInfo>(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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Binds a Page with some Categories.
|
||
/// </summary>
|
||
/// <param name="page">The Page to rebind.</param>
|
||
/// <param name="cats">The Categories to bind the Page with.</param>
|
||
/// <remarks>
|
||
/// The specified Categories must be managed by the same Provider that manages the Page.
|
||
/// The operation removes all the previous bindings.
|
||
/// </remarks>
|
||
/// <returns>True if the binding succeeded.</returns>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Merges two Categories.
|
||
/// </summary>
|
||
/// <param name="source">The source Category.</param>
|
||
/// <param name="destination">The destination Category.</param>
|
||
/// <returns>True if the Categories have been merged successfully.</returns>
|
||
/// <remarks>The <b>destination</b> Category remains, while the <b>source</b> Category is deleted, and all its Pages re-binded in the <b>destination</b> Category.
|
||
/// The two Categories must have the same provider.</remarks>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets the list of all the Categories of a namespace. The list shouldn't be modified.
|
||
/// </summary>
|
||
/// <param name="nspace">The namespace (<c>null</c> for the root).</param>
|
||
/// <returns>The categories, sorted by name.</returns>
|
||
public static List<CategoryInfo> GetCategories(NamespaceInfo nspace) {
|
||
List<CategoryInfo> allCategories = new List<CategoryInfo>(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
|
||
|
||
/// <summary>
|
||
/// Gets the Page Messages.
|
||
/// </summary>
|
||
/// <param name="page">The Page.</param>
|
||
/// <returns>The list of the <b>first-level</b> Messages, containing the replies properly nested.</returns>
|
||
public static Message[] GetPageMessages(PageInfo page) {
|
||
return page.Provider.GetMessages(page);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets the total number of Messages in a Page Discussion.
|
||
/// </summary>
|
||
/// <param name="page">The Page.</param>
|
||
/// <returns>The number of messages.</returns>
|
||
public static int GetMessageCount(PageInfo page) {
|
||
return page.Provider.GetMessageCount(page);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Finds a Message.
|
||
/// </summary>
|
||
/// <param name="messages">The Messages.</param>
|
||
/// <param name="id">The Message ID.</param>
|
||
/// <returns>The Message or null.</returns>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Adds a new Message to a Page.
|
||
/// </summary>
|
||
/// <param name="page">The Page.</param>
|
||
/// <param name="username">The Username.</param>
|
||
/// <param name="subject">The Subject.</param>
|
||
/// <param name="dateTime">The Date/Time.</param>
|
||
/// <param name="body">The Body.</param>
|
||
/// <param name="parent">The Parent Message ID, or -1.</param>
|
||
/// <returns>True if the Message has been added successfully.</returns>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Sends the email notification for a new message.
|
||
/// </summary>
|
||
/// <param name="page">The page the message was posted to.</param>
|
||
/// <param name="author">The author of the message.</param>
|
||
/// <param name="id">The message ID to be used for anchors.</param>
|
||
/// <param name="subject">The message subject.</param>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Removes a Message.
|
||
/// </summary>
|
||
/// <param name="page">The Page.</param>
|
||
/// <param name="id">The ID of the Message to remove.</param>
|
||
/// <param name="removeReplies">A value specifying whether or not to remove the replies.</param>
|
||
/// <returns>True if the Message has been removed successfully.</returns>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Removes all messages of a Page.
|
||
/// </summary>
|
||
/// <param name="page">The Page.</param>
|
||
/// <returns><c>true</c> if the messages are removed, <c>false</c> otherwise.</returns>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Modifies a Message.
|
||
/// </summary>
|
||
/// <param name="page">The Page.</param>
|
||
/// <param name="id">The ID of the Message to modify.</param>
|
||
/// <param name="username">The Username.</param>
|
||
/// <param name="subject">The Subject.</param>
|
||
/// <param name="dateTime">The Date/Time.</param>
|
||
/// <param name="body">The Body.</param>
|
||
/// <returns>True if the Message has been modified successfully.</returns>
|
||
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
|
||
|
||
/// <summary>
|
||
/// Checks the validity of a Page name.
|
||
/// </summary>
|
||
/// <param name="name">The Page name.</param>
|
||
/// <returns>True if the name is valid.</returns>
|
||
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;
|
||
}
|
||
|
||
}
|
||
|
||
/// <summary>
|
||
/// Compares PageContent objects.
|
||
/// </summary>
|
||
public class PageContentDateComparer : IComparer<PageContent> {
|
||
|
||
/// <summary>
|
||
/// Compares two PageContent objects, using the DateTime as parameter.
|
||
/// </summary>
|
||
/// <param name="x">The first object.</param>
|
||
/// <param name="y">The second object.</param>
|
||
/// <returns>The result of the comparison (1, 0 or -1).</returns>
|
||
public int Compare(PageContent x, PageContent y) {
|
||
return x.LastModified.CompareTo(y.LastModified);
|
||
}
|
||
|
||
}
|
||
|
||
}
|