diff --git a/40Fingers.SeoRedirect.csproj b/40Fingers.SeoRedirect.csproj new file mode 100644 index 0000000..6900d57 --- /dev/null +++ b/40Fingers.SeoRedirect.csproj @@ -0,0 +1,268 @@ + + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {2E88F654-FA5C-446C-A3C4-F87576591252} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + FortyFingers.SeoRedirect + 40Fingers.DNN.Modules.SeoRedirect + + + 4.0 + + + v4.5 + Svn + Svn + Svn + SubversionScc + false + + + + + + + + + + true + full + bin\ + TRACE;DEBUG + prompt + 4 + bin\40Fingers.DNN.Modules.SeoRedirect.XML + false + + + pdbonly + true + bin\ + prompt + 4 + bin\40Fingers.DNN.Modules.SeoRedirect.XML + false + + + + ..\..\..\bin\DotNetNuke.dll + False + + + _external\DotNetNuke.Instrumentation.dll + False + + + _external\DotNetNuke.Log4Net.dll + False + + + ..\..\..\bin\DotNetNuke.Web.dll + False + + + False + _external\DotNetNuke.Web.Client.dll + False + + + False + _external\DotNetNuke.WebUtility.dll + False + + + ..\..\..\bin\Microsoft.ApplicationBlocks.Data.dll + False + + + False + _external\Newtonsoft.Json.dll + False + + + + + + + + _external\System.Net.Http.dll + False + + + False + _external\System.Net.Http.Formatting.dll + False + + + + + + + + + + + False + _external\System.Web.Http.dll + False + + + + + + + + + + + + + + + + + + + + + + + + + + Edit.ascx + ASPXCodeBehind + + + Edit.ascx + + + + Settings.ascx + ASPXCodeBehind + + + Settings.ascx + + + View.ascx + ASPXCodeBehind + + + View.ascx + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + web.config + + + web.config + + + + + + + + + + + + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + + + False + True + 62596 + / + + True + http://production.local/482 + False + False + + + False + + + + + \ No newline at end of file diff --git a/40Fingers.SeoRedirect.dnn b/40Fingers.SeoRedirect.dnn new file mode 100644 index 0000000..dc89bde --- /dev/null +++ b/40Fingers.SeoRedirect.dnn @@ -0,0 +1,99 @@ + + + 40Fingers SeoRedirect + This is a 40FINGERS module will allow you to monintor and manage the 404's your DNN site is receiving. + + 40Fingers + 40Fingers + http://www.40fingers.net + info@40fingers.net + + The license for this package is not currently included within the installation file, please check with the vendor for full license details. + + + 07.02.01 + + + + + 40Fingers.SeoRedirect + 40Fingers/SeoRedirect + + + + + 40Fingers SeoRedirect + 0 + + + + DesktopModules/40Fingers/SeoRedirect/View.ascx + False + + View + + + 0 + + + Edit + DesktopModules/40Fingers/SeoRedirect/Edit.ascx + False + Change + Edit + + + + + Settings + DesktopModules/40Fingers/SeoRedirect/Settings.ascx + True + SeoRedirect Settings + Admin + + + + + + + + + + + bin + + 40Fingers.DNN.Modules.SeoRedirect.dll + + + + + + DesktopModules/40Fingers/SeoRedirect + + Resources.zip + + + + + + DesktopModules\40Fingers\SeoRedirect + + + + + + + + \ No newline at end of file diff --git a/API/Models/MappingsModel.cs b/API/Models/MappingsModel.cs new file mode 100644 index 0000000..d540927 --- /dev/null +++ b/API/Models/MappingsModel.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using DotNetNuke.Entities.Portals; +using DotNetNuke.Entities.Tabs; +using FortyFingers.SeoRedirect.Components; +using FortyFingers.SeoRedirect.Components.Data; +using Newtonsoft.Json; + +namespace FortyFingers.SeoRedirect.API.Models +{ + public class MappingsModel + { + public MappingsModel() + { + Mappings = new List(); + } + [JsonProperty("mappings")] + public List Mappings { get; set; } + } + + public class MappingModel + { + public MappingModel() { } + public MappingModel(Mapping mapping) : this() + { + Id = mapping.Id; + SourceUrl = mapping.SourceUrl; + TargetUrl = mapping.TargetUrl; + TargetTabId = mapping.TargetTabId; + UseRegex = mapping.UseRegex; + } + [JsonProperty("id")] + public string Id { get; set; } + [JsonProperty("sourceUrl")] + public string SourceUrl { get; set; } + [JsonProperty("targetUrl")] + public string TargetUrl { get; set; } + [JsonProperty("targetTabId")] + public int TargetTabId { get; set; } + [JsonProperty("targetTabName")] + public string TargetTabName + { + get + { + var retval = ""; + if (TargetTabId > 0) + { + var tab = new TabController().GetTab(TargetTabId, PortalSettings.Current.PortalId, false); + if (tab != null) + { + retval = tab.TabName; + } + } + return retval; + } + } + [JsonProperty("useRegex")] + public bool UseRegex { get; set; } + } +} \ No newline at end of file diff --git a/API/Models/UnhandledUrlsModel.cs b/API/Models/UnhandledUrlsModel.cs new file mode 100644 index 0000000..9eff21d --- /dev/null +++ b/API/Models/UnhandledUrlsModel.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using FortyFingers.SeoRedirect.Components.Data; +using Newtonsoft.Json; + +namespace FortyFingers.SeoRedirect.API.Models +{ + public class UnhandledUrlsModel + { + public UnhandledUrlsModel() + { + Urls = new List(); + } + [JsonProperty("urls")] + public List Urls { get; set; } + } + + public class UnhandledUrlModel + { + public UnhandledUrlModel(RedirectLogUrl url) + { + Url = url.Url; + Days = url.Days; + Occurrences = url.Occurrences; + } + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("days")] + public int Days { get; set; } + [JsonProperty("occurrences")] + public int Occurrences { get; set; } + } +} \ No newline at end of file diff --git a/API/RouteMapper.cs b/API/RouteMapper.cs new file mode 100644 index 0000000..72ea7e9 --- /dev/null +++ b/API/RouteMapper.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Http; +using DotNetNuke.Web.Api; + +namespace FortyFingers.SeoRedirect.API +{ + public class RouteMapper : IServiceRouteMapper + { + /// + /// RegisterRoutes is used to register the module's routes + /// + /// + public void RegisterRoutes(IMapRoute mapRouteManager) + { + mapRouteManager.MapHttpRoute( + moduleFolderName: "40Fingers", + routeName: "default", + url: "{controller}/{action}/{itemId}", + defaults: new { itemId = RouteParameter.Optional }, + namespaces: new[] { "FortyFingers.SeoRedirect.API" }); + + } + } +} \ No newline at end of file diff --git a/API/SeoRedirectController.cs b/API/SeoRedirectController.cs new file mode 100644 index 0000000..bcaf780 --- /dev/null +++ b/API/SeoRedirectController.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Web; +using System.Web.Http; +using DotNetNuke.Common.Utilities; +using DotNetNuke.Security; +using DotNetNuke.UI.Modules; +using DotNetNuke.Web.Api; +using FortyFingers.SeoRedirect.API.Models; +using FortyFingers.SeoRedirect.Components; + +namespace FortyFingers.SeoRedirect.API +{ + + [SupportedModules("40Fingers.SeoRedirect")] // can be comma separated list of supported module + public class SeoRedirectController : DnnApiController + { + [ValidateAntiForgeryToken] + [DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.Admin)] + [HttpGet] + public HttpResponseMessage GetUnhandledUrls() + { + var _config = new Components.Config(ActiveModule.ModuleSettings, ActiveModule.ModuleID, ActiveModule.TabModuleID); + var data = RedirectController.GetTopUnhandledUrls(PortalSettings.PortalId, Constants.UnhandledUrlsMaxDays, _config.NoOfEntries); + + var retval = new UnhandledUrlsModel(); + data.ForEach(u => retval.Urls.Add(new UnhandledUrlModel(u))); + + return Request.CreateResponse(HttpStatusCode.OK, retval); + } + + [ValidateAntiForgeryToken] + [DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.Admin)] + [HttpPost] + public HttpResponseMessage SaveRedirect(MappingModel model) + { + var map = new Mapping(); + map.SourceUrl = model.SourceUrl; + if (!string.IsNullOrEmpty(model.TargetUrl)) + { + map.TargetUrl = model.TargetUrl; + } + else if (model.TargetTabId > 0) + { + map.TargetTabId = model.TargetTabId; + map.TargetUrl = DotNetNuke.Common.Globals.NavigateURL(map.TargetTabId, PortalSettings, ""); + } + else + { + // keep giving 404 + map = null; + } + if (map != null) + { + map.UseRegex = false; + var cfg = RedirectConfig.Instance; + cfg.Mappings.Add(map); + cfg.ToFile(Common.RedirectConfigFile()); + RedirectConfig.Reload(PortalSettings.PortalId); + } + + // set handledon/handledby in table + RedirectController.SetHandledUrl(model.SourceUrl); + + return Request.CreateResponse(HttpStatusCode.OK, new {}); + + } + + [ValidateAntiForgeryToken] + [DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.Admin)] + [HttpGet] + public HttpResponseMessage GetMappings() + { + var data = RedirectConfig.Instance.Mappings; + + + + var retval = new MappingsModel(); + data.ForEach(u => retval.Mappings.Add(new MappingModel(u))); + + return Request.CreateResponse(HttpStatusCode.OK, retval); + } + + [ValidateAntiForgeryToken] + [DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.Admin)] + [HttpPost] + public HttpResponseMessage SaveMapping(MappingModel model) // string id, bool useRegex, string sourceUrl, string targetUrl, int targetTabId) + { + var map = RedirectConfig.Instance.Mappings.FirstOrDefault(m => m.Id == model.Id); + + if (!string.IsNullOrEmpty(model.Id) && map == null) return Request.CreateResponse(HttpStatusCode.NotFound); + + if (map == null && string.IsNullOrEmpty(model.Id)) + { + map = new Mapping(); + } + + map.SourceUrl = model.SourceUrl; + map.UseRegex = model.UseRegex; + if (!string.IsNullOrEmpty(model.TargetUrl)) + { + map.TargetTabId = Null.NullInteger; + map.TargetUrl = model.TargetUrl; + } + else if (model.TargetTabId > 0) + { + map.TargetTabId = model.TargetTabId; + map.TargetUrl = DotNetNuke.Common.Globals.NavigateURL(map.TargetTabId, PortalSettings, ""); + } + else + { + // remove mapping + RedirectConfig.Instance.Mappings.Remove(map); + map = null; + } + if (map != null) + { + RedirectConfig.Instance.Mappings.Add(map); + } + + RedirectConfig.Instance.ToFile(Common.RedirectConfigFile()); + RedirectConfig.Reload(PortalSettings.PortalId); + + // set handledon/handledby in table + if(!model.UseRegex) + RedirectController.SetHandledUrl(model.SourceUrl); + + return Request.CreateResponse(HttpStatusCode.OK, map == null ? null : new MappingModel(map)); + } + + } +} \ No newline at end of file diff --git a/App_LocalResources/ConfigDefaults.resx b/App_LocalResources/ConfigDefaults.resx new file mode 100644 index 0000000..4fdb1b6 --- /dev/null +++ b/App_LocalResources/ConfigDefaults.resx @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/App_LocalResources/Edit.ascx.resx b/App_LocalResources/Edit.ascx.resx new file mode 100644 index 0000000..3ae71e1 --- /dev/null +++ b/App_LocalResources/Edit.ascx.resx @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Edit your mappings here: + + + Mappings + + + Remove from list (future 404's will make the URL reappear in the list) + + + This url is mapped to a page, but I can't find that page. + + + Redirect to page: + + + Redirect to url: + + + Return + + + Save mappings + + + Source url + + + Target url or TabId + + + Use Regex + + \ No newline at end of file diff --git a/App_LocalResources/SharedResources.resx b/App_LocalResources/SharedResources.resx new file mode 100644 index 0000000..e70e848 --- /dev/null +++ b/App_LocalResources/SharedResources.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 10 + + \ No newline at end of file diff --git a/App_LocalResources/View.ascx.resx b/App_LocalResources/View.ascx.resx new file mode 100644 index 0000000..d1aacc0 --- /dev/null +++ b/App_LocalResources/View.ascx.resx @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + + As of DNN version 7.1 there needs to be a setting called 'AUM_ErrorPage404' with the tabid of your 404 page as SettingValue. This is especially important when you're using Advanced FriendlyURLs, but we advice you to create it anyway. + + + Edit Mappings + + + Remove mapping (no redirect will occur) + + + Count + + + Redirect to page: + + + Redirect to url: + + + Top {0} unhandled URL's + + + Requested Url + + \ No newline at end of file diff --git a/Components/Common.cs b/Components/Common.cs new file mode 100644 index 0000000..974a583 --- /dev/null +++ b/Components/Common.cs @@ -0,0 +1,89 @@ +using System; +using System.Text.RegularExpressions; +using System.Web; +using System.Web.UI; +using System.Web.UI.HtmlControls; +using DotNetNuke.Common; +using DotNetNuke.Entities.Portals; +using DotNetNuke.Instrumentation; + +namespace FortyFingers.SeoRedirect.Components +{ + public static class Common + { + public static ILog Logger + { + get + { + return LoggerSource.Instance.GetLogger(Constants.MODULE_LOGGER_NAME); + } + } + + public static PortalSettings CurrentPortalSettings + { + get + { + var retval = PortalSettings.Current; + + // if there's no current portal, try and get it from the requested domain name + if (retval == null) + { + var domainName = Globals.GetDomainName(HttpContext.Current.Request, true); + + // in multiligual sites, DNN6 might have the current locale appended + var portalAliasInfo = PortalAliasController.GetPortalAliasInfo(domainName); + if (portalAliasInfo == null) + { + if (Regex.IsMatch(domainName, ".*//??-??")) + { + domainName = domainName.Substring(0, domainName.IndexOf("/")); + portalAliasInfo = PortalAliasController.GetPortalAliasInfo(domainName); + } + } + + if (portalAliasInfo != null) + { + retval = new PortalSettings(portalAliasInfo.PortalID); + } + } + return retval; + } + } + + public static string RedirectConfigFile() + { + return Globals.ResolveUrl(String.Format("{0}\\{1}", CurrentPortalSettings.HomeDirectoryMapPath, + Constants.PORTALREDIRECTCONFIGFILE)); + } + + public static void AddCssLink(string linkFile, Page page) + { + HtmlLink oLink = new HtmlLink(); + if (!linkFile.EndsWith("/")) + { + oLink.Attributes["rel"] = "stylesheet"; + oLink.Attributes["media"] = "screen"; + oLink.Attributes["type"] = "text/css"; + oLink.Attributes["href"] = linkFile; + Control oCSS = page.FindControl("CSS"); + // try to insert it in a DNN way first + if ((oCSS != null)) + { + oCSS.Controls.Add(oLink); + } + else + { + page.Header.Controls.Add(oCSS); + } + } + } + + public static bool IsInt(string s) + { + int i; + var retval = int.TryParse(s.Trim(), out i); + + return retval; + } + } +} \ No newline at end of file diff --git a/Components/Config.cs b/Components/Config.cs new file mode 100644 index 0000000..73249b9 --- /dev/null +++ b/Components/Config.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.UI; +using DotNetNuke.Common.Utilities; +using DotNetNuke.Entities.Modules; +using DotNetNuke.Entities.Portals; +using DotNetNuke.Services.Localization; + +namespace FortyFingers.SeoRedirect.Components +{ + public class Config + { + #region Standard code + + private Hashtable _settings = null; + private int _tabModuleId = Null.NullInteger; + + /// + /// 40fingers config class + /// + /// + /// + /// + public Config(Hashtable settings, int moduleId, int tabModuleId) + { + _settings = settings; + ModuleId = moduleId; + _tabModuleId = tabModuleId; + } + /// + /// module id + /// + public int ModuleId + { + get; + private set; + } + + private string GetDefault(string setting) + { + return Localization.GetString(setting, ConfigDefaultsResourceFile); + } + + private int GetSettingInt(string setting, bool useDefault = true) + { + int i = Null.NullInteger; + string settingValue = ""; + if (_settings.ContainsKey(setting)) + { + settingValue = _settings[setting].ToString(); + } + else if (useDefault) + { + settingValue = GetDefault(setting); + } + int.TryParse(settingValue, out i); + + return i; + } + + private DateTime GetSettingDateTime(string setting, bool useDefault = true) + { + DateTime d = Null.NullDate; + string settingValue = ""; + if (_settings.ContainsKey(setting)) + { + settingValue = _settings[setting].ToString(); + } + else if (useDefault) + { + settingValue = GetDefault(setting); + } + DateTime.TryParse(settingValue, out d); + + return d; + } + + private string GetSetting(string setting, bool useDefault = true) + { + string settingValue = ""; + if (_settings.ContainsKey(setting)) + { + settingValue = _settings[setting].ToString(); + } + else if (useDefault) + { + settingValue = GetDefault(setting); + } + + return settingValue; + } + + private PortalSettings Ps + { + get { return PortalSettings.Current; } + } + + private ModuleController _moduleCtrl; + private ModuleController ModuleCtrl + { + get + { + if (_moduleCtrl == null) + { + _moduleCtrl = new ModuleController(); + } + + return _moduleCtrl; + } + } + #endregion + + private const string ConfigDefaultsResourceFile = "~/DesktopModules/40Fingers/SeoRedirect/App_LocalResources/ConfigDefaults.resx"; + + /// + /// number of settings to show + /// + public int NoOfEntries + { + get { return GetSettingInt("NoOfEntries", true); } + set { ModuleCtrl.UpdateModuleSetting(ModuleId, "NoOfEntries", value.ToString()); } + } + + } +} \ No newline at end of file diff --git a/Components/Constants.cs b/Components/Constants.cs new file mode 100644 index 0000000..dbc8e25 --- /dev/null +++ b/Components/Constants.cs @@ -0,0 +1,21 @@ +namespace FortyFingers.SeoRedirect.Components +{ + + public class Constants + { + /// + /// Module root url, for use with ResolveUrl() + /// + public const string DESKTOPMODULES_MODULEROOT_URL = "~/DesktopModules/40Fingers/SeoRedirect/"; + + public const string PORTALREDIRECTCONFIGFILE = "40Fingers\\SeoRedirect\\redirectconfig.xml"; + + public const string SHAREDRESOURCES = + "~/DesktopModules/40Fingers/SeoRedirect/App_LocalResources/SharedResources.resx"; + + public const int UnhandledUrlsMaxDays = 30; + //public const int UnhandledUrlsMaxResults = 5; + + public const string MODULE_LOGGER_NAME = "40Fingers.SeoRedirect"; + } +} \ No newline at end of file diff --git a/Components/Data/DataProvider.cs b/Components/Data/DataProvider.cs new file mode 100644 index 0000000..e851571 --- /dev/null +++ b/Components/Data/DataProvider.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Web; +using DotNetNuke; +using DotNetNuke.Framework; + +namespace FortyFingers.SeoRedirect.Components.Data +{ + public abstract class DataProvider + { + #region Shared/Static Methods + + // singleton reference to the instantiated object + + private static DataProvider objProvider; + // constructor + static DataProvider() + { + CreateProvider(); + } + + // dynamically create provider + private static void CreateProvider() + { + objProvider = (DataProvider)Reflection.CreateObject("data", "FortyFingers.SeoRedirect.Components.Data", ""); + } + + // return the provider + public static DataProvider Instance() + { + return objProvider; + } + + #endregion + + public abstract void AddRedirectLog(int portalId, string requestedUrl, DateTime requestDateTime, string referrer, + string httpUserAgent, string redirectedToUrl); + + public abstract IDataReader GetTopUnhandledUrls(int portalId, DateTime startDate, int maxUrls); + public abstract void SetHandledUrl(string url, DateTime handledOn, string handledBy); + } +} \ No newline at end of file diff --git a/Components/Data/RedirectLogItem.cs b/Components/Data/RedirectLogItem.cs new file mode 100644 index 0000000..5098df9 --- /dev/null +++ b/Components/Data/RedirectLogItem.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace FortyFingers.SeoRedirect.Components.Data +{ + public class RedirectLogItem + { + public int Id { get; set; } + public string RequestedUrl { get; set; } + public DateTime RequestDateTime { get; set; } + public string Referrer { get; set; } + public string HTTP_USER_AGENT { get; set; } + public string RedirectedToUrl { get; set; } + public DateTime HandledOn { get; set; } + public string HandledBy { get; set; } + public int PortalId { get; set; } + } + + public class RedirectLogUrl + { + public string Url { get; set; } + public int Days { get; set; } + public int Occurrences { get; set; } + } +} \ No newline at end of file diff --git a/Components/Data/SqlDataProvider.cs b/Components/Data/SqlDataProvider.cs new file mode 100644 index 0000000..c967088 --- /dev/null +++ b/Components/Data/SqlDataProvider.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Linq; +using System.Web; +using DotNetNuke; +using DotNetNuke.Common.Utilities; +using DotNetNuke.Framework.Providers; +using Microsoft.ApplicationBlocks.Data; + +namespace FortyFingers.SeoRedirect.Components.Data +{ + public class SqlDataProvider : DataProvider + { + private const string OwnerPrefix = "40F_"; + private const string ModulePrefix = "SEO_"; + + private const string ProviderType = "data"; + + private readonly string _connectionString; + + private readonly string _databaseOwner; + private readonly string _objectQualifier; + + private readonly ProviderConfiguration _providerConfiguration = ProviderConfiguration.GetProviderConfiguration(ProviderType); + + private readonly string _providerPath; + + private string GetObjectName(string shortName) + { + return DatabaseOwner + ObjectQualifier + OwnerPrefix + ModulePrefix + shortName; + } + + #region Constructors + + public SqlDataProvider() + { + // Read the configuration specific information for this provider + var objProvider = (Provider)(_providerConfiguration.Providers[_providerConfiguration.DefaultProvider]); + + // Read the attributes for this provider + _connectionString = DotNetNuke.Data.DataProvider.Instance().ConnectionString; + + _providerPath = objProvider.Attributes["providerPath"]; + + _objectQualifier = objProvider.Attributes["objectQualifier"]; + if (!string.IsNullOrEmpty(_objectQualifier) && _objectQualifier.EndsWith("_") == false) + { + _objectQualifier += "_"; + } + + _databaseOwner = objProvider.Attributes["databaseOwner"]; + if (!string.IsNullOrEmpty(_databaseOwner) && _databaseOwner.EndsWith(".") == false) + { + _databaseOwner += "."; + } + } + + #endregion + + #region Properties + + public string ConnectionString + { + get + { + return _connectionString; + } + } + + public string ProviderPath + { + get + { + return _providerPath; + } + } + + public string ObjectQualifier + { + get + { + return _objectQualifier; + } + } + + public string DatabaseOwner + { + get + { + return _databaseOwner; + } + } + + #endregion + + public override void AddRedirectLog(int portalId, string requestedUrl, DateTime requestDateTime, string referrer, string httpUserAgent, + string redirectedToUrl) + { + SqlHelper.ExecuteNonQuery(ConnectionString, GetObjectName("AddRedirectLog"), + portalId, requestedUrl, requestDateTime, referrer, httpUserAgent, redirectedToUrl); + } + + public override IDataReader GetTopUnhandledUrls(int portalId, DateTime startDate, int maxUrls) + { + return SqlHelper.ExecuteReader(ConnectionString, GetObjectName("GetTopUnhandledUrls"), portalId, startDate, maxUrls); + } + + public override void SetHandledUrl(string url, DateTime handledOn, string handledBy) + { + SqlHelper.ExecuteNonQuery(ConnectionString, GetObjectName("SetHandledUrl"), url, handledOn, handledBy); + } + } +} \ No newline at end of file diff --git a/Components/Icons.cs b/Components/Icons.cs new file mode 100644 index 0000000..4130bf6 --- /dev/null +++ b/Components/Icons.cs @@ -0,0 +1,59 @@ +using DotNetNuke; +using DotNetNuke.Common; + +namespace FortyFingers.SeoRedirect.Components +{ + /// + /// List icons by the MEANING, NOT filename. That way we can easily change an icon for a single purpose. + /// + public enum IconTypes + { + Add, + Delete, + Edit, + Save, + Cancel + } + public static class Icons + { + /// + /// Extension method to provide for easy URLs for icons. Extends Educo.MyStek.Components.IconTypes + /// + /// + /// + public static string GetUrl(this IconTypes iconType) + { + var retval = ""; + + switch (iconType) + { + case IconTypes.Delete: + retval = Globals.ResolveUrl(Constants.DESKTOPMODULES_MODULEROOT_URL + + "img/icons/delete.png"); + break; + case IconTypes.Add: + retval = Globals.ResolveUrl(Constants.DESKTOPMODULES_MODULEROOT_URL + + "img/icons/add.png"); + break; + case IconTypes.Edit: + retval = Globals.ResolveUrl(Constants.DESKTOPMODULES_MODULEROOT_URL + + "img/icons/pencil.png"); + break; + case IconTypes.Save: + retval = Globals.ResolveUrl(Constants.DESKTOPMODULES_MODULEROOT_URL + + "img/icons/disk.png"); + break; + case IconTypes.Cancel: + retval = Globals.ResolveUrl(Constants.DESKTOPMODULES_MODULEROOT_URL + + "img/icons/cancel.png"); + break; + default: + retval = Globals.ResolveUrl(Constants.DESKTOPMODULES_MODULEROOT_URL + + "img/icons/application.png"); + break; + } + + return retval; + } + } +} \ No newline at end of file diff --git a/Components/Mapping.cs b/Components/Mapping.cs new file mode 100644 index 0000000..c717539 --- /dev/null +++ b/Components/Mapping.cs @@ -0,0 +1,73 @@ +using System; +using System.Xml; +using System.Xml.Serialization; + +namespace FortyFingers.SeoRedirect.Components +{ + public class Mapping + { + // dummy xmldocument for creating cdata elements + [XmlIgnore] + private XmlDocument _dummyXmlDoc = null; + [XmlIgnore] + private XmlDocument DummyXmlDoc + { + get + { + if(_dummyXmlDoc == null) + _dummyXmlDoc = new XmlDocument(); + + return _dummyXmlDoc; + } + } + + /// + /// Id of the mapping + /// + public string Id { get; set; } + + // Ignore this property in serialization because XmlSerializer doesn't support CDATA + [XmlIgnore] + public string SourceUrl { get; set; } + + // Serialize this eloement as SourceUrl + // Method will only be called while serializing + [XmlElement("SourceUrl")] + public XmlCDataSection CDataSourceUrl + { + get + { + return DummyXmlDoc.CreateCDataSection(SourceUrl); + } + set + { + SourceUrl = value.Value; + } + } + + // Ignore this property in serialization because XmlSerializer doesn't support CDATA + [XmlIgnore] + public string TargetUrl { get; set; } + + // Serialize this eloement as TargetUrl + // Method will only be called while serializing + [XmlElement("TargetUrl")] + public XmlCDataSection CDataTargetUrl + { + get + { + return DummyXmlDoc.CreateCDataSection(TargetUrl); + } + set + { + TargetUrl = value.Value; + } + } + + [XmlElement("TargetTabId")] + public int TargetTabId { get; set; } + + [XmlElement("UseRegex")] + public bool UseRegex { get; set; } + } +} \ No newline at end of file diff --git a/Components/RedirectConfig.cs b/Components/RedirectConfig.cs new file mode 100644 index 0000000..b71ad7a --- /dev/null +++ b/Components/RedirectConfig.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Web.Caching; +using System.Xml.Serialization; +using DotNetNuke.Common; +using DotNetNuke.Common.Utilities; +using DotNetNuke.Entities.Portals; +using DotNetNuke.Services.Cache; +using DotNetNuke.Services.Exceptions; + +namespace FortyFingers.SeoRedirect.Components +{ + public class RedirectConfig + { + #region Singleton stuff + + /// + /// Static field to hold the instance of the RedirectConfigs + /// + private static Dictionary _instances; + + /// + /// Lock oject to prevent sync issues between threads + /// + private static object _lock = new object(); + + public static RedirectConfig Instance + { + get + { + if ((_instances == null)) + { + lock (_lock) + { + _instances = new Dictionary(); + } + } + + var pid = Common.CurrentPortalSettings.PortalId; + if (!_instances.ContainsKey(pid)) + { + lock (_lock) + { + _instances.Add(pid, GetConfig()); + } + } + + return _instances[pid]; + } + } + + #endregion + + + public RedirectConfig() + { + Mappings = new List(); + } + + public static void Reload(int portalId) + { + if (_instances.ContainsKey(Common.CurrentPortalSettings.PortalId)) + { + lock (_lock) + { + _instances.Remove(portalId); + } + } + } + + //private static string RedirectConfigCacheKey + //{ + // get { return String.Format("40FINGERS.SeoRedirect.CONFIG,{0}", Common.CurrentPortalSettings.PortalId); } + //} + //public static RedirectConfig GetConfig() + //{ + // return GetConfig(true); + //} + + private static string getConfigLockObject = "lockIt"; + private static RedirectConfig GetConfig() + { + RedirectConfig config = null; + //if (useCache) + // try + // { + // config = DataCache.GetCache(RedirectConfigCacheKey); + // } + // catch (Exception e) + // { + // /* do nothing */ + // } + + // if there are no mappings, the use of this module is pointless + // so we'll assume something went wrong reading the file in that case + if (config == null || config.Mappings == null || config.Mappings.Count == 0) + { + lock (getConfigLockObject) + { + var file = Common.RedirectConfigFile(); + config = FromFile(file); + // set the id's if they're empty. This comes in handy for backwards compatibility + if (config.Mappings.Any(m => String.IsNullOrEmpty(m.Id))) + { + foreach (var mapping in config.Mappings.Where(m => String.IsNullOrEmpty(m.Id))) + { + mapping.Id = Guid.NewGuid().ToString(); + } + config.ToFile(file); + } + //DataCache.SetCache(RedirectConfigCacheKey, config, new DNNCacheDependency(file)); + } + } + return config; + } + + public void ResetMappingDictionaries() + { + lock (mappingsDicLockObject) + { + _MappingsDictionary = null; + } + lock (mappingsDicRegexLockObject) + { + _MappingsDictionaryRegex = null; + } + } + + //private static string RedirectMappingsDictionaryCacheKey(bool usingRegex) + //{ + // return String.Format("40FINGERS.SeoRedirect.MAPPINGS,{0},{1}", Common.CurrentPortalSettings.PortalId, usingRegex); + //} + private static string mappingsDicLockObject = "lockIt"; + private static string mappingsDicRegexLockObject = "lockIt"; + private Dictionary _MappingsDictionary = null; + private Dictionary _MappingsDictionaryRegex = null; + public Dictionary MappingsDictionary(bool usingRegex) + { + if (usingRegex && _MappingsDictionaryRegex != null) + { + return _MappingsDictionaryRegex; + } + if (!usingRegex && _MappingsDictionary != null) + { + return _MappingsDictionary; + } + + var lockobject = usingRegex ? mappingsDicRegexLockObject : mappingsDicLockObject; + + lock (lockobject) + { + Dictionary dic = null; + + //try + //{ + // dic = DataCache.GetCache>(RedirectMappingsDictionaryCacheKey(usingRegex)); + //} + //catch (NullReferenceException e) + //{ + // // do nothing. we should handle caching differently + //} + // if there are no mappings, the use of this module is pointless + // so we'll assume something went wrong reading the file in that case + dic = new Dictionary(); + // add the sourceurl lowercased: we're case insensitive + foreach (var mapping in Mappings) + { + if (mapping.UseRegex == usingRegex && !dic.ContainsKey(mapping.SourceUrl.ToLower())) + { + string targetUrl; + if (mapping.TargetTabId > 0) + { + targetUrl = mapping.TargetUrl; + //targetUrl = Globals.NavigateURL(mapping.TargetTabId, Common.CurrentPortalSettings, ""); + } + else + { + targetUrl = mapping.TargetUrl; + } + dic.Add(mapping.SourceUrl.ToLower(), targetUrl); + } + } + + if (usingRegex) + { + _MappingsDictionaryRegex = dic; + } + else + { + _MappingsDictionary = dic; + } + + //DataCache.SetCache(RedirectMappingsDictionaryCacheKey(usingRegex), dic, new DNNCacheDependency(Common.RedirectConfigFile())); + return dic; + } + } + + //private static void ClearCache() + //{ + // DataCache.RemoveCache(RedirectConfigCacheKey); + // DataCache.RemoveCache(RedirectMappingsDictionaryCacheKey(true)); + // DataCache.RemoveCache(RedirectMappingsDictionaryCacheKey(false)); + //} + + //public static bool Exists() + //{ + // return File.Exists(Common.RedirectConfigFile()); + //} + private static RedirectConfig FromFile(string filename) + { + // create the file if it doesn't exsist + //if (!File.Exists(filename)) + // CreateFile(filename); + + RedirectConfig retval = null; + FileStream fs = null; + + try + { + // if it exists we can just open it + // otherwise return new configuration + if (File.Exists(filename)) + { + fs = new FileStream(filename, FileMode.Open); + retval = (RedirectConfig)XmlSer.Deserialize(fs); + } + else + { + retval = new RedirectConfig(); + } + } + catch (Exception ex) + { + Exceptions.LogException(ex); + } + finally + { + if (fs != null) + { + fs.Close(); + fs.Dispose(); + } + } + return retval; + } + + private static XmlSerializer XmlSer => new XmlSerializer(typeof(RedirectConfig)); + + public void ToFile(string filename) + { + var filePath = filename.Substring(0, filename.LastIndexOf("\\")); + if (!Directory.Exists(filePath)) + Directory.CreateDirectory(filePath); + + FileStream fs = null; + + try + { + if (File.Exists(filename)) + { + File.Copy(filename, filename.Replace(".xml", string.Format(".{0}.xml", DateTime.Now.ToString("yyyyMMdd-HHmmss")))); + File.Delete(filename); + } + + fs = new FileStream(filename, FileMode.CreateNew); + XmlSer.Serialize(fs, this); + + //ClearCache(); + } + catch (Exception ex) + { + Exceptions.LogException(ex); + } + finally + { + fs.Close(); + fs.Dispose(); + } + } + + public static void CreateFile(string filename) + { + var emptyConfig = new RedirectConfig(); + emptyConfig.ToFile(filename); + } + + [XmlArrayItem("Mapping")] + public List Mappings { get; set; } + } +} \ No newline at end of file diff --git a/Components/RedirectController.cs b/Components/RedirectController.cs new file mode 100644 index 0000000..3552e17 --- /dev/null +++ b/Components/RedirectController.cs @@ -0,0 +1,351 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Web; +using System.Web.UI; +using DotNetNuke; +using DotNetNuke.Common; +using DotNetNuke.Common.Utilities; +using DotNetNuke.Entities.Portals; +using DotNetNuke.Entities.Users; +using DotNetNuke.Services.Url.FriendlyUrl; +using FortyFingers.SeoRedirect.Components.Data; + +namespace FortyFingers.SeoRedirect.Components +{ + public static class RedirectController + { + + public static UserInfo UserInfo + { + get + { + return UserController.GetCurrentUserInfo(); + } + } + + private static HttpResponse Response + { + get { return HttpContext.Current.Response; } + } + + private static HttpRequest Request + { + get { return HttpContext.Current.Request; } + } + + public static void DoRedirect(ControlCollection logToControls, bool redirectWhenNo404Detected = false, bool onlyLogWhen404 = false) + { + // find incoming URL + string incoming = ""; + + try + { + // nothing to do if it's already a redirect because of an error + //if (!String.IsNullOrEmpty(Request.QueryString["aspxerrorpath"])) + // return; + + bool is404 = false; + // we're matching lowercased: case insensitive + incoming = IncomingUrl(logToControls, ref is404).ToLower(); + // enable logging when DNN detects a 404 later on (e.g. for extentionless urls) + HttpContext.Current.Items["40F_SEO_IncomingUrl"] = incoming; + // register whether or not a 404 was detected + RedirectController.SetRequest404Detected(is404); + + // since generating a 404 inside a 404 page + // (which is what we do in case of .aspx errors) + // will cause an additional redirect to the error page, + // we here check if the incoming url contains an aspx error path + // if so: stop processing, we're already done. + if (HasAspxError(incoming)) + { + SetStatus404(); + return; + } + + if (UserInfo.IsSuperUser) logToControls.Add(new LiteralControl(String.Format("Incoming: {0}
", incoming))); + + // check if URL is in Sources of mappingtable + string target = ""; + + var mappingsNoRegex = RedirectConfig.Instance.MappingsDictionary(false); + + if (UserInfo.IsSuperUser) logToControls.Add(new LiteralControl(String.Format("Mappings (with regex): {0}
", RedirectConfig.Instance.MappingsDictionary(true).Count))); + if (UserInfo.IsSuperUser) logToControls.Add(new LiteralControl(String.Format("Mappings (no regex): {0}
", mappingsNoRegex.Count))); + + // if we're in a 404, let's try to find a mapping + if (is404 || redirectWhenNo404Detected) + { + Common.Logger.Debug($"Try to find a mapping for [{incoming}]"); + // first try non-regex mappings, as they're supposed to be faster + if (mappingsNoRegex.ContainsKey(incoming)) + { + target = mappingsNoRegex[incoming]; + Common.Logger.Debug($"Mapping without regex found, Target: [{target}]"); + } + else if (mappingsNoRegex.ContainsKey(ToRelativeUrl(incoming))) + { + target = mappingsNoRegex[ToRelativeUrl(incoming)]; + Common.Logger.Debug($"Mapping without regex found, Target: [{target}]"); + } + else + { + // no match found without regexes, try the ones with regex + var mappingsUsingRegex = RedirectConfig.Instance.MappingsDictionary(true); + + // now try and match each one + foreach (var mapping in mappingsUsingRegex) + { + var mappingSource = ToFullUrl(mapping.Key); + var mappingTarget = ToFullUrl(mapping.Value); + + if (Regex.IsMatch(incoming, mappingSource)) + { + // got a match! + target = Regex.Replace(incoming, mappingSource, mappingTarget); + Common.Logger.Debug($"Mapping with regex found, Target: [{target}]"); + } + } + } + } + + // Log this 404 + var ps = Common.CurrentPortalSettings; + if (is404 || (redirectWhenNo404Detected && !string.IsNullOrEmpty(target))) + { + Common.Logger.Debug($"Logging redirect: is404:{is404}, redirectWhenNo404Detected:{redirectWhenNo404Detected}, target:[{target}]"); + AddRedirectLog(ps.PortalId, incoming, target); + } + else if (!onlyLogWhen404) + { + Common.Logger.Debug($"Logging redirect for !onlyLogWhen404 target:[{target}]"); + AddRedirectLog(ps.PortalId, incoming, target); + } + + if (UserInfo.IsSuperUser) logToControls.Add(new LiteralControl(String.Format("Target: {0}
", target))); + // if so: redirect + if (!String.IsNullOrEmpty(target)) + { + try + { + Common.Logger.Debug($"Redirect to:[{target}]"); + Response.Redirect(target, false); + Response.StatusCode = 301; + Response.End(); + } + catch (Exception) + { + // do nothing: threadabortexception is normal behaviour + } + } + + // we're only displaying the logging if it's a 404 + if (!is404) + { + logToControls.Clear(); + } + + //else if (is404) // only if it was a 404 incoming + //{ + // // tell the client that the page wasn't found + // SetStatus404(); + //} + } + catch (Exception exception) + { + Common.Logger.Error($"Exception in DoRedirect: {exception.Message}. Stacktrace: {exception.StackTrace}"); + // we're not writing in the eventlog, since the amount of log records can really explode + if (UserInfo.IsSuperUser) logToControls.Add(new LiteralControl(String.Format("Error: {0}
", exception.Message + "
" + exception.StackTrace))); + } + + } + + private static string ToFullUrl(string relativeUrl) + { + var retval = relativeUrl; + if (retval.StartsWith("/")) + { + retval = Globals.AddHTTP(Common.CurrentPortalSettings.PortalAlias.HTTPAlias + retval); + } + return retval; + } + private static string ToRelativeUrl(string fullUrl) + { + var retval = fullUrl; + + retval = retval.Replace("http://", "").Replace("https://", ""); + + retval = retval.Substring(retval.IndexOf("/")); + + + return retval; + } + + internal static void SetStatus404() + { + Common.Logger.Debug($"Setting ResponseStatus to 404"); + Response.Status = "404 Not Found"; + Response.StatusCode = 404; + } + + internal static void SetRequest404Detected(bool is404) + { + if (HttpContext.Current.Items["40F_SEO_404Detected"] != null) return; + HttpContext.Current.Items["40F_SEO_404Detected"] = is404; + } + internal static bool RequestHas404Detected() + { + if (HttpContext.Current.Items["40F_SEO_404Detected"] == null) return false; + return (bool)HttpContext.Current.Items["40F_SEO_404Detected"]; + } + + internal static void AddRedirectLog(int portalId, string incoming, string target) + { + if (HttpContext.Current.Items["40F_SEO_AlreadyLogged"] != null) return; + + DataProvider.Instance() + .AddRedirectLog(portalId, incoming, DateTime.UtcNow, + Request.UrlReferrer == null ? "" : Request.UrlReferrer.ToString(), + Request.ServerVariables.AllKeys.Contains("HTTP_USER_AGENT") ? Request.ServerVariables["HTTP_USER_AGENT"] : "", + target); + // clear the context item so it isn't logged twice + HttpContext.Current.Items["40F_SEO_IncomingUrl"] = ""; + HttpContext.Current.Items["40F_SEO_AlreadyLogged"] = true; + + } + //private static void AddRedirectLog(IList values) + //{ + // DataProvider.Instance() + // .AddRedirectLog(Common.CurrentPortalSettings.PortalId, values[0], DateTime.UtcNow, Request.UrlReferrer.ToString(), + // Request.ServerVariables["HTTP_USER_AGENT"], values[1]); + // // clear the context item so it isn't logged twice + // HttpContext.Current.Items["40F_SEO_IncomingUrl"] = ""; + // HttpContext.Current.Items["40F_SEO_AlreadyLogged"] = true; + //} + //internal static void AddRedirectLogAsync(string incoming, string target) + //{ + // var values = new List() + // {incoming, target}; + + // Action> action = AddRedirectLog; + // action.BeginInvoke(values, null, null); + //} + + public static List GetTopUnhandledUrls(int portalId, int maxDays, int maxUrls) + { + var startdate = DateTime.Today.AddDays(-1 * maxDays); + var dr = DataProvider.Instance().GetTopUnhandledUrls(portalId, startdate, maxUrls); + + var retval = CBO.FillCollection(dr); + + return retval; + } + + public static void SetHandledUrl(string url) + { + DataProvider.Instance().SetHandledUrl(url, DateTime.Now, UserController.GetCurrentUserInfo().Username); + } + + public static string IncomingUrl(ControlCollection logToControls, ref bool is404) + { + string incoming = ""; + + //Request.RawUrl "/404.aspx?404;http://dev_seo.local:80/banaan.asp?id=123123" string + //Request.RawUrl "/404.aspx?404;http://dev_seo.local:80/banaan.asp" string + //Request.RawUrl "/404.aspx?404;http://dev_seo.local:80/banaan" string + //Request.RawUrl "/404.aspx?404;http://dev_seo.local:80/banaan.jsf?asdasd" string + //Request.RawUrl "/404.aspx?aspxerrorpath=/asdasd/dfgdfg/werwer/bla.aspx" string + //Request.RawUrl "/404.aspx?aspxerrorpath=/banaan.aspx" string + + if (UserInfo.IsSuperUser) logToControls.Add(new LiteralControl(String.Format("RawUrl: {0}
", Request.RawUrl))); + if (UserInfo.IsSuperUser) logToControls.Add(new LiteralControl(String.Format("AbsoluteUri: {0}
", Request.Url.AbsoluteUri))); + + Regex MyRegex = new Regex("^.*(?i:404;)(.*)", + RegexOptions.CultureInvariant | RegexOptions.Compiled); + if (MyRegex.IsMatch(Request.RawUrl)) + { + Match m = MyRegex.Match(Request.RawUrl); + incoming = m.Groups[1].Captures[0].Value; + + // remove port + incoming = Regex.Replace(incoming, "(:\\d+)", ""); + Common.Logger.Debug($"Incoming found with \"404;\" method: {incoming}"); + } + + // if incoming is not found try the AbsoluteUri + if (String.IsNullOrEmpty(incoming)) + { + var absoluteUri = HttpUtility.UrlDecode(Request.Url.AbsoluteUri); + if (MyRegex.IsMatch(absoluteUri)) + { + //Match m = MyRegex.Match(absoluteUri); + //incoming = m.Groups[1].Captures[0].Value; + + // remove port + // incoming = Regex.Replace(incoming, "(:\\d+)", ""); + + incoming = String.Format("{0}://{1}{2}", + Request.Url.Scheme, + Request.Url.Host, + Request.RawUrl); + + Common.Logger.Debug($"Incoming found with AbsoluteUri method: {incoming}"); + } + } + + // if incoming is not found try the other option + if (String.IsNullOrEmpty(incoming)) + { + if (HasAspxError(Request.RawUrl)) + { + Match m = AspxErrorRegex.Match(Request.RawUrl); + incoming = m.Groups[1].Captures[0].Value; + Common.Logger.Debug($"Incoming found with HasAspxError method: {incoming}"); + } + + // in this case, the path is reletative to the website root, so we need to + // put hostname in front of it + if (!String.IsNullOrEmpty(incoming)) + { + incoming = String.Format("{0}://{1}{2}{3}", + Request.Url.Scheme, + Request.Url.Host, + incoming.StartsWith("/") ? "" : "/", + incoming); + + Common.Logger.Debug($"Incoming changed to: {incoming}"); + } + } + + // if still no incoming found, then we're not here by 404 + // let's just get the RawUrl in this case + // 2015-05-12: this used to take the AbsoluteUri + if (String.IsNullOrEmpty(incoming)) + { + is404 = false; + incoming = String.Format("{0}://{1}{2}", + Request.Url.Scheme, + Request.Url.Host, + Request.RawUrl); + Common.Logger.Debug($"Incoming for not-404 set to: {incoming}"); + } + else + { + is404 = true; + } + return incoming; + + } + + private static Regex AspxErrorRegex = new Regex("^.*(?i:aspxerrorpath=)(.*)", + RegexOptions.CultureInvariant | RegexOptions.Compiled); + + private static bool HasAspxError(string url) + { + return AspxErrorRegex.IsMatch(url); + } + } +} \ No newline at end of file diff --git a/Components/SeoRedirectModule.cs b/Components/SeoRedirectModule.cs new file mode 100644 index 0000000..c011711 --- /dev/null +++ b/Components/SeoRedirectModule.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using System.Web; +using System.Web.UI; +using DotNetNuke.Common; +using DotNetNuke.Entities.Portals; +using DotNetNuke.Entities.Users; + +namespace FortyFingers.SeoRedirect.Components +{ + public class SeoRedirectModule : IHttpModule + { + public void Init(HttpApplication context) + { + context.BeginRequest += OnBeginRequest; + context.EndRequest += Context_EndRequest; + } + + private void Context_EndRequest(object sender, EventArgs e) + { + try + { + var rsp = HttpContext.Current.Response; + var ps = Common.CurrentPortalSettings; + string incoming = (string) HttpContext.Current.Items["40F_SEO_IncomingUrl"]; + if (rsp.StatusCode == (int) HttpStatusCode.NotFound && !string.IsNullOrEmpty(incoming)) + { + Common.Logger.Debug($"Logging redirect from Context_EndRequest. incoming:[{incoming}]"); + RedirectController.AddRedirectLog(ps.PortalId, incoming, ""); + } + } + catch (Exception exception) + { + // we're not writing in the eventlog, since the amount of log records can really explode + // we MUST catch every possible exception here, otherwise the website would be completely down in case of a bug + } + } + + private void OnBeginRequest(object sender, EventArgs e) + { + + // find incoming URL + string incoming = ""; + + try + { + // in case of een upgrade, PortalSettings will be null, nothing to do in such a case: + // if (PortalSettings.Current == null) return; + + var fake = new ControlCollection(new LiteralControl("")); + Common.Logger.Debug($"Calling DoRedirect From HttpModule"); + RedirectController.DoRedirect(fake, redirectWhenNo404Detected: true, onlyLogWhen404: true); + } + catch (Exception exception) + { + // we're not writing in the eventlog, since the amount of log records can really explode + // we MUST catch every possible exception here, otherwise the website would be completely down in case of a bug + } + + + } + + public void Dispose() + { + + } + } +} \ No newline at end of file diff --git a/Edit.ascx b/Edit.ascx new file mode 100644 index 0000000..5e065e4 --- /dev/null +++ b/Edit.ascx @@ -0,0 +1,104 @@ +<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="Edit.ascx.cs" Inherits="FortyFingers.SeoRedirect.Edit" %> +<%@ Import Namespace="FortyFingers.SeoRedirect.Components" %> +<%@ Register TagPrefix="dnn" TagName="Label" Src="~/controls/LabelControl.ascx" %> +<%@ Register TagPrefix="dnn" Assembly="DotNetNuke.Web" Namespace="DotNetNuke.Web.UI.WebControls" %> + + + + +
+
+
+ + + + + + + + + + + + + + + + + + +
<%= Localization.GetString("SourceUrlHeaderLabel", LocalResourceFile) %><%= Localization.GetString("TargetUrlHeaderLabel", LocalResourceFile) %>
+ + + + +
+
+ +
diff --git a/Edit.ascx.cs b/Edit.ascx.cs new file mode 100644 index 0000000..9516815 --- /dev/null +++ b/Edit.ascx.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.UI; +using System.Web.UI.WebControls; +using DotNetNuke; +using DotNetNuke.Entities.Modules; +using DotNetNuke.Entities.Portals; +using DotNetNuke.Entities.Tabs; +using DotNetNuke.Framework; +using DotNetNuke.UI.Utilities; +using DotNetNuke.Web.Client; +using DotNetNuke.Web.Client.ClientResourceManagement; +using DotNetNuke.Web.UI.WebControls; +using FortyFingers.SeoRedirect.Components; +using Globals = DotNetNuke.Common.Globals; + +namespace FortyFingers.SeoRedirect +{ + public partial class Edit : PortalModuleBase + { + protected void Page_Load(object sender, EventArgs e) + { + RegisterResources(); + + //if (!IsPostBack) + //{ + // // we don't want anything cached here + // var config = RedirectConfig.Instance; + // FillForm(config); + //} + } + + private void RegisterResources() + { + jQuery.RequestRegistration(); + + ClientResourceManager.RegisterScript(Page, "resources/shared/scripts/knockout.js", FileOrder.Js.jQuery); + ClientResourceManager.RegisterScript(Page, "resources/shared/scripts/knockout.mapping.js", FileOrder.Js.jQuery + 1); + ClientAPI.RegisterClientReference(Page, ClientAPI.ClientNamespaceReferences.dnn); + ServicesFramework.Instance.RequestAjaxAntiForgerySupport(); + ServicesFramework.Instance.RequestAjaxScriptSupport(); + ClientResourceManager.RegisterScript(Page, "desktopmodules/40fingers/seoredirect/js/40F-Common.js", FileOrder.Js.jQuery); + ClientResourceManager.RegisterScript(Page, "desktopmodules/40fingers/seoredirect/js/SeoRedirect.js", FileOrder.Js.jQuery); + + //Page.ClientScript.RegisterClientScriptInclude("jQuery.DataTables.1.8.2", ResolveUrl("js/DataTables-1.8.2/media/js/jquery.dataTables.min.js")); + //Common.AddCssLink(ResolveUrl("js/DataTables-1.8.2/media/demo_table.css"), Page); + //Page.ClientScript.RegisterStartupScript(this.GetType(), "40Fingers.SeoRedirect.DataTablesStartup", "jQuery(document).ready(function() { jQuery('#SeoRedirectMappingsTable').dataTable(); } );", true); + } + + //protected void MappingsRepeater_ItemDatabound(object sender, RepeaterItemEventArgs e) + //{ + // switch (e.Item.ItemType) + // { + // case ListItemType.Header: + // BindMappingsRepeaterHeader(e); + // break; + // case ListItemType.Footer: + // BindMappingsRepeaterFooter(e); + // break; + // case ListItemType.Item: + // case ListItemType.AlternatingItem: + // BindMappingsRepeaterItem(e); + // break; + // } + //} + + //private void BindMappingsRepeaterFooter(RepeaterItemEventArgs e) + //{ + //} + + //private void BindMappingsRepeaterHeader(RepeaterItemEventArgs e) + //{ + // var btn = (ImageButton) e.Item.FindControl("AddButton"); + // btn.ImageUrl = IconTypes.Add.GetUrl(); + //} + + //private List _PortalTabs = null; + //private List PortalTabs + //{ + // get + // { + // if(_PortalTabs == null) + // { + // _PortalTabs = TabController.GetPortalTabs(PortalId, -1, false, true, true, true); + // } + + // return _PortalTabs; + // } + //} + + //private void BindMappingsRepeaterItem(RepeaterItemEventArgs e) + //{ + // var btn = (ImageButton)e.Item.FindControl("DeleteButton"); + // btn.CommandName = "DeleteRow"; + // btn.ImageUrl = IconTypes.Delete.GetUrl(); + + // var map = (Mapping) e.Item.DataItem; + + // var txtSource = (TextBox) e.Item.FindControl("SourceUrlTextBox"); + // var txtTarget = (TextBox) e.Item.FindControl("TargetUrlTextBox"); + // var cboTarget = (DnnPageDropDownList) e.Item.FindControl("TargetTabId"); + // cboTarget.IncludeDisabledTabs = false; + // cboTarget.OnClientSelectionChanged.Add("ff_seo_selectedPageChanged"); + + // var radUrl = (RadioButton) e.Item.FindControl("UseUrlRadio"); + // var radTab = (RadioButton) e.Item.FindControl("UseTabRadio"); + + // radUrl.GroupName = txtSource.ClientID; + // radTab.GroupName = txtSource.ClientID; + + // if (map.TargetTabId > 0) + // { + // txtTarget.Text = ""; + + // radUrl.Checked = false; + // radTab.Checked = true; + // cboTarget.SelectedPage = PortalTabs.FirstOrDefault(t => t.TabID == map.TargetTabId); + // cboTarget.PortalId = PortalId; + // } + // else + // { + // radUrl.Checked = true; + // radTab.Checked = false; + // txtTarget.Text = map.TargetUrl; + // cboTarget.SelectedPage = null; + // } + //} + + //protected void MappingsRepeater_ItemCommand(object source, RepeaterCommandEventArgs e) + //{ + // switch (e.CommandName) + // { + // case "StartEdit": + // break; + // case "CancelEdit": + // break; + // case "EndEdit": + // break; + // case "NewRow": + // break; + // case "DeleteRow": + // // read from controls + // var config = ReadConfigData(); + // // remove the one + // config.Mappings.RemoveAt(e.Item.ItemIndex); + // // rebind + // FillForm(config); + // break; + // } + //} + + //protected void SaveMappingsButton_Click(object sender, EventArgs e) + //{ + // // read from controls + // var config = ReadConfigData(); + // // serialize to file + // config.ToFile(Common.RedirectConfigFile()); + // RedirectConfig.Reload(PortalId); + // FillForm(RedirectConfig.Instance); + //} + + //protected void AddButton_Click(object sender, ImageClickEventArgs e) + //{ + // // read from controls + // var config = ReadConfigData(); + // // add 1 object + // config.Mappings.Insert(0, new Mapping()); + // // rebind to repeater + // FillForm(config); + //} + + //private RedirectConfig ReadConfigData() + //{ + // var retval = new RedirectConfig(); + + // foreach (RepeaterItem repeaterItem in MappingsRepeater.Items) + // { + // var source = ((TextBox)repeaterItem.FindControl("SourceUrlTextBox")).Text; + // var txtTarget = ((TextBox)repeaterItem.FindControl("TargetUrlTextBox")).Text; + // var tabTarget = ((DnnPageDropDownList)repeaterItem.FindControl("TargetTabId")).SelectedPage; + // var useRegex = ((CheckBox) repeaterItem.FindControl("UseRegexCheckBox")).Checked; + + // var radUrl = (RadioButton)repeaterItem.FindControl("UseUrlRadio"); + // var radTab = (RadioButton)repeaterItem.FindControl("UseTabRadio"); + + // string targetUrl = ""; + // int targetTabId = 0; + // if (radTab.Checked) + // { + // targetTabId = tabTarget.TabID; + // targetUrl = Globals.NavigateURL(targetTabId, PortalSettings.Current, ""); + // } + // else if(radUrl.Checked) + // { + // targetUrl = txtTarget; + // targetTabId = -1; + // } + // retval.Mappings.Add(new Mapping() + // { + // SourceUrl = source, + // TargetUrl = targetUrl, + // TargetTabId = targetTabId, + // UseRegex = useRegex + // }); + // } + + // return retval; + //} + + //private void FillForm(RedirectConfig redirectConfig) + //{ + // if(redirectConfig.Mappings.Count == 0) + // redirectConfig.Mappings.Add(new Mapping()); + + // MappingsRepeater.DataSource = redirectConfig.Mappings; + // MappingsRepeater.DataBind(); + //} + + } +} \ No newline at end of file diff --git a/Edit.ascx.designer.cs b/Edit.ascx.designer.cs new file mode 100644 index 0000000..3e50cef --- /dev/null +++ b/Edit.ascx.designer.cs @@ -0,0 +1,15 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace FortyFingers.SeoRedirect { + + + public partial class Edit { + } +} diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..2873931 --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,33 @@ +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("40FINGERS.SimpleGalleryShow")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("40FINGERS")] +[assembly: AssemblyProduct("SimpleGalleryShow")] +[assembly: AssemblyCopyright("(c) 40FINGERS 2010")] +[assembly: AssemblyTrademark("")] +[assembly: GuidAttribute("b06aa409-090b-4767-9426-6bb946d1ebf6")] + +// 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)] + + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("02.02.03.00")] +[assembly: AssemblyFileVersion("02.02.03.00")] diff --git a/README.md b/README.md index 1762035..2633846 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # DNN-SEORedirect -This is a 40FINGERS module will allow you to monintor and manage the 404's your DNN site is receiving. +This is a 40FINGERS module will allow you to monitor and manage the 404's your DNN site is receiving. + +Go to [our](https://www.40fingers.net/) [project page](https://www.40fingers.net/Products/DNN-SeoRedirect) for the documentation on this module. \ No newline at end of file diff --git a/Settings.ascx b/Settings.ascx new file mode 100644 index 0000000..720574f --- /dev/null +++ b/Settings.ascx @@ -0,0 +1,12 @@ +<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="Settings.ascx.cs" Inherits="FortyFingers.SeoRedirect.Settings" %> +<%@ Register TagPrefix="dnn" TagName="Label" Src="~/controls/LabelControl.ascx" %> + + + + + +
+ + +
+ diff --git a/Settings.ascx.cs b/Settings.ascx.cs new file mode 100644 index 0000000..6e3d28d --- /dev/null +++ b/Settings.ascx.cs @@ -0,0 +1,44 @@ +using System; +using DotNetNuke; +using DotNetNuke.Entities.Modules; +using cfg = FortyFingers.SeoRedirect.Components.Config; + +namespace FortyFingers.SeoRedirect +{ + public partial class Settings : ModuleSettingsBase + { + + private cfg _config; + private cfg Config + { + get + { + if (_config == null) + _config = new cfg(Settings, ModuleId, TabModuleId); + + return _config; + } + } + + protected void Page_Load(object sender, EventArgs e) + { + + } + /// + /// loads the module settings + /// + public override void LoadSettings() + { + NoOfEntries.Text = Config.NoOfEntries.ToString(); + } + /// + /// updates module settings + /// + public override void UpdateSettings() + { + Config.NoOfEntries = int.Parse(NoOfEntries.Text); + } + + + } +} \ No newline at end of file diff --git a/Settings.ascx.designer.cs b/Settings.ascx.designer.cs new file mode 100644 index 0000000..4c825f8 --- /dev/null +++ b/Settings.ascx.designer.cs @@ -0,0 +1,42 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace FortyFingers.SeoRedirect { + + + public partial class Settings { + + /// + /// plNoOfEntries control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.UserControl plNoOfEntries; + + /// + /// NoOfEntries control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.TextBox NoOfEntries; + + /// + /// ValNoE control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.RangeValidator ValNoE; + } +} diff --git a/Sql/01.02.01.SqlDataProvider b/Sql/01.02.01.SqlDataProvider new file mode 100644 index 0000000..e84ad7e --- /dev/null +++ b/Sql/01.02.01.SqlDataProvider @@ -0,0 +1,38 @@ +CREATE TABLE {databaseOwner}[{objectQualifier}40F_SEO_RedirectLog]( + [Id] [int] IDENTITY(1,1) NOT NULL, + [RequestedUrl] [nvarchar](max) NOT NULL, + [RequestDateTime] [datetime] NOT NULL, + [Referrer] [nvarchar](max) NULL, + [HTTP_USER_AGENT] [nvarchar](max) NULL, + [RedirectedToUrl] [nvarchar](max) NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] + +GO + +CREATE PROCEDURE {databaseOwner}[{objectQualifier}40F_SEO_AddRedirectLog] +@RequestedUrl nvarchar(max), +@RequestDateTime DATETIME, +@Referrer nvarchar(max), +@HTTP_USER_AGENT nvarchar(max), +@RedirectedToUrl nvarchar(max) +AS +BEGIN + +INSERT INTO {databaseOwner}[{objectQualifier}40F_SEO_RedirectLog] + ([RequestedUrl] + ,[RequestDateTime] + ,[Referrer] + ,[HTTP_USER_AGENT] + ,[RedirectedToUrl]) + VALUES + (@RequestedUrl + ,@RequestDateTime + ,@Referrer + ,@HTTP_USER_AGENT + ,@RedirectedToUrl) + +END + +GO + + diff --git a/Sql/02.00.00.SqlDataProvider b/Sql/02.00.00.SqlDataProvider new file mode 100644 index 0000000..70d86a3 --- /dev/null +++ b/Sql/02.00.00.SqlDataProvider @@ -0,0 +1,46 @@ +ALTER TABLE {databaseOwner}[{objectQualifier}40F_SEO_RedirectLog] ADD HandledOn DATETIME NULL +ALTER TABLE {databaseOwner}[{objectQualifier}40F_SEO_RedirectLog] ADD HandledBy nvarchar(100) NULL +GO + +CREATE NONCLUSTERED INDEX [{objectQualifier}IX_40F_SEO_RedirectLog_Url_Unhandled] ON {databaseOwner}[{objectQualifier}40F_SEO_RedirectLog] +( + [RequestDateTime] ASC +) +INCLUDE ([RequestedUrl],[RedirectedToUrl],[HandledOn]) +GO + +CREATE PROCEDURE {databaseOwner}[{objectQualifier}40F_SEO_GetTopUnhandledUrls] +@StartDate DATETIME, +@MaxUrls INT +AS +BEGIN + SELECT + TOP (@MaxUrls) + L.RequestedUrl AS Url, COUNT(L.Id) as Occurrences + FROM + {databaseOwner}[{objectQualifier}40F_SEO_RedirectLog] L + WHERE + L.RequestDateTime > @StartDate + AND + L.RedirectedToUrl = '' + AND + L.HandledOn IS NULL + GROUP BY + L.RequestedUrl + ORDER BY COUNT(L.Id) DESC +END +GO + +CREATE PROCEDURE {databaseOwner}[{objectQualifier}40F_SEO_SetHandledUrl] +@url nvarchar(100), +@handledOn DATETIME, +@handledBy NVARCHAR(100) +AS +BEGIN + UPDATE {databaseOwner}[{objectQualifier}40F_SEO_RedirectLog] SET + HandledOn = @handledOn, + HandledBy = @handledBy + WHERE + RequestedUrl = @url +END +GO \ No newline at end of file diff --git a/Sql/02.01.02.SqlDataProvider b/Sql/02.01.02.SqlDataProvider new file mode 100644 index 0000000..afc1a19 --- /dev/null +++ b/Sql/02.01.02.SqlDataProvider @@ -0,0 +1,59 @@ +ALTER TABLE {databaseOwner}[{objectQualifier}40F_SEO_RedirectLog] +ADD PortalId INT NULL +CONSTRAINT [{objectQualifier}DF_40F_SEO_RedirectLog] DEFAULT 0 +WITH VALUES +GO + +ALTER PROCEDURE {databaseOwner}[{objectQualifier}40F_SEO_AddRedirectLog] +@PortalId INT, +@RequestedUrl nvarchar(max), +@RequestDateTime DATETIME, +@Referrer nvarchar(max), +@HTTP_USER_AGENT nvarchar(max), +@RedirectedToUrl nvarchar(max) +AS +BEGIN + +INSERT INTO {databaseOwner}[{objectQualifier}40F_SEO_RedirectLog] + ([PortalId] + ,[RequestedUrl] + ,[RequestDateTime] + ,[Referrer] + ,[HTTP_USER_AGENT] + ,[RedirectedToUrl]) + VALUES + (@PortalId + ,@RequestedUrl + ,@RequestDateTime + ,@Referrer + ,@HTTP_USER_AGENT + ,@RedirectedToUrl) + +END + +GO + +ALTER PROCEDURE {databaseOwner}[{objectQualifier}40F_SEO_GetTopUnhandledUrls] +@PortalId INT, +@StartDate DATETIME, +@MaxUrls INT +AS +BEGIN + SELECT + TOP (@MaxUrls) + L.RequestedUrl AS Url, COUNT(L.Id) as Occurrences + FROM + {databaseOwner}[{objectQualifier}40F_SEO_RedirectLog] L + WHERE + L.PortalId = @PortalId + AND + L.RequestDateTime > @StartDate + AND + L.RedirectedToUrl = '' + AND + L.HandledOn IS NULL + GROUP BY + L.RequestedUrl + ORDER BY COUNT(L.Id) DESC +END +GO \ No newline at end of file diff --git a/View.ascx b/View.ascx new file mode 100644 index 0000000..43eb5b7 --- /dev/null +++ b/View.ascx @@ -0,0 +1,90 @@ +<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="View.ascx.cs" Inherits="FortyFingers.SeoRedirect.View" %> +<%@ Import Namespace="FortyFingers.SeoRedirect.Components" %> +<%@ Register TagPrefix="dnn" Assembly="DotNetNuke.Web" Namespace="DotNetNuke.Web.UI.WebControls" %> + + + + + +
+

+

+
+
+ + + + + + + + + + + + + + + + + + +
<%= Localization.GetString("Url.Header", LocalResourceFile) %><%= Localization.GetString("Occurrences.Header", LocalResourceFile) %><%= Localization.GetString("Actions.Header", LocalResourceFile) %>
+ + +
+
+
+
+ diff --git a/View.ascx.cs b/View.ascx.cs new file mode 100644 index 0000000..a8b8519 --- /dev/null +++ b/View.ascx.cs @@ -0,0 +1,136 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading; +using System.Web; +using System.Web.UI; +using System.Web.UI.HtmlControls; +using System.Web.UI.WebControls; +using DotNetNuke.Common.Utilities; +using DotNetNuke.Entities.Host; +using DotNetNuke.Entities.Modules; +using DotNetNuke.Entities.Modules.Actions; +using DotNetNuke.Entities.Portals; +using DotNetNuke.Entities.Tabs; +using DotNetNuke.Framework; +using DotNetNuke.Security.Permissions; +using DotNetNuke.Services.Localization; +using DotNetNuke.Services.Tokens; +using DotNetNuke.UI.Utilities; +using DotNetNuke.Web.Client; +using DotNetNuke.Web.Client.ClientResourceManagement; +using DotNetNuke.Web.UI.WebControls; +using FortyFingers.SeoRedirect.Components; +using FortyFingers.SeoRedirect.Components.Data; +using Globals = DotNetNuke.Common.Globals; +using cfg = FortyFingers.SeoRedirect.Components.Config; + +namespace FortyFingers.SeoRedirect +{ + public partial class View : PortalModuleBase, IActionable + { + private cfg _config; + protected cfg Config + { + get + { + if (_config == null) + _config = new cfg(Settings, ModuleId, TabModuleId); + + return _config; + } + } + protected override void OnInit(EventArgs e) + { + base.OnInit(e); + Page.PreRender += Page_PreRender; + // as of dnn 7.1, when using Advanced FriendlyURLs, there needs to be a setting in Portalsettings for 404 pages + // here we're checking that + + var ver = DotNetNuke.Application.DotNetNukeContext.Current.Application.Version; + + if ((UserInfo.IsSuperUser || UserInfo.IsInRole(PortalSettings.AdministratorRoleName)) && (ver.Major == 7 && ver.Minor < 3)) + { + if(PortalController.GetPortalSettingAsInteger("AUM_ErrorPage404", PortalId, Null.NullInteger) <= 0) + Controls.Add( + new LiteralControl(String.Format("

{0}

", + Localization.GetString("Dnn404PortalSettingAbsent.Text", LocalResourceFile)))); + } + + // if it's not a 404, we only want to log when the requested URL is for a deeper level than the page itself is + //// TODO: This doesn't work as intended, so it's gone + //// so OnlyLogwhen404 should be true if the request is for the same or higher level + ////Request.RawUrl.Count(t => t == '/') <= PortalSettings.ActiveTab.Level + 1; + var onlyLogWhen404 = true; + + Common.Logger.Debug($"Calling DoRedirect From View.ascx OnInit"); + RedirectController.DoRedirect(LoggingPlaceholder.Controls, true, onlyLogWhen404); + + if (!IsPostBack) + { + if (TabPermissionController.CanAdminPage()) + { + UnhandledUrlsPanel.Visible = true; + } + } + } + + private void Page_PreRender(object sender, EventArgs e) + { + string incoming = (string)HttpContext.Current.Items["40F_SEO_IncomingUrl"]; + + // check if IIS/ASP.NET/DNN already found this to be a 404 + if (Response.Status == "404 Not Found") + { + if (Response.StatusCode == (int)HttpStatusCode.NotFound && !string.IsNullOrEmpty(incoming)) + { + Common.Logger.Debug($"Logging redirect from Context_EndRequest. incoming:[{incoming}]"); + RedirectController.AddRedirectLog(PortalId, incoming, ""); + } + } + + // DNN returns 200 for static files that are actually 404's + if (RedirectController.RequestHas404Detected() && Response.StatusCode == (int) HttpStatusCode.OK) + { + //RedirectController.AddRedirectLog(PortalId, incoming, ""); + RedirectController.SetStatus404(); + } + } + + protected void Page_Load(object sender, EventArgs e) + { + ClientResourceManager.RegisterScript(Page, "resources/shared/scripts/knockout.js", FileOrder.Js.jQuery); + ClientResourceManager.RegisterScript(Page, "resources/shared/scripts/knockout.mapping.js", FileOrder.Js.jQuery + 1); + ClientAPI.RegisterClientReference(Page, ClientAPI.ClientNamespaceReferences.dnn); + ServicesFramework.Instance.RequestAjaxAntiForgerySupport(); + ServicesFramework.Instance.RequestAjaxScriptSupport(); + ClientResourceManager.RegisterScript(Page, "desktopmodules/40fingers/seoredirect/js/40F-Common.js", FileOrder.Js.jQuery); + ClientResourceManager.RegisterScript(Page, "desktopmodules/40fingers/seoredirect/js/SeoRedirect.js", FileOrder.Js.jQuery); + + UnhandledUrlsPanelHeader.Text = String.Format(Localization.GetString("UnhandledUrlsPanelHeader.Text", LocalResourceFile), Config.NoOfEntries); + + } + + + public ModuleActionCollection ModuleActions + { + get + { + var actions = new ModuleActionCollection(); + actions.Add(GetNextActionID(), + Localization.GetString("EditModule.Action", LocalResourceFile), + ModuleActionType.EditContent, + "", + "", + EditUrl(), + false, DotNetNuke.Security.SecurityAccessLevel.Edit, + true, + false); + + return actions; + } + } + } +} \ No newline at end of file diff --git a/View.ascx.designer.cs b/View.ascx.designer.cs new file mode 100644 index 0000000..bcb708b --- /dev/null +++ b/View.ascx.designer.cs @@ -0,0 +1,42 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace FortyFingers.SeoRedirect { + + + public partial class View { + + /// + /// LoggingPlaceholder control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.PlaceHolder LoggingPlaceholder; + + /// + /// UnhandledUrlsPanel control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.Panel UnhandledUrlsPanel; + + /// + /// UnhandledUrlsPanelHeader control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.Label UnhandledUrlsPanelHeader; + } +} diff --git a/_external/DotNetNuke.Instrumentation.dll b/_external/DotNetNuke.Instrumentation.dll new file mode 100644 index 0000000..8d30e97 Binary files /dev/null and b/_external/DotNetNuke.Instrumentation.dll differ diff --git a/_external/DotNetNuke.Log4Net.dll b/_external/DotNetNuke.Log4Net.dll new file mode 100644 index 0000000..1e16391 Binary files /dev/null and b/_external/DotNetNuke.Log4Net.dll differ diff --git a/_external/DotNetNuke.Web.Client.dll b/_external/DotNetNuke.Web.Client.dll new file mode 100644 index 0000000..0e8ba11 Binary files /dev/null and b/_external/DotNetNuke.Web.Client.dll differ diff --git a/_external/DotNetNuke.Web.dll b/_external/DotNetNuke.Web.dll new file mode 100644 index 0000000..76cccae Binary files /dev/null and b/_external/DotNetNuke.Web.dll differ diff --git a/_external/DotNetNuke.WebUtility.dll b/_external/DotNetNuke.WebUtility.dll new file mode 100644 index 0000000..fe89947 Binary files /dev/null and b/_external/DotNetNuke.WebUtility.dll differ diff --git a/_external/DotNetNuke.dll b/_external/DotNetNuke.dll new file mode 100644 index 0000000..b2e6508 Binary files /dev/null and b/_external/DotNetNuke.dll differ diff --git a/_external/Microsoft.ApplicationBlocks.Data.dll b/_external/Microsoft.ApplicationBlocks.Data.dll new file mode 100644 index 0000000..8158be3 Binary files /dev/null and b/_external/Microsoft.ApplicationBlocks.Data.dll differ diff --git a/_external/Newtonsoft.Json.dll b/_external/Newtonsoft.Json.dll new file mode 100644 index 0000000..1c16c11 Binary files /dev/null and b/_external/Newtonsoft.Json.dll differ diff --git a/_external/System.Net.Http.Formatting.dll b/_external/System.Net.Http.Formatting.dll new file mode 100644 index 0000000..367d253 Binary files /dev/null and b/_external/System.Net.Http.Formatting.dll differ diff --git a/_external/System.Net.Http.dll b/_external/System.Net.Http.dll new file mode 100644 index 0000000..2ee8ff7 Binary files /dev/null and b/_external/System.Net.Http.dll differ diff --git a/_external/System.Web.Http.dll b/_external/System.Web.Http.dll new file mode 100644 index 0000000..206c331 Binary files /dev/null and b/_external/System.Web.Http.dll differ diff --git a/img/icons/add.png b/img/icons/add.png new file mode 100644 index 0000000..6332fef Binary files /dev/null and b/img/icons/add.png differ diff --git a/img/icons/cancel.png b/img/icons/cancel.png new file mode 100644 index 0000000..c149c2b Binary files /dev/null and b/img/icons/cancel.png differ diff --git a/img/icons/delete.png b/img/icons/delete.png new file mode 100644 index 0000000..08f2493 Binary files /dev/null and b/img/icons/delete.png differ diff --git a/img/icons/disk.png b/img/icons/disk.png new file mode 100644 index 0000000..99d532e Binary files /dev/null and b/img/icons/disk.png differ diff --git a/img/icons/pencil.png b/img/icons/pencil.png new file mode 100644 index 0000000..0bfecd5 Binary files /dev/null and b/img/icons/pencil.png differ diff --git a/js/40F-Common.js b/js/40F-Common.js new file mode 100644 index 0000000..f1c4674 --- /dev/null +++ b/js/40F-Common.js @@ -0,0 +1,558 @@ +var FF = FF || {}; + +// Always use this method for logging. That way we can always implemented more advanced techniques later on +FF.log = function (msg) { + console.log(msg); +} + +FF.parseJSON = function (json) { + var retval; + + if (typeof (json) == "string") { + retval = JSON.parse(json); + } else { + // it's probably already an object + retval = json; + } + return retval; +} + +FF.isNullOrEmptyString = function (s) { + return s === "" || s == null; +} + +FF.msgHide = function (selector) { + $(selector).html(""); +} +FF.msgError = function (selector, msg) { + $(selector).html("
" + msg + "
"); +} +FF.msgOk = function (selector, msg) { + $(selector).html("
" + msg + "
"); +} +FF.msgWarning = function (selector, msg) { + $(selector).html("
" + msg + "
"); +} + +//FF.setUpdatePending = function (selector) { +// FF.setUpdatePending($(selector)); +//} +FF.setUpdatePending = function (selector) { + $(selector).addClass("has-warning"); + $(selector).removeClass("has-success"); + $(selector).removeClass("has-error"); +} + +FF.setUpdateSuccess = function (jqElm) { + $(jqElm).removeClass("has-warning"); + $(jqElm).removeClass("has-error"); + $(jqElm).addClass("has-success"); + setTimeout(function () { $(jqElm).removeClass("has-success"); }, 3000); +} +FF.setUpdateFailed = function (selector) { + $(selector).removeClass("has-warning"); + $(selector).removeClass("has-success"); + $(selector).addClass("has-error"); + setTimeout(function () { $(selector).removeClass("has-error"); }, 3000); +} + + +FF.constants = { + animationSpeed: 500 +} + +FF.confirm = function (message) { + // currently the easy javascreipt way, but probably a nicer method needed in the future + return confirm(message); +} + +FF.isVisible = function(selector) { + return !$(selector).hasClass("hidden"); +} +FF.hide = function (selector, callback) { + $(selector).addClass("hidden"); + // maybe we need to call a function + if (typeof (callback) == "function") { + callback(); + } +} +FF.show = function (selector, callback) { + // remove the hidden class + $(selector).removeClass("hidden"); + // maybe we need to call a function + if (typeof (callback) == "function") { + callback(); + } +} + +FF.hideNshow = function (hideSelector, showSelector, callback) { + FF.hide(hideSelector, function () { FF.show(showSelector, callback); }); +} + +//https://stackoverflow.com/questions/9234830/how-to-hide-a-option-in-a-select-menu-with-css +jQuery.fn.toggleOption = function (show) { + $(this).each(function () { + if (show) { + FF.show(this); + if ($(this).parent("span.toggleOption").length) + $(this).unwrap(); + } else { + FF.hide(this); + if ($(this).parent("span.toggleOption").length === 0) + $(this).wrap('