Commits

Christian Axelsson committed 6b5ccf0 Merge

Merge with default

Comments (0)

Files changed (134)

 e018cd85584a559631b86b8d997f196e40199905 3.0.2.500
 6329a8e8373a52a9a771ccb5edffbe407346c089 3.0.3.555
 5125963cb8b47a871b602034a85e271bbb6cc7c7 3.0.4.560
+cf79aae3a615092e1c1757eb86f2089df0abc0c0 3.0.5.600

AssemblyVersion.cs

 //
 // You can specify all the values or you can default the Revision and Build Numbers 
 // by using the '*' as shown below:
-[assembly: AssemblyVersion("3.0.5.600")]
-[assembly: AssemblyFileVersion("3.0.5.600")]
+[assembly: AssemblyVersion("3.0.5.628")]
+[assembly: AssemblyFileVersion("3.0.5.628")]

BackupRestore/BackupRestore.cs

+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using ScrewTurn.Wiki.PluginFramework;
+using System.Web.Script.Serialization;
+using System.Collections;
+using ScrewTurn.Wiki.AclEngine;
+using Ionic.Zip;
+using System.IO;
+
+namespace ScrewTurn.Wiki.BackupRestore {
+
+	/// <summary>
+	/// Implements a Backup and Restore procedure for settings storage providers.
+	/// </summary>
+	public static class BackupRestore {
+
+		private const string BACKUP_RESTORE_UTILITY_VERSION = "1.0";
+
+		private static VersionFile generateVersionFile(string backupName) {
+			return new VersionFile() {
+				BackupRestoreVersion = BACKUP_RESTORE_UTILITY_VERSION,
+				WikiVersion = typeof(BackupRestore).Assembly.GetName().Version.ToString(),
+				BackupName = backupName
+			};
+		}
+
+		/// <summary>
+		/// Backups all the providers (excluded global settings storage provider).
+		/// </summary>
+		/// <param name="backupZipFileName">The name of the zip file where to store the backup file.</param>
+		/// <param name="plugins">The available plugins.</param>
+		/// <param name="settingsStorageProvider">The settings storage provider.</param>
+		/// <param name="pagesStorageProviders">The pages storage providers.</param>
+		/// <param name="usersStorageProviders">The users storage providers.</param>
+		/// <param name="filesStorageProviders">The files storage providers.</param>
+		/// <returns><c>true</c> if the backup has been succesfull.</returns>
+		public static bool BackupAll(string backupZipFileName, string[] plugins, ISettingsStorageProviderV30 settingsStorageProvider, IPagesStorageProviderV30[] pagesStorageProviders, IUsersStorageProviderV30[] usersStorageProviders, IFilesStorageProviderV30[] filesStorageProviders) {
+			string tempPath = Path.Combine(Environment.GetEnvironmentVariable("TEMP"), Guid.NewGuid().ToString());
+			Directory.CreateDirectory(tempPath);
+
+			using(ZipFile backupZipFile = new ZipFile(backupZipFileName)) {
+
+				// Find all namespaces
+				List<string> namespaces = new List<string>();
+				foreach(IPagesStorageProviderV30 pagesStorageProvider in pagesStorageProviders) {
+					foreach(NamespaceInfo ns in pagesStorageProvider.GetNamespaces()) {
+						namespaces.Add(ns.Name);
+					}
+				}
+
+				// Backup settings storage provider
+				string zipSettingsBackup = Path.Combine(tempPath, "SettingsBackup-" + settingsStorageProvider.GetType().FullName + ".zip");
+				BackupSettingsStorageProvider(zipSettingsBackup, settingsStorageProvider, namespaces.ToArray(), plugins);
+				backupZipFile.AddFile(zipSettingsBackup, ""); 
+
+				// Backup pages storage providers
+				foreach(IPagesStorageProviderV30 pagesStorageProvider in pagesStorageProviders) {
+					string zipPagesBackup = Path.Combine(tempPath, "PagesBackup-" + pagesStorageProvider.GetType().FullName + ".zip");
+					BackupPagesStorageProvider(zipPagesBackup, pagesStorageProvider);
+					backupZipFile.AddFile(zipPagesBackup, "");
+				}
+
+				// Backup users storage providers
+				foreach(IUsersStorageProviderV30 usersStorageProvider in usersStorageProviders) {
+					string zipUsersProvidersBackup = Path.Combine(tempPath, "UsersBackup-" + usersStorageProvider.GetType().FullName + ".zip");
+					BackupUsersStorageProvider(zipUsersProvidersBackup, usersStorageProvider);
+					backupZipFile.AddFile(zipUsersProvidersBackup, "");
+				}
+
+				// Backup files storage providers
+				foreach(IFilesStorageProviderV30 filesStorageProvider in filesStorageProviders) {
+					string zipFilesProviderBackup = Path.Combine(tempPath, "FilesBackup-" + filesStorageProvider.GetType().FullName + ".zip");
+					BackupFilesStorageProvider(zipFilesProviderBackup, filesStorageProvider, pagesStorageProviders);
+					backupZipFile.AddFile(zipFilesProviderBackup, "");
+				}
+				backupZipFile.Save();
+			}
+
+			Directory.Delete(tempPath, true);
+			return true;
+		}
+
+		/// <summary>
+		/// Backups the specified settings provider.
+		/// </summary>
+		/// <param name="zipFileName">The zip file name where to store the backup.</param>
+		/// <param name="settingsStorageProvider">The source settings provider.</param>
+		/// <param name="knownNamespaces">The currently known page namespaces.</param>
+		/// <param name="knownPlugins">The currently known plugins.</param>
+		/// <returns><c>true</c> if the backup file has been succesfully created.</returns>
+		public static bool BackupSettingsStorageProvider(string zipFileName, ISettingsStorageProviderV30 settingsStorageProvider, string[] knownNamespaces, string[] knownPlugins) {
+			SettingsBackup settingsBackup = new SettingsBackup();
+
+			// Settings
+			settingsBackup.Settings = (Dictionary<string, string>)settingsStorageProvider.GetAllSettings();
+
+			// Plugins Status and Configuration
+			settingsBackup.PluginsFileNames = knownPlugins.ToList();
+			Dictionary<string, bool> pluginsStatus = new Dictionary<string, bool>();
+			Dictionary<string, string> pluginsConfiguration = new Dictionary<string, string>();
+			foreach(string plugin in knownPlugins) {
+				pluginsStatus[plugin] = settingsStorageProvider.GetPluginStatus(plugin);
+				pluginsConfiguration[plugin] = settingsStorageProvider.GetPluginConfiguration(plugin);
+			}
+			settingsBackup.PluginsStatus = pluginsStatus;
+			settingsBackup.PluginsConfiguration = pluginsConfiguration;
+
+			// Metadata
+			List<MetaData> metadataList = new List<MetaData>();
+			// Meta-data (global)
+			metadataList.Add(new MetaData() {
+				Item = MetaDataItem.AccountActivationMessage,
+				Tag = null,
+				Content = settingsStorageProvider.GetMetaDataItem(MetaDataItem.AccountActivationMessage, null)
+			});
+			metadataList.Add(new MetaData() { Item = MetaDataItem.PasswordResetProcedureMessage, Tag = null, Content = settingsStorageProvider.GetMetaDataItem(MetaDataItem.PasswordResetProcedureMessage, null) });
+			metadataList.Add(new MetaData() { Item = MetaDataItem.LoginNotice, Tag = null, Content = settingsStorageProvider.GetMetaDataItem(MetaDataItem.LoginNotice, null) });
+			metadataList.Add(new MetaData() { Item = MetaDataItem.PageChangeMessage, Tag = null, Content = settingsStorageProvider.GetMetaDataItem(MetaDataItem.PageChangeMessage, null) });
+			metadataList.Add(new MetaData() { Item = MetaDataItem.DiscussionChangeMessage, Tag = null, Content = settingsStorageProvider.GetMetaDataItem(MetaDataItem.DiscussionChangeMessage, null) });
+			// Meta-data (ns-specific)
+			List<string> namespacesToProcess = new List<string>();
+			namespacesToProcess.Add("");
+			namespacesToProcess.AddRange(knownNamespaces);
+			foreach(string nspace in namespacesToProcess) {
+				metadataList.Add(new MetaData() { Item = MetaDataItem.EditNotice, Tag = nspace, Content = settingsStorageProvider.GetMetaDataItem(MetaDataItem.EditNotice, nspace) });
+				metadataList.Add(new MetaData() { Item = MetaDataItem.Footer, Tag = nspace, Content = settingsStorageProvider.GetMetaDataItem(MetaDataItem.Footer, nspace) });
+				metadataList.Add(new MetaData() { Item = MetaDataItem.Header, Tag = nspace, Content = settingsStorageProvider.GetMetaDataItem(MetaDataItem.Header, nspace) });
+				metadataList.Add(new MetaData() { Item = MetaDataItem.HtmlHead, Tag = nspace, Content = settingsStorageProvider.GetMetaDataItem(MetaDataItem.HtmlHead, nspace) });
+				metadataList.Add(new MetaData() { Item = MetaDataItem.PageFooter, Tag = nspace, Content = settingsStorageProvider.GetMetaDataItem(MetaDataItem.PageFooter, nspace) });
+				metadataList.Add(new MetaData() { Item = MetaDataItem.PageHeader, Tag = nspace, Content = settingsStorageProvider.GetMetaDataItem(MetaDataItem.PageHeader, nspace) });
+				metadataList.Add(new MetaData() { Item = MetaDataItem.Sidebar, Tag = nspace, Content = settingsStorageProvider.GetMetaDataItem(MetaDataItem.Sidebar, nspace) });
+			}
+			settingsBackup.Metadata = metadataList;
+
+			// RecentChanges
+			settingsBackup.RecentChanges = settingsStorageProvider.GetRecentChanges().ToList();
+
+			// OutgoingLinks
+			settingsBackup.OutgoingLinks = (Dictionary<string, string[]>)settingsStorageProvider.GetAllOutgoingLinks();
+
+			// ACLEntries
+			AclEntry[] aclEntries = settingsStorageProvider.AclManager.RetrieveAllEntries();
+			settingsBackup.AclEntries = new List<AclEntryBackup>(aclEntries.Length);
+			foreach(AclEntry aclEntry in aclEntries) {
+				settingsBackup.AclEntries.Add(new AclEntryBackup() {
+					Action = aclEntry.Action,
+					Resource = aclEntry.Resource,
+					Subject = aclEntry.Subject,
+					Value = aclEntry.Value
+				});
+			}
+
+			JavaScriptSerializer javascriptSerializer = new JavaScriptSerializer();
+			javascriptSerializer.MaxJsonLength = javascriptSerializer.MaxJsonLength * 10;
+
+			string tempDir = Path.Combine(Environment.GetEnvironmentVariable("TEMP"), Guid.NewGuid().ToString());
+			Directory.CreateDirectory(tempDir);
+
+			FileStream tempFile = File.Create(Path.Combine(tempDir, "Settings.json"));
+			byte[] buffer = Encoding.Unicode.GetBytes(javascriptSerializer.Serialize(settingsBackup));
+			tempFile.Write(buffer, 0, buffer.Length);
+			tempFile.Close();
+
+			tempFile = File.Create(Path.Combine(tempDir, "Version.json"));
+			buffer = Encoding.Unicode.GetBytes(javascriptSerializer.Serialize(generateVersionFile("Settings")));
+			tempFile.Write(buffer, 0, buffer.Length);
+			tempFile.Close();
+
+			using(ZipFile zipFile = new ZipFile()) {
+				zipFile.AddDirectory(tempDir, "");
+				zipFile.Save(zipFileName);
+			}
+			Directory.Delete(tempDir, true);
+
+			return true;
+		}
+
+		/// <summary>
+		/// Backups the pages storage provider.
+		/// </summary>
+		/// <param name="zipFileName">The zip file name where to store the backup.</param>
+		/// <param name="pagesStorageProvider">The pages storage provider.</param>
+		/// <returns><c>true</c> if the backup file has been succesfully created.</returns>
+		public static bool BackupPagesStorageProvider(string zipFileName, IPagesStorageProviderV30 pagesStorageProvider) {
+			JavaScriptSerializer javascriptSerializer = new JavaScriptSerializer();
+			javascriptSerializer.MaxJsonLength = javascriptSerializer.MaxJsonLength * 10;
+
+			string tempDir = Path.Combine(Environment.GetEnvironmentVariable("TEMP"), Guid.NewGuid().ToString());
+			Directory.CreateDirectory(tempDir);
+
+			List<NamespaceInfo> nspaces = new List<NamespaceInfo>(pagesStorageProvider.GetNamespaces());
+			nspaces.Add(null);
+			List<NamespaceBackup> namespaceBackupList = new List<NamespaceBackup>(nspaces.Count);
+			foreach(NamespaceInfo nspace in nspaces) {
+
+				// Backup categories
+				CategoryInfo[] categories = pagesStorageProvider.GetCategories(nspace);
+				List<CategoryBackup> categoriesBackup = new List<CategoryBackup>(categories.Length);
+				foreach(CategoryInfo category in categories) {
+					// Add this category to the categoriesBackup list
+					categoriesBackup.Add(new CategoryBackup() {
+						FullName = category.FullName,
+						Pages = category.Pages
+					});
+				}
+
+				// Backup NavigationPaths
+				NavigationPath[] navigationPaths = pagesStorageProvider.GetNavigationPaths(nspace);
+				List<NavigationPathBackup> navigationPathsBackup = new List<NavigationPathBackup>(navigationPaths.Length);
+				foreach(NavigationPath navigationPath in navigationPaths) {
+					navigationPathsBackup.Add(new NavigationPathBackup() {
+						FullName = navigationPath.FullName,
+						Pages = navigationPath.Pages
+					});
+				}
+
+				// Add this namespace to the namespaceBackup list
+				namespaceBackupList.Add(new NamespaceBackup() {
+					Name = nspace == null ? "" : nspace.Name,
+					DefaultPageFullName = nspace == null ? "" : nspace.DefaultPage.FullName,
+					Categories = categoriesBackup,
+					NavigationPaths = navigationPathsBackup
+				});
+
+				// Backup pages (one json file for each page containing a maximum of 100 revisions)
+				PageInfo[] pages = pagesStorageProvider.GetPages(nspace);
+				foreach(PageInfo page in pages) {
+					PageContent pageContent = pagesStorageProvider.GetContent(page);
+					PageBackup pageBackup = new PageBackup();
+					pageBackup.FullName = page.FullName;
+					pageBackup.CreationDateTime = page.CreationDateTime;
+					pageBackup.LastModified = pageContent.LastModified;
+					pageBackup.Content = pageContent.Content;
+					pageBackup.Comment = pageContent.Comment;
+					pageBackup.Description = pageContent.Description;
+					pageBackup.Keywords = pageContent.Keywords;
+					pageBackup.Title = pageContent.Title;
+					pageBackup.User = pageContent.User;
+					pageBackup.LinkedPages = pageContent.LinkedPages;
+					pageBackup.Categories = (from c in pagesStorageProvider.GetCategoriesForPage(page)
+											 select c.FullName).ToArray();
+
+					// Backup the 100 most recent versions of the page
+					List<PageRevisionBackup> pageContentBackupList = new List<PageRevisionBackup>();
+					int[] revisions = pagesStorageProvider.GetBackups(page);
+					for(int i = revisions.Length - 1; i > revisions.Length - 100 && i >= 0; i--) {
+						PageContent pageRevision = pagesStorageProvider.GetBackupContent(page, revisions[i]);
+						PageRevisionBackup pageContentBackup = new PageRevisionBackup() {
+							Revision = revisions[i],
+							Content = pageRevision.Content,
+							Comment = pageRevision.Comment,
+							Description = pageRevision.Description,
+							Keywords = pageRevision.Keywords,
+							Title = pageRevision.Title,
+							User = pageRevision.User,
+							LastModified = pageRevision.LastModified
+						};
+						pageContentBackupList.Add(pageContentBackup);
+					}
+					pageBackup.Revisions = pageContentBackupList;
+
+					// Backup draft of the page
+					PageContent draft = pagesStorageProvider.GetDraft(page);
+					if(draft != null) {
+						pageBackup.Draft = new PageRevisionBackup() {
+							Content = draft.Content,
+							Comment = draft.Comment,
+							Description = draft.Description,
+							Keywords = draft.Keywords,
+							Title = draft.Title,
+							User = draft.User,
+							LastModified = draft.LastModified
+						};
+					}
+
+					// Backup all messages of the page
+					List<MessageBackup> messageBackupList = new List<MessageBackup>();
+					foreach(Message message in pagesStorageProvider.GetMessages(page)) {
+						messageBackupList.Add(BackupMessage(message));
+					}
+					pageBackup.Messages = messageBackupList;
+
+					FileStream tempFile = File.Create(Path.Combine(tempDir, page.FullName + ".json"));
+					byte[] buffer = Encoding.Unicode.GetBytes(javascriptSerializer.Serialize(pageBackup));
+					tempFile.Write(buffer, 0, buffer.Length);
+					tempFile.Close();
+				}
+			}
+			FileStream tempNamespacesFile = File.Create(Path.Combine(tempDir, "Namespaces.json"));
+			byte[] namespacesBuffer = Encoding.Unicode.GetBytes(javascriptSerializer.Serialize(namespaceBackupList));
+			tempNamespacesFile.Write(namespacesBuffer, 0, namespacesBuffer.Length);
+			tempNamespacesFile.Close();
+
+			// Backup content templates
+			ContentTemplate[] contentTemplates = pagesStorageProvider.GetContentTemplates();
+			List<ContentTemplateBackup> contentTemplatesBackup = new List<ContentTemplateBackup>(contentTemplates.Length);
+			foreach(ContentTemplate contentTemplate in contentTemplates) {
+				contentTemplatesBackup.Add(new ContentTemplateBackup() {
+					Name = contentTemplate.Name,
+					Content = contentTemplate.Content
+				});
+			}
+			FileStream tempContentTemplatesFile = File.Create(Path.Combine(tempDir, "ContentTemplates.json"));
+			byte[] contentTemplateBuffer = Encoding.Unicode.GetBytes(javascriptSerializer.Serialize(contentTemplatesBackup));
+			tempContentTemplatesFile.Write(contentTemplateBuffer, 0, contentTemplateBuffer.Length);
+			tempContentTemplatesFile.Close();
+
+			// Backup Snippets
+			Snippet[] snippets = pagesStorageProvider.GetSnippets();
+			List<SnippetBackup> snippetsBackup = new List<SnippetBackup>(snippets.Length);
+			foreach(Snippet snippet in snippets) {
+				snippetsBackup.Add(new SnippetBackup() {
+					Name = snippet.Name,
+					Content = snippet.Content
+				});
+			}
+			FileStream tempSnippetsFile = File.Create(Path.Combine(tempDir, "Snippets.json"));
+			byte[] snippetBuffer = Encoding.Unicode.GetBytes(javascriptSerializer.Serialize(snippetsBackup));
+			tempSnippetsFile.Write(snippetBuffer, 0, snippetBuffer.Length);
+			tempSnippetsFile.Close();
+
+			FileStream tempVersionFile = File.Create(Path.Combine(tempDir, "Version.json"));
+			byte[] versionBuffer = Encoding.Unicode.GetBytes(javascriptSerializer.Serialize(generateVersionFile("Pages")));
+			tempVersionFile.Write(versionBuffer, 0, versionBuffer.Length);
+			tempVersionFile.Close();
+
+			using(ZipFile zipFile = new ZipFile()) {
+				zipFile.AddDirectory(tempDir, "");
+				zipFile.Save(zipFileName);
+			}
+			Directory.Delete(tempDir, true);
+
+			return true;
+		}
+
+		// Backup a message with a recursive function to backup all its replies.
+		private static MessageBackup BackupMessage(Message message) {
+			MessageBackup messageBackup = new MessageBackup() {
+				Id = message.ID,
+				Subject = message.Subject,
+				Body = message.Body,
+				DateTime = message.DateTime,
+				Username = message.Username
+			};
+			List<MessageBackup> repliesBackup = new List<MessageBackup>(message.Replies.Length);
+			foreach(Message reply in message.Replies) {
+				repliesBackup.Add(BackupMessage(reply));
+			}
+			messageBackup.Replies = repliesBackup;
+			return messageBackup;
+		}
+
+		/// <summary>
+		/// Backups the users storage provider.
+		/// </summary>
+		/// <param name="zipFileName">The zip file name where to store the backup.</param>
+		/// <param name="usersStorageProvider">The users storage provider.</param>
+		/// <returns><c>true</c> if the backup file has been succesfully created.</returns>
+		public static bool BackupUsersStorageProvider(string zipFileName, IUsersStorageProviderV30 usersStorageProvider) {
+			JavaScriptSerializer javascriptSerializer = new JavaScriptSerializer();
+			javascriptSerializer.MaxJsonLength = javascriptSerializer.MaxJsonLength * 10;
+
+			string tempDir = Path.Combine(Environment.GetEnvironmentVariable("TEMP"), Guid.NewGuid().ToString());
+			Directory.CreateDirectory(tempDir);
+
+			// Backup users
+			UserInfo[] users = usersStorageProvider.GetUsers();
+			List<UserBackup> usersBackup = new List<UserBackup>(users.Length);
+			foreach(UserInfo user in users) {
+				usersBackup.Add(new UserBackup() {
+					Username = user.Username,
+					Active = user.Active,
+					DateTime = user.DateTime,
+					DisplayName = user.DisplayName,
+					Email = user.Email,
+					Groups = user.Groups,
+					UserData = usersStorageProvider.RetrieveAllUserData(user)
+				});
+			}
+			FileStream tempFile = File.Create(Path.Combine(tempDir, "Users.json"));
+			byte[] buffer = Encoding.Unicode.GetBytes(javascriptSerializer.Serialize(usersBackup));
+			tempFile.Write(buffer, 0, buffer.Length);
+			tempFile.Close();
+
+			// Backup UserGroups
+			UserGroup[] userGroups = usersStorageProvider.GetUserGroups();
+			List<UserGroupBackup> userGroupsBackup = new List<UserGroupBackup>(userGroups.Length);
+			foreach(UserGroup userGroup in userGroups) {
+				userGroupsBackup.Add(new UserGroupBackup() {
+					Name = userGroup.Name,
+					Description = userGroup.Description
+				});
+			}
+			
+			tempFile = File.Create(Path.Combine(tempDir, "Groups.json"));
+			buffer = Encoding.Unicode.GetBytes(javascriptSerializer.Serialize(userGroupsBackup));
+			tempFile.Write(buffer, 0, buffer.Length);
+			tempFile.Close();
+
+			tempFile = File.Create(Path.Combine(tempDir, "Version.json"));
+			buffer = Encoding.Unicode.GetBytes(javascriptSerializer.Serialize(generateVersionFile("Users")));
+			tempFile.Write(buffer, 0, buffer.Length);
+			tempFile.Close();
+
+
+			using(ZipFile zipFile = new ZipFile()) {
+				zipFile.AddDirectory(tempDir, "");
+				zipFile.Save(zipFileName);
+			}
+			Directory.Delete(tempDir, true);
+
+			return true;
+		}
+
+		/// <summary>
+		/// Backups the files storage provider.
+		/// </summary>
+		/// <param name="zipFileName">The zip file name where to store the backup.</param>
+		/// <param name="filesStorageProvider">The files storage provider.</param>
+		/// <param name="pagesStorageProviders">The pages storage providers.</param>
+		/// <returns><c>true</c> if the backup file has been succesfully created.</returns>
+		public static bool BackupFilesStorageProvider(string zipFileName, IFilesStorageProviderV30 filesStorageProvider, IPagesStorageProviderV30[] pagesStorageProviders) {
+			JavaScriptSerializer javascriptSerializer = new JavaScriptSerializer();
+			javascriptSerializer.MaxJsonLength = javascriptSerializer.MaxJsonLength * 10;
+
+			string tempDir = Path.Combine(Environment.GetEnvironmentVariable("TEMP"), Guid.NewGuid().ToString());
+			Directory.CreateDirectory(tempDir);
+
+			DirectoryBackup directoriesBackup = BackupDirectory(filesStorageProvider, tempDir, null);
+			FileStream tempFile = File.Create(Path.Combine(tempDir, "Files.json"));
+			byte[] buffer = Encoding.Unicode.GetBytes(javascriptSerializer.Serialize(directoriesBackup));
+			tempFile.Write(buffer, 0, buffer.Length);
+			tempFile.Close();
+
+
+			// Backup Pages Attachments
+			string[] pagesWithAttachment = filesStorageProvider.GetPagesWithAttachments();
+			foreach(string pageWithAttachment in pagesWithAttachment) {
+				PageInfo pageInfo = FindPageInfo(pageWithAttachment, pagesStorageProviders);
+				if(pageInfo != null) {
+					string[] attachments = filesStorageProvider.ListPageAttachments(pageInfo);
+					List<AttachmentBackup> attachmentsBackup = new List<AttachmentBackup>(attachments.Length);
+					foreach(string attachment in attachments) {
+						FileDetails attachmentDetails = filesStorageProvider.GetPageAttachmentDetails(pageInfo, attachment);
+						attachmentsBackup.Add(new AttachmentBackup() {
+							Name = attachment,
+							PageFullName = pageWithAttachment,
+							LastModified = attachmentDetails.LastModified,
+							Size = attachmentDetails.Size
+						});
+						using(MemoryStream stream = new MemoryStream()) {
+							filesStorageProvider.RetrievePageAttachment(pageInfo, attachment, stream, false);
+							stream.Seek(0, SeekOrigin.Begin);
+							byte[] tempBuffer = new byte[stream.Length];
+							stream.Read(tempBuffer, 0, (int)stream.Length);
+
+							DirectoryInfo dir = Directory.CreateDirectory(Path.Combine(tempDir, Path.Combine("__attachments", pageInfo.FullName)));
+							tempFile = File.Create(Path.Combine(dir.FullName, attachment));
+							tempFile.Write(tempBuffer, 0, tempBuffer.Length);
+							tempFile.Close();
+						}
+					}
+					tempFile = File.Create(Path.Combine(tempDir, Path.Combine("__attachments", Path.Combine(pageInfo.FullName, "Attachments.json"))));
+					buffer = Encoding.Unicode.GetBytes(javascriptSerializer.Serialize(attachmentsBackup));
+					tempFile.Write(buffer, 0, buffer.Length);
+					tempFile.Close();
+				}
+			}
+
+			tempFile = File.Create(Path.Combine(tempDir, "Version.json"));
+			buffer = Encoding.Unicode.GetBytes(javascriptSerializer.Serialize(generateVersionFile("Files")));
+			tempFile.Write(buffer, 0, buffer.Length);
+			tempFile.Close();
+
+			using(ZipFile zipFile = new ZipFile()) {
+				zipFile.AddDirectory(tempDir, "");
+				zipFile.Save(zipFileName);
+			}
+			Directory.Delete(tempDir, true);
+
+			return true;
+		}
+
+		private static PageInfo FindPageInfo(string pageWithAttachment, IPagesStorageProviderV30[] pagesStorageProviders) {
+			foreach(IPagesStorageProviderV30 pagesStorageProvider in pagesStorageProviders) {
+				PageInfo pageInfo = pagesStorageProvider.GetPage(pageWithAttachment);
+				if(pageInfo != null) return pageInfo;
+			}
+			return null;
+		}
+
+		private static DirectoryBackup BackupDirectory(IFilesStorageProviderV30 filesStorageProvider, string zipFileName, string directory) {
+			DirectoryBackup directoryBackup = new DirectoryBackup();
+
+			string[] files = filesStorageProvider.ListFiles(directory);
+			List<FileBackup> filesBackup = new List<FileBackup>(files.Length);
+			foreach(string file in files) {
+				FileDetails fileDetails = filesStorageProvider.GetFileDetails(file);
+				filesBackup.Add(new FileBackup() {
+					Name = file,
+					Size = fileDetails.Size,
+					LastModified = fileDetails.LastModified
+				});
+
+				FileStream tempFile = File.Create(Path.Combine(zipFileName.Trim('/').Trim('\\'), file.Trim('/').Trim('\\')));
+				using(MemoryStream stream = new MemoryStream()) {
+					filesStorageProvider.RetrieveFile(file, stream, false);
+					stream.Seek(0, SeekOrigin.Begin);
+					byte[] buffer = new byte[stream.Length];
+					stream.Read(buffer, 0, buffer.Length);
+					tempFile.Write(buffer, 0, buffer.Length);
+					tempFile.Close();
+				}
+			}
+			directoryBackup.Name = directory;
+			directoryBackup.Files = filesBackup;
+
+			string[] directories = filesStorageProvider.ListDirectories(directory);
+			List<DirectoryBackup> subdirectoriesBackup = new List<DirectoryBackup>(directories.Length);
+			foreach(string d in directories) {
+				subdirectoriesBackup.Add(BackupDirectory(filesStorageProvider, zipFileName, d));
+			}
+			directoryBackup.SubDirectories = subdirectoriesBackup;
+
+			return directoryBackup;
+		}
+
+	}
+
+	internal class SettingsBackup {
+		public Dictionary<string, string> Settings { get; set; }
+		public List<string> PluginsFileNames { get; set; }
+		public Dictionary<string, bool> PluginsStatus { get; set; }
+		public Dictionary<string, string> PluginsConfiguration { get; set; }
+		public List<MetaData> Metadata { get; set; }
+		public List<RecentChange> RecentChanges { get; set; }
+		public Dictionary<string, string[]> OutgoingLinks { get; set; }
+		public List<AclEntryBackup> AclEntries { get; set; }
+	}
+
+	internal class AclEntryBackup {
+		public Value Value { get; set; }
+		public string Subject { get; set; }
+		public string Resource { get; set; }
+		public string Action { get; set; }
+	}
+
+	internal class MetaData {
+		public MetaDataItem Item {get; set;}
+		public string Tag {get; set;}
+		public string Content {get; set;}
+	}
+
+	internal class GlobalSettingsBackup {
+		public Dictionary<string, string> Settings { get; set; }
+		public List<string> pluginsFileNames { get; set; }
+	}
+
+	internal class PageBackup {
+		public String FullName { get; set; }
+		public DateTime CreationDateTime { get; set; }
+		public DateTime LastModified { get; set; }
+		public string Content { get; set; }
+		public string Comment { get; set; }
+		public string Description { get; set; }
+		public string[] Keywords { get; set; }
+		public string Title { get; set; }
+		public string User { get; set; }
+		public string[] LinkedPages { get; set; }
+		public List<PageRevisionBackup> Revisions { get; set; }
+		public PageRevisionBackup Draft { get; set; }
+		public List<MessageBackup> Messages { get; set; }
+		public string[] Categories { get; set; }
+	}
+
+	internal class PageRevisionBackup {
+		public string Content { get; set; }
+		public string Comment { get; set; }
+		public string Description { get; set; }
+		public string[] Keywords { get; set; }
+		public string Title { get; set; }
+		public string User { get; set; }
+		public DateTime LastModified { get; set; }
+		public int Revision { get; set; }
+	}
+
+	internal class NamespaceBackup {
+		public string Name { get; set; }
+		public string DefaultPageFullName { get; set; }
+		public List<CategoryBackup> Categories { get; set; }
+		public List<NavigationPathBackup> NavigationPaths { get; set; }
+	}
+
+	internal class CategoryBackup {
+		public string FullName { get; set; }
+		public string[] Pages { get; set; }
+	}
+
+	internal class ContentTemplateBackup {
+		public string Name { get; set; }
+		public string Content { get; set; }
+	}
+
+	internal class MessageBackup {
+		public List<MessageBackup> Replies { get; set; }
+		public int Id { get; set; }
+		public string Subject { get; set; }
+		public string Body { get; set; }
+		public DateTime DateTime { get; set; }
+		public string Username { get; set; }
+	}
+
+	internal class NavigationPathBackup {
+		public string FullName { get; set; }
+		public string[] Pages { get; set; }
+	}
+
+	internal class SnippetBackup {
+		public string Name { get; set; }
+		public string Content { get; set; }
+	}
+
+	internal class UserBackup {
+		public string Username { get; set; }
+		public bool Active { get; set; }
+		public DateTime DateTime { get; set; }
+		public string DisplayName { get; set; }
+		public string Email { get; set; }
+		public string[] Groups { get; set; }
+		public IDictionary<string, string> UserData { get; set; }
+	}
+
+	internal class UserGroupBackup {
+		public string Name { get; set; }
+		public string Description { get; set; }
+	}
+
+	internal class DirectoryBackup {
+		public List<FileBackup> Files { get; set; }
+		public List<DirectoryBackup> SubDirectories { get; set; }
+		public string Name { get; set; }
+	}
+
+	internal class FileBackup {
+		public string Name { get; set; }
+		public long Size { get; set; }
+		public DateTime LastModified { get; set; }
+		public string DirectoryName { get; set; }
+	}
+
+	internal class VersionFile {
+		public string BackupRestoreVersion { get; set; }
+		public string WikiVersion { get; set; }
+		public string BackupName { get; set; }
+	}
+
+	internal class AttachmentBackup {
+		public string Name { get; set; }
+		public string PageFullName { get; set; }
+		public DateTime LastModified { get; set; }
+		public long Size { get; set; }
+	}
+
+}

