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);
}
}
}