BackupRestore/BackupRestore.csproj

+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProductVersion>8.0.30703</ProductVersion>
+    <SchemaVersion>2.0</SchemaVersion>
+    <ProjectGuid>{06320590-F6FC-4F8B-B3A2-2B5991676BE8}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>ScrewTurn.Wiki.BackupRestore</RootNamespace>
+    <AssemblyName>ScrewTurn.Wiki.BackupRestore</AssemblyName>
+    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+    <TargetFrameworkProfile />
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <DocumentationFile>bin\Debug\ScrewTurn.Wiki.BackupRestore.XML</DocumentationFile>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>none</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <DocumentationFile>bin\Release\ScrewTurn.Wiki.BackupRestore.XML</DocumentationFile>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="Ionic.Zip.Reduced">
+      <HintPath>..\References\Lib\DotNetZipLib-DevKit-v1.9\Ionic.Zip.Reduced.dll</HintPath>
+    </Reference>
+    <Reference Include="System" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Web.Extensions" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="..\AssemblyVersion.cs">
+      <Link>AssemblyVersion.cs</Link>
+    </Compile>
+    <Compile Include="BackupRestore.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\AclEngine\AclEngine.csproj">
+      <Project>{44B0F4C1-8CDC-4272-B2A2-C0AF689CEB81}</Project>
+      <Name>AclEngine</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\PluginFramework\PluginFramework.csproj">
+      <Project>{531A83D6-76F9-4014-91C5-295818E2D948}</Project>
+      <Name>PluginFramework</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>

BackupRestore/Properties/AssemblyInfo.cs

+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("ScrewTurn Wiki Backup Restore")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible 
+// to COM components.  If you need to access a type in this assembly from 
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("b06902e4-3d04-4c40-928c-862bdcd83815")]

Core-Tests/AuthCheckerTests.cs

 		}
 
 		[Test]
+		public void CheckActionForPage_GrantGroupFullControl_DenyGroupExplicitNamespace_ExceptReadPages() {
+			List<AclEntry> entries = new List<AclEntry>();
+			entries.Add(new AclEntry(Actions.ForGlobals.ResourceMasterPrefix, Actions.FullControl, "G.Group", Value.Grant));
+			entries.Add(new AclEntry(Actions.ForNamespaces.ResourceMasterPrefix + "NS1", Actions.ForNamespaces.ReadPages, "G.Group", Value.Grant));
+			entries.Add(new AclEntry(Actions.ForNamespaces.ResourceMasterPrefix + "NS1", Actions.FullControl, "G.Group", Value.Deny));
+
+			Collectors.SettingsProvider = MockProvider(entries);
+			Assert.IsFalse(AuthChecker.CheckActionForPage(new PageInfo(NameTools.GetFullName("NS1", "Page"), null, DateTime.Now), Actions.ForPages.ModifyPage, "User", new string[] { "Group" }), "Permission should be denied");
+			Assert.IsTrue(AuthChecker.CheckActionForPage(new PageInfo(NameTools.GetFullName("NS1", "Page"), null, DateTime.Now), Actions.ForPages.ReadPage, "User", new string[] { "Group" }), "Permission should be granted");
+		}
+
+		[Test]
+		public void CheckActionForPage_GrantGroupFullControl_DenyGroupNamespaceEscalator_ExceptReadPages() {
+			List<AclEntry> entries = new List<AclEntry>();
+			entries.Add(new AclEntry(Actions.ForGlobals.ResourceMasterPrefix, Actions.FullControl, "G.Group", Value.Grant));
+			entries.Add(new AclEntry(Actions.ForNamespaces.ResourceMasterPrefix, Actions.ForNamespaces.ReadPages, "G.Group", Value.Grant));
+			entries.Add(new AclEntry(Actions.ForNamespaces.ResourceMasterPrefix, Actions.FullControl, "G.Group", Value.Deny));
+
+			Collectors.SettingsProvider = MockProvider(entries);
+			Assert.IsFalse(AuthChecker.CheckActionForPage(new PageInfo(NameTools.GetFullName("NS1", "Page"), null, DateTime.Now), Actions.ForPages.ModifyPage, "User", new string[] { "Group" }), "Permission should be denied");
+			Assert.IsTrue(AuthChecker.CheckActionForPage(new PageInfo(NameTools.GetFullName("NS1", "Page"), null, DateTime.Now), Actions.ForPages.ReadPage, "User", new string[] { "Group" }), "Permission should be granted");
+		}
+
+		[Test]
+		public void CheckActionForPage_DenyGroupFullControl_GrantGroupExplicitNamespace() {
+			List<AclEntry> entries = new List<AclEntry>();
+			entries.Add(new AclEntry(Actions.ForGlobals.ResourceMasterPrefix, Actions.FullControl, "G.Group", Value.Deny));
+			entries.Add(new AclEntry(Actions.ForNamespaces.ResourceMasterPrefix + "NS1", Actions.FullControl, "G.Group", Value.Grant));
+
+			Collectors.SettingsProvider = MockProvider(entries);
+			Assert.IsTrue(AuthChecker.CheckActionForPage(new PageInfo(NameTools.GetFullName("NS1", "Page"), null, DateTime.Now), Actions.ForPages.ModifyPage, "User", new string[] { "Group" }), "Permission should be granted");
+			Assert.IsTrue(AuthChecker.CheckActionForPage(new PageInfo(NameTools.GetFullName("NS1", "Page"), null, DateTime.Now), Actions.ForPages.ReadPage, "User", new string[] { "Group" }), "Permission should be granted");
+		}
+
+		[Test]
+		public void CheckActionForPage_DenyGroupFullControl_GrantGroupNamespaceEscalator() {
+			List<AclEntry> entries = new List<AclEntry>();
+			entries.Add(new AclEntry(Actions.ForGlobals.ResourceMasterPrefix, Actions.FullControl, "G.Group", Value.Deny));
+			entries.Add(new AclEntry(Actions.ForNamespaces.ResourceMasterPrefix, Actions.FullControl, "G.Group", Value.Grant));
+
+			Collectors.SettingsProvider = MockProvider(entries);
+			Assert.IsTrue(AuthChecker.CheckActionForPage(new PageInfo(NameTools.GetFullName("NS1", "Page"), null, DateTime.Now), Actions.ForPages.ModifyPage, "User", new string[] { "Group" }), "Permission should be granted");
+			Assert.IsTrue(AuthChecker.CheckActionForPage(new PageInfo(NameTools.GetFullName("NS1", "Page"), null, DateTime.Now), Actions.ForPages.ReadPage, "User", new string[] { "Group" }), "Permission should be granted");
+		}
+
+		[Test]
+		public void CheckActionForPage_DenyGroupFullControl_GrantGroupReadPagesExplicitNamespace() {
+			List<AclEntry> entries = new List<AclEntry>();
+			entries.Add(new AclEntry(Actions.ForGlobals.ResourceMasterPrefix, Actions.FullControl, "G.Group", Value.Deny));
+			entries.Add(new AclEntry(Actions.ForNamespaces.ResourceMasterPrefix + "NS1", Actions.ForNamespaces.ReadPages, "G.Group", Value.Grant));
+
+			Collectors.SettingsProvider = MockProvider(entries);
+			Assert.IsFalse(AuthChecker.CheckActionForPage(new PageInfo(NameTools.GetFullName("NS1", "Page"), null, DateTime.Now), Actions.ForPages.ModifyPage, "User", new string[] { "Group" }), "Permission should be denied");
+			Assert.IsTrue(AuthChecker.CheckActionForPage(new PageInfo(NameTools.GetFullName("NS1", "Page"), null, DateTime.Now), Actions.ForPages.ReadPage, "User", new string[] { "Group" }), "Permission should be granted");
+		}
+
+		[Test]
+		public void CheckActionForPage_DenyGroupFullControl_GrantGroupReadPagesNamespaceEscalator() {
+			List<AclEntry> entries = new List<AclEntry>();
+			entries.Add(new AclEntry(Actions.ForGlobals.ResourceMasterPrefix, Actions.FullControl, "G.Group", Value.Deny));
+			entries.Add(new AclEntry(Actions.ForNamespaces.ResourceMasterPrefix, Actions.ForNamespaces.ReadPages, "G.Group", Value.Grant));
+
+			Collectors.SettingsProvider = MockProvider(entries);
+			Assert.IsFalse(AuthChecker.CheckActionForPage(new PageInfo(NameTools.GetFullName("NS1", "Page"), null, DateTime.Now), Actions.ForPages.ModifyPage, "User", new string[] { "Group" }), "Permission should be denied");
+			Assert.IsTrue(AuthChecker.CheckActionForPage(new PageInfo(NameTools.GetFullName("NS1", "Page"), null, DateTime.Now), Actions.ForPages.ReadPage, "User", new string[] { "Group" }), "Permission should be granted");
+		}
+
+		[Test]
+		public void CheckActionForPage_DenyGroupFullControl_GrantGroupReadPagesExplicitNamespaceLocalEscalator() {
+			List<AclEntry> entries = new List<AclEntry>();
+			entries.Add(new AclEntry(Actions.ForGlobals.ResourceMasterPrefix, Actions.FullControl, "G.Group", Value.Deny));
+			entries.Add(new AclEntry(Actions.ForNamespaces.ResourceMasterPrefix + "NS1", Actions.ForNamespaces.ManagePages, "G.Group", Value.Grant));
+
+			Collectors.SettingsProvider = MockProvider(entries);
+			Assert.IsTrue(AuthChecker.CheckActionForPage(new PageInfo(NameTools.GetFullName("NS1", "Page"), null, DateTime.Now), Actions.ForPages.ModifyPage, "User", new string[] { "Group" }), "Permission should be granted");
+			Assert.IsTrue(AuthChecker.CheckActionForPage(new PageInfo(NameTools.GetFullName("NS1", "Page"), null, DateTime.Now), Actions.ForPages.ReadPage, "User", new string[] { "Group" }), "Permission should be granted");
+		}
+
+		[Test]
+		public void CheckActionForPage_DenyGroupFullControl_GrantGroupReadPagesNamespaceEscalatorLocalEscalator() {
+			List<AclEntry> entries = new List<AclEntry>();
+			entries.Add(new AclEntry(Actions.ForGlobals.ResourceMasterPrefix, Actions.FullControl, "G.Group", Value.Deny));
+			entries.Add(new AclEntry(Actions.ForNamespaces.ResourceMasterPrefix, Actions.ForNamespaces.ManagePages, "G.Group", Value.Grant));
+
+			Collectors.SettingsProvider = MockProvider(entries);
+			Assert.IsTrue(AuthChecker.CheckActionForPage(new PageInfo(NameTools.GetFullName("NS1", "Page"), null, DateTime.Now), Actions.ForPages.ModifyPage, "User", new string[] { "Group" }), "Permission should be granted");
+			Assert.IsTrue(AuthChecker.CheckActionForPage(new PageInfo(NameTools.GetFullName("NS1", "Page"), null, DateTime.Now), Actions.ForPages.ReadPage, "User", new string[] { "Group" }), "Permission should be granted");
+		}
+
+		[Test]
 		public void CheckActionForPage_GrantUserRootEscalator_DenyGroupExplicitPage() {
 			List<AclEntry> entries = new List<AclEntry>();
 			entries.Add(new AclEntry(Actions.ForNamespaces.ResourceMasterPrefix,

Core-Tests/ReverseFormatterTests.cs

 		[TestCase("<u>text</u>", "__text__")]
 		[TestCase("<s>text</s>", "--text--")]
 		[TestCase("<html><table border=\"1\" bgcolor=\"LightBlue\"><thead><tr><th>Cells x.1</th><th>Cells x.2</th></tr></thead><tbody><tr><td>Cell 1.1</td><td>Cell 1.2</td></tr><tr><td>Cell 2.1</td><td>Cell 2.2</td></tr></tbody></table></html>", "{| border=\"1\" bgcolor=\"LightBlue\" \n|- \n! Cells x.1\n! Cells x.2\n|- \n| Cell 1.1\n| Cell 1.2\n|- \n| Cell 2.1\n| Cell 2.2\n|}\n")]
-		[TestCase("<ol><li><a class=\"internallink\" target=\"_blank\" href=\"www.try.com\" title=\"try\">try</a></li><li><a class=\"internallink\" target=\"_blank\" href=\"www.secondtry.com\" title=\"www.secondtry.com\">www.secondtry.com</a><br></li></ol>","# [^www.try.com|try]\n# [^www.secondtry.com|www.secondtry.com]\n")]
+		[TestCase("<ol><li><a class=\"internallink\" target=\"_blank\" href=\"www.try.com\" title=\"try\">try</a></li><li><a class=\"internallink\" target=\"_blank\" href=\"www.secondtry.com\" title=\"www.secondtry.com\">www.secondtry.com</a><br></li></ol>","# [^www.try.com|try]\n# [^www.secondtry.com]\n")]
 		[TestCase("<table><tbody><tr><td bgcolor=\"Blue\">Styled Cell</td><td>Normal cell</td></tr><tr><td>Normal cell</td><td bgcolor=\"Yellow\">Styled cell</td></tr></tbody></table>", "{| \n|- \n|  bgcolor=\"Blue\"  | Styled Cell\n| Normal cell\n|- \n| Normal cell\n|  bgcolor=\"Yellow\"  | Styled cell\n|}\n")]
 		[TestCase("<h1>text</h1>", "==text==\n")]
 		[TestCase("<h2>text</h2>", "===text===\n")]

Core/AuthChecker.cs

 
 			if(currentUser == "admin") return true;
 
+			return LocalCheckActionForGlobals(action, currentUser, groups) == Authorization.Granted;
+		}
+
+		private static Authorization LocalCheckActionForGlobals(string action, string currentUser, string[] groups) {
 			AclEntry[] entries = SettingsProvider.AclManager.RetrieveEntriesForResource(Actions.ForGlobals.ResourceMasterPrefix);
 			Authorization auth = AclEvaluator.AuthorizeAction(Actions.ForGlobals.ResourceMasterPrefix, action,
 				AuthTools.PrepareUsername(currentUser), AuthTools.PrepareGroups(groups), entries);
 
-			return auth == Authorization.Granted;
+			return auth;
 		}
 
 		/// <summary>
 		/// <param name="action">The action the user is attempting to perform.</param>
 		/// <param name="currentUser">The current user.</param>
 		/// <param name="groups">The groups the user is member of.</param>
+		/// <param name="localEscalator"><c>true</c> is the method is called in a local escalator process.</param>
 		/// <returns><c>true</c> if the action is allowed, <c>false</c> otherwise.</returns>
-		public static bool CheckActionForNamespace(NamespaceInfo nspace, string action, string currentUser, string[] groups) {
+		public static bool CheckActionForNamespace(NamespaceInfo nspace, string action, string currentUser, string[] groups, bool localEscalator = false) {
 			if(action == null) throw new ArgumentNullException("action");
 			if(action.Length == 0) throw new ArgumentException("Action cannot be empty", "action");
 			if(!AuthTools.IsValidAction(action, Actions.ForNamespaces.All)) throw new ArgumentException("Invalid action", "action");
 
 			if(currentUser == "admin") return true;
 
+			return LocalCheckActionForNamespace(nspace, action, currentUser, groups, localEscalator) == Authorization.Granted;
+		}
+
+		private static Authorization LocalCheckActionForNamespace(NamespaceInfo nspace, string action, string currentUser, string[] groups, bool localEscalator = false) {
 			string namespaceName = nspace != null ? nspace.Name : "";
 
 			AclEntry[] entries = SettingsProvider.AclManager.RetrieveEntriesForResource(
 			Authorization auth = AclEvaluator.AuthorizeAction(Actions.ForNamespaces.ResourceMasterPrefix + namespaceName,
 				action, AuthTools.PrepareUsername(currentUser), AuthTools.PrepareGroups(groups), entries);
 
-			if(auth != Authorization.Unknown) return auth == Authorization.Granted;
+			if(localEscalator || auth != Authorization.Unknown) return auth;
 
 			// Try local escalators
 			string[] localEscalators = null;
 			if(Actions.ForNamespaces.LocalEscalators.TryGetValue(action, out localEscalators)) {
 				foreach(string localAction in localEscalators) {
-					bool authorized = CheckActionForNamespace(nspace, localAction, currentUser, groups);
-					if(authorized) return true;
+					Authorization authorization = LocalCheckActionForNamespace(nspace, localAction, currentUser, groups, true);
+					if(authorization != Authorization.Unknown) return authorization;
 				}
 			}
 
 			// Try root escalation
 			if(nspace != null) {
-				bool authorized = CheckActionForNamespace(null, action, currentUser, groups);
-				if(authorized) return true;
+				Authorization authorization = LocalCheckActionForNamespace(null, action, currentUser, groups);
+				if(authorization != Authorization.Unknown) return authorization;
 			}
 
 			// Try global escalators
 			string[] globalEscalators = null;
 			if(Actions.ForNamespaces.GlobalEscalators.TryGetValue(action, out globalEscalators)) {
 				foreach(string globalAction in globalEscalators) {
-					bool authorized = CheckActionForGlobals(globalAction, currentUser, groups);
-					if(authorized) return true;
+					Authorization authorization = LocalCheckActionForGlobals(globalAction, currentUser, groups);
+					if(authorization != Authorization.Unknown) return authorization;
 				}
 			}
 
-			return false;
+			return Authorization.Unknown;
 		}
 
 		/// <summary>
 		/// <param name="action">The action the user is attempting to perform.</param>
 		/// <param name="currentUser">The current user.</param>
 		/// <param name="groups">The groups the user is member of.</param>
+		/// <param name="localEscalator"><c>true</c> is the method is called in a local escalator process.</param>
 		/// <returns><c>true</c> if the action is allowed, <c>false</c> otherwise.</returns>
-		public static bool CheckActionForPage(PageInfo page, string action, string currentUser, string[] groups) {
+		public static bool CheckActionForPage(PageInfo page, string action, string currentUser, string[] groups, bool localEscalator = false) {
 			if(page == null) throw new ArgumentNullException("page");
 
 			if(action == null) throw new ArgumentNullException("action");
 
 			if(currentUser == "admin") return true;
 
+			return LocalCheckActionForPage(page, action, currentUser, groups, localEscalator) == Authorization.Granted;
+		}
+
+		private static Authorization LocalCheckActionForPage(PageInfo page, string action, string currentUser, string[] groups, bool localEscalator = false) {
 			AclEntry[] entries = SettingsProvider.AclManager.RetrieveEntriesForResource(Actions.ForPages.ResourceMasterPrefix + page.FullName);
 			Authorization auth = AclEvaluator.AuthorizeAction(Actions.ForPages.ResourceMasterPrefix + page.FullName, action,
 				AuthTools.PrepareUsername(currentUser), AuthTools.PrepareGroups(groups), entries);
 
-			if(auth != Authorization.Unknown) return auth == Authorization.Granted;
+			if(localEscalator || auth != Authorization.Unknown) return auth;
 
 			// Try local escalators
 			string[] localEscalators = null;
 			if(Actions.ForPages.LocalEscalators.TryGetValue(action, out localEscalators)) {
 				foreach(string localAction in localEscalators) {
-					bool authorized = CheckActionForPage(page, localAction, currentUser, groups);
-					if(authorized) return true;
+					Authorization authorization = LocalCheckActionForPage(page, localAction, currentUser, groups, true);
+					if(authorization != Authorization.Unknown) return authorization;
 				}
 			}
 
 			NamespaceInfo ns = string.IsNullOrEmpty(nsName) ? null : new NamespaceInfo(nsName, null, null);
 			if(Actions.ForPages.NamespaceEscalators.TryGetValue(action, out namespaceEscalators)) {
 				foreach(string namespaceAction in namespaceEscalators) {
-					bool authorized = CheckActionForNamespace(ns, namespaceAction, currentUser, groups);
-					if(authorized) return true;
+					Authorization authorization = LocalCheckActionForNamespace(ns, namespaceAction, currentUser, groups, true);
+					if(authorization != Authorization.Unknown) return authorization;
+					
+					// Try root escalation
+					if(ns != null) {
+						authorization = LocalCheckActionForNamespace(null, namespaceAction, currentUser, groups, true);
+						if(authorization != Authorization.Unknown) return authorization;
+					}
 				}
 			}
 
 			string[] globalEscalators = null;
 			if(Actions.ForPages.GlobalEscalators.TryGetValue(action, out globalEscalators)) {
 				foreach(string globalAction in globalEscalators) {
-					bool authorized = CheckActionForGlobals(globalAction, currentUser, groups);
-					if(authorized) return true;
+					Authorization authorization = LocalCheckActionForGlobals(globalAction, currentUser, groups);
+					if(authorization != Authorization.Unknown) return authorization;
 				}
 			}
 
-			return false;
+			return Authorization.Unknown;
 		}
 
 		/// <summary>

Core/Formatter.cs

 						n = fields[1];
 					}
 					else {
+						done = true;
 						StringBuilder img = new StringBuilder();
 						// Image
 						if(fields[0].ToLowerInvariant().Equals("imageleft") || fields[0].ToLowerInvariant().Equals("imageright") || fields[0].ToLowerInvariant().Equals("imageauto")) {
 				else {
 					string formatterErrorString = @"<b style=""color: #FF0000;"">FORMATTER ERROR (Transcluded inexistent page or this same page)</b>";
 					sb.Insert(match.Index, formatterErrorString);
-					sb.Insert(match.Index, match);
 				}
 
 				match = TransclusionRegex.Match(sb.ToString());

Core/ReverseFormatter.cs

 	/// </summary>
 	public static class ReverseFormatter {
 
-		private static string ProcessList(XmlNodeList nodes, string marker) {
-			string result = "";
+		private static void ProcessList(XmlNodeList nodes, string marker, StringBuilder sb) {
 			string ul = "*";
 			string ol = "#";
 			foreach(XmlNode node in nodes) {
 						else if(child.Name != "ol" && child.Name != "ul") {
 							TextReader reader = new StringReader(child.OuterXml);
 							XmlDocument n = FromHTML(reader);
-							text += ProcessChild(n.ChildNodes);
+							StringBuilder tempSb = new StringBuilder();
+							ProcessChild(n.ChildNodes, tempSb);
+							text += tempSb.ToString();
 						}
 					}
 					XmlAttribute styleAttribute = node.Attributes["style"];
 							text = "__" + text + "__";
 						}
 					}
-					result += marker + " " + text;
-					if(!result.EndsWith("\n")) result += "\n";
+					sb.Append(marker + " " + text);
+					if(!sb.ToString().EndsWith("\n")) sb.Append("\n");
 					foreach(XmlNode child in node.ChildNodes) {
-						if(child.Name == "ol") result += ProcessList(child.ChildNodes, marker + ol);
-						if(child.Name == "ul") result += ProcessList(child.ChildNodes, marker + ul);
+						if(child.Name == "ol") ProcessList(child.ChildNodes, marker + ol, sb);
+						if(child.Name == "ul") ProcessList(child.ChildNodes, marker + ul, sb);
 					}
 				}
 			}
-			return result;
 		}
 
 		private static string ProcessImage(XmlNode node) {
 			if(node.Attributes.Count != 0) {
 				foreach(XmlAttribute attName in node.Attributes) {
 					if(attName.Name == "src") {
-						string[] path = attName.Value.ToString().Split('=');
-						if(path.Length > 2) result += "{" + "UP(" + path[1].Split('&')[0] + ")}" + path[2];
-						else result += "{UP}" + path[path.Length - 1];
+						string[] path = attName.Value.Split('=');
+						if(path.Length > 2) result += "{" + "UP(" + path[1].Split('&')[0].Replace("%20", " ") + ")}" + path[2].Replace("%20", " ");
+						else result += "{UP}" + path[path.Length - 1].Replace("%20", " ");
 					}
 				}
 			}
 		}
 
 		private static string ProcessLink(string link) {
+			link = link.Replace("%20", " ");
 			string subLink = "";
-			string[] links = link.Split('=');
-			if(links[0] == "GetFile.aspx?File") {
-				subLink += "{UP}";
-				for(int i = 1; i < links.Length - 1; i++) {
-					subLink += links[i] + "=";
+			if(link.ToLowerInvariant().StartsWith("getfile.aspx")) {
+				string[] urlParameters = link.Remove(0, 13).Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries);
+
+				string pageName = urlParameters.FirstOrDefault(p => p.ToLowerInvariant().StartsWith("page"));
+				if(!string.IsNullOrEmpty(pageName)) pageName = Uri.UnescapeDataString(pageName.Split(new char[] { '=' })[1]);
+				string fileName = urlParameters.FirstOrDefault(p => p.ToLowerInvariant().StartsWith("file"));
+				fileName = Uri.UnescapeDataString(fileName.Split(new char[] { '=' })[1]);
+				if(string.IsNullOrEmpty(pageName)) {
+					subLink = "{UP}" + fileName;
 				}
-				subLink += links[links.Length - 1];
+				else {
+					subLink = "{UP(" + pageName + ")}" + fileName;
+				}
 				link = subLink;
 			}
 			return link;
 		}
 
-		private static string ProcessChildImage(XmlNodeList nodes) {
+		private static void ProcessChildImage(XmlNodeList nodes, StringBuilder sb) {
 			string image = "";
 			string p = "";
 			string url = "";
-			string result = "";
 			bool hasDescription = false;
 			foreach(XmlNode node in nodes) {
 				if(node.Name.ToLowerInvariant() == "img") image += ProcessImage(node);
 				else if(node.Name.ToLowerInvariant() == "p") {
 					hasDescription = true;
-					p += "|" + ProcessChild(node.ChildNodes) + "|";
+					StringBuilder tempSb = new StringBuilder();
+					ProcessChild(node.ChildNodes, tempSb);
+					p += "|" + tempSb.ToString() + "|";
 				}
 				else if(node.Name.ToLowerInvariant() == "a") {
 					string link = "";
 					if(node.Attributes.Count != 0) {
 						XmlAttributeCollection attribute = node.Attributes;
 						foreach(XmlAttribute attName in attribute) {
-							if(attName.Value.ToString() == "_blank") target += "^";
-							if(attName.Name.ToString() == "href") link += attName.Value.ToString();
+							if(attName.Value == "_blank") target += "^";
+							if(attName.Name == "href") link += attName.Value;
 						}
 					}
 					link = ProcessLink(link);
 				}
 			}
 			if(!hasDescription) p = "||";
-			result = p + image + url;
-			return result;
+			sb.Append(p + image + url);
 		}
 		
-		private static string ProcessTableImage(XmlNodeList nodes) {
-			string result = "";
+		private static void ProcessTableImage(XmlNodeList nodes, StringBuilder sb) {
 			foreach(XmlNode node in nodes) {
 				switch(node.Name.ToLowerInvariant()) {
 					case "tbody":
-						result += ProcessTableImage(node.ChildNodes);
+						ProcessTableImage(node.ChildNodes, sb);
 						break;
 					case "tr":
-						result += ProcessTableImage(node.ChildNodes);
+						ProcessTableImage(node.ChildNodes, sb);
 						break;
 					case "td":
 						string image = "";
 						string aref = "";
 						string p = "";
 						bool hasLink = false;
-						if(node.FirstChild.Name.ToLowerInvariant() == "img") image += ProcessTableImage(node.ChildNodes);
+						if(node.FirstChild.Name.ToLowerInvariant() == "img") {
+							StringBuilder tempSb = new StringBuilder();
+							ProcessTableImage(node.ChildNodes, tempSb);
+							image += tempSb.ToString();
+						}
 						if(node.FirstChild.Name.ToLowerInvariant() == "a") {
 							hasLink = true;
-							aref += ProcessTableImage(node.ChildNodes);
+							StringBuilder tempSb = new StringBuilder();
+							ProcessTableImage(node.ChildNodes, tempSb);
+							aref += tempSb.ToString();
 						}
-						if(node.LastChild.Name.ToLowerInvariant() == "p") p += node.LastChild.InnerText.ToString();
-						if(!hasLink) result += p + image;
-						else result += p + aref;
+						if(node.LastChild.Name.ToLowerInvariant() == "p") p += node.LastChild.InnerText;
+						if(!hasLink) sb.Append(p + image);
+						else sb.Append(p + aref);
 						break;
 					case "img":
-						result += "|" + ProcessImage(node);
+						sb.Append("|" + ProcessImage(node));
 						break;
 					case "a":
 						string link = "";
 						if(node.Attributes.Count != 0) {
 							XmlAttributeCollection attribute = node.Attributes;
 							foreach(XmlAttribute attName in attribute) {
-								if(attName.Name.ToString() != "id".ToLowerInvariant()) {
-									if(attName.Value.ToString() == "_blank") target += "^";
-									if(attName.Name.ToString() == "href") link += attName.Value.ToString();
-									if(attName.Name.ToString() == "title") title += attName.Value.ToString();
+								if(attName.Name != "id".ToLowerInvariant()) {
+									if(attName.Value == "_blank") target += "^";
+									if(attName.Name == "href") link += attName.Value;
+									if(attName.Name == "title") title += attName.Value;
 								}
 								link = ProcessLink(link);
 							}
-							result += ProcessTableImage(node.ChildNodes) + "|" + target + link;
+							ProcessTableImage(node.ChildNodes, sb);
+							sb.Append("|" + target + link);
 						}
 						break;
 				}
 			}
-			return result;
 		}
 
-		private static string ProcessTable(XmlNodeList nodes) {
-			string result = "";
+		private static void ProcessTable(XmlNodeList nodes, StringBuilder sb) {
 			foreach(XmlNode node in nodes) {
 				switch(node.Name.ToLowerInvariant()) {
 					case "thead":
-						result += ProcessTable(node.ChildNodes);
+						ProcessTable(node.ChildNodes, sb);
 						break;
 					case "th":
-						result += "! " + ProcessChild(node.ChildNodes) + "\n";
+						sb.Append("! ");
+						ProcessChild(node.ChildNodes, sb);
+						sb.Append("\n");
 						break;
 					case "caption":
-						result += "|+ " + ProcessChild(node.ChildNodes) + "\n";
+						sb.Append("|+ ");
+						ProcessChild(node.ChildNodes, sb);
+						sb.Append("\n");
 						break;
 					case "tbody":
-						result += ProcessTable(node.ChildNodes) + "";
+						ProcessTable(node.ChildNodes, sb);
 						break;
 					case "tr":
 						string style = "";
 						foreach(XmlAttribute attr in node.Attributes) {
-							if(attr.Name.ToLowerInvariant() == "style") style += "style=\"" + attr.Value.ToString() + "\" ";
+							if(attr.Name.ToLowerInvariant() == "style") style += "style=\"" + attr.Value + "\" ";
 						}
-						result += "|- " + style + "\n" + ProcessTable(node.ChildNodes);
+						sb.Append("|- " + style + "\n");
+						ProcessTable(node.ChildNodes, sb);
 						break;
 					case "td":
 						string styleTd = "";
 						if(node.Attributes.Count != 0) {
 							foreach(XmlAttribute attr in node.Attributes) {
-								styleTd += " " + attr.Name + "=\"" + attr.Value.ToString() + "\" ";
+								styleTd += " " + attr.Name + "=\"" + attr.Value + "\" ";
 							}
-							result += "| " + styleTd + " | " + ProcessChild(node.ChildNodes) + "\n";
+							sb.Append("| " + styleTd + " | ");
+							ProcessChild(node.ChildNodes, sb);
+							sb.Append("\n");
 						}
-						else result += "| " + ProcessChild(node.ChildNodes) + "\n";
+						else {
+							sb.Append("| ");
+							ProcessChild(node.ChildNodes, sb);
+							sb.Append("\n");
+						}
 						break;
 				}
 			}
-			return result;
 		}
 
-		private static string ProcessChild(XmlNodeList nodes) {
-			string result = "";
+		private static void ProcessChild(XmlNodeList nodes, StringBuilder sb) {
 			foreach(XmlNode node in nodes) {
 				bool anchor = false;
-				if(node.NodeType == XmlNodeType.Text) result += node.Value.TrimStart('\n');
+				if(node.NodeType == XmlNodeType.Text) sb.Append(node.Value.TrimStart('\n'));
 				else if(node.NodeType != XmlNodeType.Whitespace) {
 					switch(node.Name.ToLowerInvariant()) {
 						case "html":
-							result += ProcessChild(node.ChildNodes);
+							ProcessChild(node.ChildNodes, sb);
 							break;
 						case "b":
 						case "strong":
-							result += node.HasChildNodes ? "'''" + ProcessChild(node.ChildNodes) + "'''" : "";
+							if(node.HasChildNodes) {
+								sb.Append("'''");
+								ProcessChild(node.ChildNodes, sb);
+								sb.Append("'''");
+							}
 							break;
 						case "strike":
 						case "s":
-							result += node.HasChildNodes ? "--" + ProcessChild(node.ChildNodes) + "--" : "";
+							if(node.HasChildNodes) {
+								sb.Append("--");
+								ProcessChild(node.ChildNodes, sb);
+								sb.Append("--");
+							}
 							break;
 						case "em":
 						case "i":
-							result += node.HasChildNodes ? "''" + ProcessChild(node.ChildNodes) + "''" : "";
+							if(node.HasChildNodes) {
+								sb.Append("''");
+								ProcessChild(node.ChildNodes, sb);
+								sb.Append("''");
+							}
 							break;
 						case "u":
-							result += node.HasChildNodes ? "__" + ProcessChild(node.ChildNodes) + "__" : "";
+							if(node.HasChildNodes) {
+								sb.Append("__");
+								ProcessChild(node.ChildNodes, sb);
+								sb.Append("__");
+							}
 							break;
 						case "h1":
 							if(node.HasChildNodes) {
-								if(node.FirstChild.NodeType == XmlNodeType.Whitespace) result += "----\n" + ProcessChild(node.ChildNodes);
-								else result += "==" + ProcessChild(node.ChildNodes) + "==\n";
+								if(node.FirstChild.NodeType == XmlNodeType.Whitespace) {
+									sb.Append("----\n");
+									ProcessChild(node.ChildNodes, sb);
+								}
+								else {
+									if(!(sb.Length == 0 || sb.ToString().EndsWith("\n"))) sb.Append("\n");
+									sb.Append("==");
+									ProcessChild(node.ChildNodes, sb);
+									sb.Append("==\n");
+								}
 							}
-							else result += "----\n";
+							else sb.Append("----\n");
 							break;
 						case "h2":
-							result += "===" + ProcessChild(node.ChildNodes) + "===\n";
+							if(!(sb.Length == 0 || sb.ToString().EndsWith("\n"))) sb.Append("\n");
+							sb.Append("===");
+							ProcessChild(node.ChildNodes, sb);
+							sb.Append("===\n");
 							break;
 						case "h3":
-							result += "====" + ProcessChild(node.ChildNodes) + "====\n";
+							if(!(sb.Length == 0 || sb.ToString().EndsWith("\n"))) sb.Append("\n");
+							sb.Append("====");
+							ProcessChild(node.ChildNodes, sb);
+							sb.Append("====\n");
 							break;
 						case "h4":
-							result += "=====" + ProcessChild(node.ChildNodes) + "=====\n";
+							if(!(sb.Length == 0 || sb.ToString().EndsWith("\n"))) sb.Append("\n");
+							sb.Append("=====");
+							ProcessChild(node.ChildNodes, sb);
+							sb.Append("=====\n");
 							break;
 						case "pre":
-							result += node.HasChildNodes ? "@@" + node.InnerText.ToString() + "@@" : "";
+							if(node.HasChildNodes) sb.Append("@@" + node.InnerText + "@@");
 							break;
 						case "code":
-							result += node.HasChildNodes ? "{{" + ProcessChild(node.ChildNodes) + "}}" : "";
+							if(node.HasChildNodes) {
+								sb.Append("{{");
+								ProcessChild(node.ChildNodes, sb);
+								sb.Append("}}");
+							}
 							break;
 						case "hr":
 						case "hr /":
-							result += "\n== ==\n" + ProcessChild(node.ChildNodes);
+							sb.Append("\n== ==\n");
+							ProcessChild(node.ChildNodes, sb);
 							break;
 						case "span":
-							if(node.Attributes["style"] != null) {
-								if(node.Attributes["style"].Value.Replace(" ", "").ToLowerInvariant().Contains("font-weight:normal")) {
-									result += ProcessChild(node.ChildNodes);
-								}
-								if(node.Attributes["style"].Value.Replace(" ", "").ToLowerInvariant().Contains("white-space:pre")) {
-									result += ": ";
-								}
+							if(node.Attributes["style"] != null && node.Attributes["style"].Value.Replace(" ", "").ToLowerInvariant().Contains("font-weight:normal")) {
+								ProcessChild(node.ChildNodes, sb);
 							}
-							if(node.Attributes.Count > 0) {
-								XmlAttributeCollection attributeCollection = node.Attributes;
-								foreach(XmlAttribute attribute in attributeCollection) {
-									if(attribute.Value == "italic") result += "''" + ProcessChild(node.ChildNodes) + "''";
-								}
+							else if(node.Attributes["style"] != null && node.Attributes["style"].Value.Replace(" ", "").ToLowerInvariant().Contains("white-space:pre")) {
+								sb.Append(": ");
+							}
+							else {
+								sb.Append(node.OuterXml);
 							}
 							break;
 						case "br":
 							if(node.PreviousSibling != null && node.PreviousSibling.Name == "br") {
-								result += "\n";
+								sb.Append("\n");
 							}
 							else {
-								result += Settings.ProcessSingleLineBreaks ? "\n" : "\n\n";
+								if(Settings.ProcessSingleLineBreaks) sb.Append("\n");
+								else sb.Append("\n\n");
 							}
 							break;
 						case "table":
 							string tableStyle = "";
 
 							if(node.Attributes["class"] != null && node.Attributes["class"].Value.Contains("imageauto")) {
-								result += "[imageauto|" + ProcessTableImage(node.ChildNodes) + "]";
+								sb.Append("[imageauto|");
+								ProcessTableImage(node.ChildNodes, sb);
+								sb.Append("]");
 							}
 							else {
 								foreach(XmlAttribute attName in node.Attributes) {
 									tableStyle += attName.Name + "=\"" + attName.Value + "\" ";
 								}
-								result += "{| " + tableStyle + "\n" + ProcessTable(node.ChildNodes) + "|}\n";
+								sb.Append("{| " + tableStyle + "\n");
+								ProcessTable(node.ChildNodes, sb);
+								sb.Append("|}\n");
 							}
 							break;
 						case "ol":
 							if(node.PreviousSibling != null && node.PreviousSibling.Name != "br") {
-								result += "\n";
+								sb.Append("\n");
 							}
 							if(node.ParentNode != null) {
-								if(node.ParentNode.Name != "td") result += ProcessList(node.ChildNodes, "#");
-								else result += node.OuterXml.ToString();
+								if(node.ParentNode.Name != "td") ProcessList(node.ChildNodes, "#", sb);
+								else sb.Append(node.OuterXml);
 							}
-							else result += ProcessList(node.ChildNodes, "#");
+							else ProcessList(node.ChildNodes, "#", sb);
 							break;
 						case "ul":
 							if(node.PreviousSibling != null && node.PreviousSibling.Name != "br") {
-								result += "\n";
+								sb.Append("\n");
 							}
 							if(node.ParentNode != null) {
-								if(node.ParentNode.Name != "td") result += ProcessList(node.ChildNodes, "*");
-								else result += node.OuterXml.ToString();
+								if(node.ParentNode.Name != "td") ProcessList(node.ChildNodes, "*", sb);
+								else sb.Append(node.OuterXml);
 							}
-							else result += ProcessList(node.ChildNodes, "*");
+							else ProcessList(node.ChildNodes, "*", sb);
 							break;
 						case "sup":
-							result += "<sup>" + ProcessChild(node.ChildNodes) + "</sup>";
+							sb.Append("<sup>");
+							ProcessChild(node.ChildNodes, sb);
+							sb.Append("</sup>");
 							break;
 						case "sub":
-							result += "<sub>" + ProcessChild(node.ChildNodes) + "</sub>";
+							sb.Append("<sub>");
+							ProcessChild(node.ChildNodes, sb);
+							sb.Append("</sub>");
 							break;
 						case "p":
-							if(node.Attributes["class"] != null && node.Attributes["class"].Value.Contains("imagedescription")) continue;
-							else result += ProcessChild(node.ChildNodes) + "\n" + (Settings.ProcessSingleLineBreaks ? "" : "\n");
+							if(node.Attributes["class"] != null && node.Attributes["class"].Value.Contains("imagedescription")) {
+								continue;
+							}
+							else {
+								ProcessChild(node.ChildNodes, sb);
+								sb.Append("\n");
+								if(!Settings.ProcessSingleLineBreaks) sb.Append("\n");
+							}
 							break;
 						case "div":
-							if(node.Attributes["class"] != null) {
-								if(node.Attributes["class"].Value.Contains("box")) result += node.HasChildNodes ? "(((" + ProcessChild(node.ChildNodes) + ")))" : "";
-								else if(node.Attributes["class"].Value.Contains("imageleft")) result += "[imageleft" + ProcessChildImage(node.ChildNodes) + "]";
-								else if(node.Attributes["class"].Value.Contains("imageright")) result += "[imageright" + ProcessChildImage(node.ChildNodes) + "]";
-								else if(node.Attributes["class"].Value.Contains("image")) result += "[image" + ProcessChildImage(node.ChildNodes) + "]";
-								else if(node.Attributes["class"].Value.Contains("indent")) result += ": " + ProcessChild(node.ChildNodes) + "\n";
+							if(node.Attributes["class"] != null && node.Attributes["class"].Value.Contains("box")) {
+								if(node.HasChildNodes) {
+									sb.Append("(((");
+									ProcessChild(node.ChildNodes, sb);
+									sb.Append(")))");
+								}
+							}
+							else if(node.Attributes["class"] != null && node.Attributes["class"].Value.Contains("imageleft")) {
+								sb.Append("[imageleft");
+								ProcessChildImage(node.ChildNodes, sb);
+								sb.Append("]");
+							}
+							else if(node.Attributes["class"] != null && node.Attributes["class"].Value.Contains("imageright")) {
+								sb.Append("[imageright");
+								ProcessChildImage(node.ChildNodes, sb);
+								sb.Append("]");
+							}
+							else if(node.Attributes["class"] != null && node.Attributes["class"].Value.Contains("image")) {
+								sb.Append("[image");
+								ProcessChildImage(node.ChildNodes, sb);
+								sb.Append("]");
+							}
+							else if(node.Attributes["class"] != null && node.Attributes["class"].Value.Contains("indent")) {
+								sb.Append(": ");
+								ProcessChild(node.ChildNodes, sb);
+								sb.Append("\n");
+							}
+							else if(node.Attributes.Count > 0) {
+								sb.Append(node.OuterXml);
 							}
 							else {
-								result += "\n";
+								sb.Append("\n");
 								if(node.PreviousSibling != null && node.PreviousSibling.Name != "div") {
-									result += Settings.ProcessSingleLineBreaks ? "" : "\n";
+									if(!Settings.ProcessSingleLineBreaks) sb.Append("\n");
 								}
 								if(node.FirstChild != null && node.FirstChild.Name == "br") {
 									node.RemoveChild(node.FirstChild);
 								}
 								if(node.HasChildNodes) {
-									result += ProcessChild(node.ChildNodes);
-									result += Settings.ProcessSingleLineBreaks ? "\n" : "\n\n";
+									ProcessChild(node.ChildNodes, sb);
+									if(Settings.ProcessSingleLineBreaks) sb.Append("\n");
+									else sb.Append("\n\n");
 								}
 							}
 							break;
 							string description = "";
 							bool hasClass = false;
 							bool isLink = false;
-							if(node.ParentNode != null && node.ParentNode.Name.ToLowerInvariant().ToString() == "a") isLink = true;
+							if(node.ParentNode != null && node.ParentNode.Name.ToLowerInvariant() == "a") isLink = true;
 							if(node.Attributes.Count != 0) {
 								foreach(XmlAttribute attName in node.Attributes) {
-									if(attName.Name.ToString() == "alt") description = attName.Value.ToString();
-									if(attName.Name.ToString() == "class") hasClass = true;
+									if(attName.Name == "alt") description = attName.Value;
+									if(attName.Name == "class") hasClass = true;
 								}
 							}
-							if(!hasClass && !isLink) result += "[image|" + description + "|" + ProcessImage(node) + "]\n";
-							else if(!hasClass && isLink) result += "[image|" + description + "|" + ProcessImage(node);
-							else result += description + "|" + ProcessImage(node);
+							if(!hasClass && !isLink) sb.Append("[image|" + description + "|" + ProcessImage(node) + "]\n");
+							else if(!hasClass && isLink) sb.Append("[image|" + description + "|" + ProcessImage(node));
+							else sb.Append(description + "|" + ProcessImage(node));
 							break;
 						case "a":
 							bool isTable = false;
 							string link = "";
 							string target = "";
 							string title = "";
-							bool isInternalLink = false;
+							string formattedLink = "";
+							bool isSystemLink = false;
 							bool childImg = false;
 							bool pageLink = false;
 							if(node.FirstChild != null && node.FirstChild.Name == "img") childImg = true;
 								XmlAttributeCollection attribute = node.Attributes;
 								foreach(XmlAttribute attName in attribute) {
 									if(attName.Name != "id".ToLowerInvariant()) {
-										if(attName.Value == "_blank") target += "^";
-										if(attName.Name == "href") link += attName.Value.ToString();
-										if(attName.Name == "title") title += attName.Value.ToString();
-										if(attName.Value == "SystemLink".ToLowerInvariant()) isInternalLink = true;
-										if(attName.Value.ToLowerInvariant() == "unknownlink" || attName.Value.ToLowerInvariant() == "pagelink") pageLink = true;
+										if(attName.Value.ToLowerInvariant() == "_blank") target += "^";
+										if(attName.Name.ToLowerInvariant() == "href") link += attName.Value.Replace("%20", " ");
+										if(attName.Name.ToLowerInvariant() == "title") title += attName.Value;
+										if(attName.Name.ToLowerInvariant() == "class" && attName.Value.ToLowerInvariant() == "systemlink") isSystemLink = true;
+										if(attName.Name.ToLowerInvariant() == "class" && (attName.Value.ToLowerInvariant() == "unknownlink" || attName.Value.ToLowerInvariant() == "pagelink")) pageLink = true;
 									}
 									else {
 										anchor = true;
-										result += "[anchor|#" + attName.Value + "]" + ProcessChild(node.ChildNodes);
+										sb.Append("[anchor|#" + attName.Value + "]");
+										ProcessChild(node.ChildNodes, sb);
 										break;
 									}
 								}
-								if(isInternalLink) {
+								if(isSystemLink) {
 									string[] splittedLink = link.Split('=');
-									link = "c:" + splittedLink[1];
+									if(splittedLink.Length == 2) formattedLink = "c:" + splittedLink[1];
+									else formattedLink = link.LastIndexOf('/') > 0 ? link.Substring(link.LastIndexOf('/') + 1) : link;
 								}
-								else if(pageLink) link = link.Remove(link.IndexOf(Settings.PageExtension));
-								else link = ProcessLink(link);
+								else if(pageLink) {
+									formattedLink = link.LastIndexOf('/') > 0 ? link.Substring(link.LastIndexOf('/') + 1) : link;
+									formattedLink = formattedLink.Remove(formattedLink.IndexOf(Settings.PageExtension));
+									formattedLink = Uri.UnescapeDataString(formattedLink);
+								}
+								else {
+									formattedLink = ProcessLink(link);
+								}
 								if(!anchor && !isTable && !childImg) {
-									if(title != link) result += "[" + target + link + "|" + ProcessChild(node.ChildNodes) + "]";
-									else result += "[" + target + link + "|" + ProcessChild(node.ChildNodes) + "]";
+									if(HttpUtility.HtmlDecode(title) != HttpUtility.HtmlDecode(link)) {
+										sb.Append("[" + target + formattedLink + "|");
+										ProcessChild(node.ChildNodes, sb);
+										sb.Append("]");
+									}
+									else {
+										sb.Append("[" + target + formattedLink + "]");
+									}
 								}
-								if(!anchor && !childImg && isTable) result += "[" + target + link + "|" + ProcessChild(node.ChildNodes) + "]";
-								if(!anchor && childImg && !isTable) result += ProcessChild(node.ChildNodes) + "|" + target + link + "]";
+								if(!anchor && !childImg && isTable) {
+									sb.Append("[" + target + formattedLink + "|");
+									ProcessChild(node.ChildNodes, sb);
+									sb.Append("]");
+								}
+								if(!anchor && childImg && !isTable) {
+									ProcessChild(node.ChildNodes, sb);
+									sb.Append("|" + target + formattedLink + "]");
+								}
 							}
 							break;
 						default:
-							result += node.OuterXml;
+							sb.Append(node.OuterXml);
 							break;
 					}
 				}
-				else result += "";
 			}
-			return result;
 		}
 
 		private static XmlDocument FromHTML(TextReader reader) {
 		public static string ReverseFormat(string html) {
 			StringReader strReader = new StringReader(html);
 			XmlDocument x = FromHTML((TextReader)strReader);
-			if(x != null && x.HasChildNodes && x.FirstChild.HasChildNodes) return ProcessChild(x.FirstChild.ChildNodes);
-			else return "";
+			if(x != null && x.HasChildNodes && x.FirstChild.HasChildNodes) {
+				StringBuilder sb = new StringBuilder();
+				ProcessChild(x.FirstChild.ChildNodes, sb);
+				return sb.ToString();
+			}
+			else {
+				return "";
+			}
 		}
 	}
 }
 		}
 
 		/// <summary>
+		/// Gets the canonical URL tag for a page.
+		/// </summary>
+		/// <param name="requestUrl">The request URL.</param>
+		/// <param name="currentPage">The current page.</param>
+		/// <param name="nspace">The namespace.</param>
+		/// <returns>The canonical URL, or an empty string if <paramref name="requestUrl"/> is already canonical.</returns>
+		public static string GetCanonicalUrlTag(string requestUrl, PageInfo currentPage, NamespaceInfo nspace) {
+			string url = "";
+			if(nspace == null && currentPage.FullName == Settings.DefaultPage) url = Settings.GetMainUrl().ToString();
+			else url = Settings.GetMainUrl().ToString().TrimEnd('/') + "/" + currentPage.FullName + Settings.PageExtension;
+
+			// Case sensitive
+			if(url == requestUrl) return "";
+			else return "<link rel=\"canonical\" href=\"" + url + "\" />";
+		}
+
+		/// <summary>
 		/// Converts a byte number into a string, formatted using KB, MB or GB.
 		/// </summary>
 		/// <param name="bytes">The # of bytes.</param>

References/Lib/DotNetZipLib-DevKit-v1.9/Ionic.Zip.Reduced.dll

Binary file added.

References/Lib/DotNetZipLib-DevKit-v1.9/License.txt

+Microsoft Public License (Ms-PL)
+
+This license governs use of the accompanying software, the DotNetZip library ("the software"). If you use the software, you accept this license. If you do not accept the license, do not use the software.
+
+1. Definitions
+
+The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under U.S. copyright law.
+
+A "contribution" is the original software, or any additions or changes to the software.
+
+A "contributor" is any person that distributes its contribution under this license.
+
+"Licensed patents" are a contributor's patent claims that read directly on its contribution.
+
+2. Grant of Rights
+
+(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create.
+
+(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software.
+
+3. Conditions and Limitations
+
+(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks.
+
+(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically.
+
+(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software.
+
+(D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license.
+
+(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement. 
+
+

ScrewTurnWiki.sln

 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RatingManagerPlugin", "RatingManagerPlugin\RatingManagerPlugin.csproj", "{B65C793F-62C4-4B81-95C4-3E4805E80411}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackupRestore", "BackupRestore\BackupRestore.csproj", "{06320590-F6FC-4F8B-B3A2-2B5991676BE8}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
 		{B65C793F-62C4-4B81-95C4-3E4805E80411}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{B65C793F-62C4-4B81-95C4-3E4805E80411}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{B65C793F-62C4-4B81-95C4-3E4805E80411}.Release|Any CPU.Build.0 = Release|Any CPU
+		{06320590-F6FC-4F8B-B3A2-2B5991676BE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{06320590-F6FC-4F8B-B3A2-2B5991676BE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{06320590-F6FC-4F8B-B3A2-2B5991676BE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{06320590-F6FC-4F8B-B3A2-2B5991676BE8}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

WebApplication/Admin.master

 <html dir="<%= ScrewTurn.Wiki.Settings.Direction %>" xmlns="http://www.w3.org/1999/xhtml" >
 <head runat="server">
 	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-    <title>Administration</title>
-    <link rel="shortcut icon" href="Images/Icon.ico" type="image/x-icon" />
-    <link rel="stylesheet" type="text/css" href="Themes/Admin.css" />
-    <link rel="stylesheet" type="text/css" href="Themes/Editor.css" />
-    <meta http-equiv="X-UA-Compatible" content="IE=8" />
-    <asp:Literal ID="lblJS" runat="server" EnableViewState="false" />
-    <asp:ContentPlaceHolder ID="head" runat="server" />
+	<title>Administration</title>
+	<link rel="shortcut icon" href="Images/Icon.ico" type="image/x-icon" />
+	<link rel="stylesheet" type="text/css" href="Themes/Admin.css" />
+	<link rel="stylesheet" type="text/css" href="Themes/Editor.css" />
+	<asp:Literal ID="lblJS" runat="server" EnableViewState="false" />
+	<asp:ContentPlaceHolder ID="head" runat="server" />
 </head>
 <body>
-    <form id="frmAdmin" runat="server">
-    <div id="MainDiv">
-    
+	<form id="frmAdmin" runat="server">
+	<div id="MainDiv">
+	
 		<asp:Literal ID="lblStrings" runat="server" meta:resourcekey="lblStringsResource1" />
 		<script type="text/javascript">
 		<!--
 			}
 		// -->
 		</script>
-    
+	
 		<h1 class="admintitle">
 			<a href="?Refresh=1" title="<% Response.Write(ScrewTurn.Wiki.Properties.Messages.Refresh); %>"><img src="Images/Refresh.png" alt="<% Response.Write(ScrewTurn.Wiki.Properties.Messages.Refresh); %>" /></a>
 			<asp:Literal ID="lblAdminTitle" runat="server" Text="Administration" meta:resourcekey="lblAdminTitleResource1" /> - 
 			<a href="Default.aspx"><asp:Literal ID="lblHomeLink" runat="server" Text="Main Page" meta:resourcekey="lblHomeLinkResource1" /> &raquo;</a>
 		</h1>
-		<asp:Label ID="lblBrowserSupport" runat="server" Visible="False" CssClass="resulterror" 
-			Text="Your Browser might present problems with this Page (Internet Explorer 7+ and Firefox 3+ are suggested)." 
-			meta:resourcekey="lblBrowserSupportResource1" />
 		<br />
 		
 		<div id="TabDiv">
 			<asp:HyperLink ID="lnkSelectConfig" runat="server" Text="Configuration" 
 				CssClass="tab" NavigateUrl="~/AdminConfig.aspx" meta:resourcekey="lnkSelectConfigResource1" />
 		</div>
-    
-        <asp:ContentPlaceHolder ID="cphAdmin" runat="server" />
-        <br /><br /><br />
+	
+		<asp:ContentPlaceHolder ID="cphAdmin" runat="server" />
+		<br /><br /><br />
 		
 		<div id="LicenseInfoDiv">
 			<h2 class="separator">GNU General Public License v2</h2>
 			<p>This Program is released under the GNU General Public License v2. You cannot remove this statement.<br />
 			<a href="GPL.txt" target="_blank">View the GNU General Public License v2</a> or visit the <a class="externallink" href="http://www.gnu.org" target="_blank">GNU</a> website.</p>
 		</div>
-        
-        <!-- Used to keep the session alive -->
+		
+		<!-- Used to keep the session alive -->
 		<iframe src="SessionRefresh.aspx" style="width: 1px; height: 1px; border: none;" scrolling="no"></iframe> 
 		
-    </div>