diff --git a/src/Orchard.Web/Modules/Orchard.Alias/AdminMenu.cs b/src/Orchard.Web/Modules/Orchard.Alias/AdminMenu.cs new file mode 100644 index 000000000..5d524b196 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/AdminMenu.cs @@ -0,0 +1,21 @@ +using Orchard.Environment.Extensions; +using Orchard.Localization; +using Orchard.Security; +using Orchard.UI.Navigation; + +namespace Orchard.Alias +{ + [OrchardFeature("Orchard.Alias.UI")] + public class AdminMenu : INavigationProvider + { + public Localizer T { get; set; } + + public string MenuName { get { return "admin"; } } + + public void GetNavigation(NavigationBuilder builder) + { + builder + .Add(T("Aliases"), "4", item => item.Action("Index", "Admin", new { area = "Orchard.Alias" }).Permission(StandardPermissions.SiteOwner)); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Controllers/AdminController.cs b/src/Orchard.Web/Modules/Orchard.Alias/Controllers/AdminController.cs new file mode 100644 index 000000000..168f37e32 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Controllers/AdminController.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Mvc; +using System.Web.Routing; +using Orchard.Alias.Implementation.Holder; +using Orchard.Alias.ViewModels; +using Orchard.Core.Contents.Controllers; +using Orchard.Environment.Extensions; +using Orchard.Localization; +using Orchard.Security; +using Orchard.UI.Navigation; +using Orchard.UI.Notify; + +namespace Orchard.Alias.Controllers { + [OrchardFeature("Orchard.Alias.UI")] + [ValidateInput(false)] + public class AdminController : Controller { + private readonly IAliasService _aliasService; + private readonly IAliasHolder _aliasHolder; + + public AdminController( + IAliasService aliasService, + IOrchardServices orchardServices, + IAliasHolder aliasHolder ) { + _aliasService = aliasService; + _aliasHolder = aliasHolder; + Services = orchardServices; + T = NullLocalizer.Instance; + } + + public IOrchardServices Services { get; private set; } + public Localizer T { get; set; } + + public ActionResult Index(AdminIndexOptions options, PagerParameters pagerParameters) { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage aliases"))) + return new HttpUnauthorizedResult(); + + var pager = new Pager(Services.WorkContext.CurrentSite, pagerParameters); + + // default options + if (options == null) + options = new AdminIndexOptions(); + + switch (options.Filter) { + case AliasFilter.All: + break; + default: + throw new ArgumentOutOfRangeException(); + } + + var aliases = _aliasHolder.GetMaps().SelectMany(x => x.GetAliases()); + + if (!String.IsNullOrWhiteSpace(options.Search)) { + var invariantSearch = options.Search.ToLowerInvariant(); + aliases = aliases.Where(x => x.Path.ToLowerInvariant().Contains(invariantSearch)); + } + + aliases = aliases.ToList(); + + var pagerShape = Services.New.Pager(pager).TotalItemCount(aliases.Count()); + + switch (options.Order) { + case AliasOrder.Path: + aliases = aliases.OrderBy(x => x.Path); + break; + } + + if (pager.PageSize != 0) { + aliases = aliases.Skip(pager.GetStartIndex()).Take(pager.PageSize); + } + + var model = new AdminIndexViewModel { + Options = options, + Pager = pagerShape, + AliasEntries = aliases.Select(x => new AliasEntry() {Alias = x, IsChecked = false}).ToList() + }; + + return View(model); + } + + [HttpPost] + [FormValueRequired("submit.BulkEdit")] + public ActionResult Index(FormCollection input) { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage aliases"))) + return new HttpUnauthorizedResult(); + + var viewModel = new AdminIndexViewModel { AliasEntries = new List(), Options = new AdminIndexOptions() }; + UpdateModel(viewModel); + + var checkedItems = viewModel.AliasEntries.Where(c => c.IsChecked); + + switch (viewModel.Options.BulkAction) { + case AliasBulkAction.None: + break; + case AliasBulkAction.Delete: + foreach (var checkedItem in checkedItems) { + _aliasService.Delete(checkedItem.Alias.Path); + } + + break; + default: + throw new ArgumentOutOfRangeException(); + } + + return RedirectToAction("Index"); + } + + public ActionResult Add() { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage aliases"))) + return new HttpUnauthorizedResult(); + + return View(); + } + + [HttpPost] + public ActionResult Add(string aliasPath, string routePath) { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage aliases"))) + return new HttpUnauthorizedResult(); + + if(aliasPath == "/") { + aliasPath = String.Empty; + } + + if (String.IsNullOrWhiteSpace(aliasPath)) { + ModelState.AddModelError("Path", T("Path can't be empty").Text); + } + + if (String.IsNullOrWhiteSpace(aliasPath)) { + ModelState.AddModelError("Route", T("Route can't be empty").Text); + } + + if(!ModelState.IsValid) { + return View(); + } + + try { + _aliasService.Set(aliasPath, routePath, String.Empty); + } + catch { + Services.TransactionManager.Cancel(); + Services.Notifier.Error(T("An error occured while creating the alias. Please check the values are correct.", aliasPath)); + + ViewBag.Path = aliasPath; + ViewBag.Route = routePath; + + return View(); + } + + Services.Notifier.Information(T("Alias {0} created", aliasPath)); + + return RedirectToAction("Index"); + } + + public ActionResult Edit(string path) { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage aliases"))) + return new HttpUnauthorizedResult(); + + if (path == "/") { + path = String.Empty; + } + + var routeValues = _aliasService.Get(path); + + if (routeValues==null) + return HttpNotFound(); + + var virtualPaths = _aliasService.LookupVirtualPaths(routeValues,HttpContext) + .Select(vpd=>vpd.VirtualPath); + + ViewBag.AliasPath = path; + ViewBag.RoutePath = virtualPaths.FirstOrDefault(); + + return View("Edit"); + } + + [HttpPost] + public ActionResult Edit(string path, string aliasPath, string routePath) { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage aliases"))) + return new HttpUnauthorizedResult(); + + // TODO: (PH:Autoroute) This could overwrite an existing Alias without warning, should handle this + _aliasService.Set(aliasPath, routePath, "Custom"); + + // Remove previous alias + if (path != aliasPath) + { + // TODO: (PH:Autoroute) Ability to fire an "AliasChanged" event so we make a redirect + _aliasService.Delete(path); + } + + Services.Notifier.Information(T("Alias {0} updated", path)); + + return RedirectToAction("Index"); + } + + [HttpPost] + public ActionResult Delete(string path) { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage aliases"))) + return new HttpUnauthorizedResult(); + + if (path == "/") { + path = String.Empty; + } + + _aliasService.Delete(path); + + Services.Notifier.Information(T("Alias {0} deleted", path)); + + return RedirectToAction("Index"); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/IAliasService.cs b/src/Orchard.Web/Modules/Orchard.Alias/IAliasService.cs new file mode 100644 index 000000000..aec3fcde1 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/IAliasService.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Web.Routing; + +namespace Orchard.Alias +{ + public interface IAliasService : IDependency + { + RouteValueDictionary Get(string aliasPath); + void Set(string aliasPath, RouteValueDictionary routeValues, string aliasSource); + void Set(string aliasPath, string routePath, string aliasSource); + void Delete(string aliasPath); + void Delete(string aliasPath, string aliasSource); + /// + /// Delete Alias from a particular source + /// + /// + void DeleteBySource(string aliasSource); + + IEnumerable Lookup(RouteValueDictionary routeValues); + IEnumerable Lookup(string routePath); + + void Replace(string aliasPath, RouteValueDictionary routeValues, string aliasSource); + void Replace(string aliasPath, string routePath, string aliasSource); + + IEnumerable> List(); + IEnumerable> List(string sourceStartsWith); + IEnumerable LookupVirtualPaths(RouteValueDictionary routeValues, System.Web.HttpContextBase HttpContext); + + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Implementation/AliasRoute.cs b/src/Orchard.Web/Modules/Orchard.Alias/Implementation/AliasRoute.cs new file mode 100644 index 000000000..34bf1c6c2 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Implementation/AliasRoute.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.Web; +using System.Web.Mvc; +using System.Web.Routing; +using Orchard.Alias.Implementation.Holder; +using Orchard.Alias.Implementation.Map; + +namespace Orchard.Alias.Implementation +{ + public class AliasRoute : RouteBase, IRouteWithArea + { + private readonly AliasMap _aliasMap; + private readonly IRouteHandler _routeHandler; + + public AliasRoute(IAliasHolder aliasHolder, string areaName, IRouteHandler routeHandler) + { + Area = areaName; + _aliasMap = aliasHolder.GetMap(areaName); + _routeHandler = routeHandler; + } + + public override RouteData GetRouteData(HttpContextBase httpContext) + { + // Get the full inbound request path + var virtualPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo; + + // Attempt to lookup RouteValues in the alias map + IDictionary routeValues; + // TODO: Might as well have the lookup in AliasHolder... + if (_aliasMap.TryGetAlias(virtualPath, out routeValues)) + { + // Construct RouteData from the route values + var data = new RouteData(this, _routeHandler); + foreach (var routeValue in routeValues) + { + var key = routeValue.Key; + if (key.EndsWith("-")) + data.Values.Add(key.Substring(0, key.Length - 1), routeValue.Value); + else + data.Values.Add(key, routeValue.Value); + } + + data.Values["area"] = Area; + data.DataTokens["area"] = Area; + + return data; + } + return null; + } + + public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary routeValues) + { + // Lookup best match for route values in the expanded tree + var match = _aliasMap.Locate(routeValues); + if (match != null) + { + // Build any "spare" route values onto the Alias (so we correctly support any additional query parameters) + var sb = new StringBuilder(match.Item2); + var extra = 0; + foreach (var routeValue in routeValues) + { + // Ignore any we already have + if (match.Item1.ContainsKey(routeValue.Key)) + { + continue; + } + // Add a query string fragment + sb.Append((extra++ == 0) ? '?' : '&'); + sb.Append(Uri.EscapeDataString(routeValue.Key)); + sb.Append('='); + sb.Append(Uri.EscapeDataString(Convert.ToString(routeValue.Value, CultureInfo.InvariantCulture))); + } + // Construct data + var data = new VirtualPathData(this, sb.ToString()); + // Set the Area for this route + data.DataTokens["area"] = Area; + return data; + } + + return null; + } + + public string Area { get; private set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Implementation/DefaultAliasService.cs b/src/Orchard.Web/Modules/Orchard.Alias/Implementation/DefaultAliasService.cs new file mode 100644 index 000000000..ccf809791 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Implementation/DefaultAliasService.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Web; +using System.Web.Routing; +using Orchard.Alias.Implementation.Storage; +using Orchard.Mvc.Routes; +using Orchard.Utility.Extensions; + +namespace Orchard.Alias.Implementation { + public class DefaultAliasService : IAliasService { + private readonly IAliasStorage _aliasStorage; + private readonly IEnumerable _routeProviders; + private readonly Lazy> _routeDescriptors; + + public DefaultAliasService( + IAliasStorage aliasStorage, + IEnumerable routeProviders) { + _aliasStorage = aliasStorage; + _routeProviders = routeProviders; + + _routeDescriptors = new Lazy>(GetRouteDescriptors); + } + + public RouteValueDictionary Get(string aliasPath) { + return _aliasStorage.Get(aliasPath).ToRouteValueDictionary(); + } + + public void Set(string aliasPath, RouteValueDictionary routeValues, string aliasSource) { + _aliasStorage.Set( + aliasPath, + ToDictionary(routeValues), + aliasSource); + } + + public void Set(string aliasPath, string routePath, string aliasSource) { + _aliasStorage.Set( + aliasPath.TrimStart('/'), + ToDictionary(routePath), + aliasSource); + } + + public void Delete(string aliasPath) { + + if (aliasPath == null) { + aliasPath = String.Empty; + } + + _aliasStorage.Remove(aliasPath); + } + + public void Delete(string aliasPath, string aliasSource) { + + if (aliasPath == null) { + aliasPath = String.Empty; + } + + _aliasStorage.Remove(aliasPath, aliasSource); + } + + public void DeleteBySource(string aliasSource) { + _aliasStorage.RemoveBySource(aliasSource); + } + + public IEnumerable Lookup(string routePath) { + return Lookup(ToDictionary(routePath).ToRouteValueDictionary()); + } + + public void Replace(string aliasPath, RouteValueDictionary routeValues, string aliasSource) { + foreach (var lookup in Lookup(routeValues).Where(path => path != aliasPath)) { + Delete(lookup, aliasSource); + } + Set(aliasPath, routeValues, aliasSource); + } + + public void Replace(string aliasPath, string routePath, string aliasSource) { + Replace(aliasPath, ToDictionary(routePath).ToRouteValueDictionary(), aliasSource); + } + + public IEnumerable Lookup(RouteValueDictionary routeValues) { + return List().Where(item => item.Item2.Match(routeValues)).Select(item=>item.Item1).ToList(); + } + + public IEnumerable> List() { + return _aliasStorage.List().Select(item => Tuple.Create(item.Item1, item.Item3.ToRouteValueDictionary())); + } + + public IEnumerable> List(string sourceStartsWith) { + return _aliasStorage.List(sourceStartsWith).Select(item => Tuple.Create(item.Item1, item.Item3.ToRouteValueDictionary(), item.Item4)); + } + + public IEnumerable LookupVirtualPaths(RouteValueDictionary routeValues,HttpContextBase httpContext) { + return Utils.LookupVirtualPaths(httpContext, _routeDescriptors.Value, routeValues); + } + + private IDictionary ToDictionary(string routePath) { + if (routePath == null) + return null; + + return Utils.LookupRouteValues(new StubHttpContext(), _routeDescriptors.Value, routePath); + } + + private static IDictionary ToDictionary(IEnumerable> routeValues) { + if (routeValues == null) + return null; + + return routeValues.ToDictionary(kv => kv.Key, kv => Convert.ToString(kv.Value, CultureInfo.InvariantCulture)); + } + + private IEnumerable GetRouteDescriptors() { + return _routeProviders + .SelectMany(routeProvider => { + var routes = new List(); + routeProvider.GetRoutes(routes); + return routes; + }) + .Where(routeDescriptor => !(routeDescriptor.Route is AliasRoute)) + .OrderByDescending(routeDescriptor => routeDescriptor.Priority); + } + + private class StubHttpContext : HttpContextBase { + public override HttpRequestBase Request + { + get{return new StubHttpRequest();} + } + + private class StubHttpRequest : HttpRequestBase {} + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Holder/AliasHolder.cs b/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Holder/AliasHolder.cs new file mode 100644 index 000000000..07697ab79 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Holder/AliasHolder.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Collections.Concurrent; +using Orchard.Alias.Implementation.Map; + +namespace Orchard.Alias.Implementation.Holder { + public class AliasHolder : IAliasHolder { + public AliasHolder() { + _aliasMaps = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + } + + private readonly ConcurrentDictionary _aliasMaps; + + public void SetAliases(IEnumerable aliases) { + var grouped = aliases.GroupBy(alias => alias.Area ?? String.Empty, StringComparer.InvariantCultureIgnoreCase); + + foreach (var group in grouped) { + var map = GetMap(group.Key); + + foreach (var alias in group) { + map.Insert(alias); + } + } + } + + public void SetAlias(AliasInfo alias) { + foreach(var map in _aliasMaps.Values) { + map.Remove(alias); + } + + GetMap(alias.Area).Insert(alias); + } + + public IEnumerable GetMaps() { + return _aliasMaps.Values; + } + + public AliasMap GetMap(string areaName) { + return _aliasMaps.GetOrAdd(areaName ?? String.Empty, key => new AliasMap(key)); + } + + public void RemoveAlias(AliasInfo aliasInfo) { + GetMap(aliasInfo.Area ?? String.Empty).Remove(aliasInfo); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Holder/AliasInfo.cs b/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Holder/AliasInfo.cs new file mode 100644 index 000000000..a85c4377a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Holder/AliasInfo.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Orchard.Alias.Implementation.Holder { + public class AliasInfo { + public string Area { get; set; } + public string Path { get; set; } + public IDictionary RouteValues { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Holder/IAliasHolder.cs b/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Holder/IAliasHolder.cs new file mode 100644 index 000000000..cff41f482 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Holder/IAliasHolder.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Orchard.Alias.Implementation.Map; + +namespace Orchard.Alias.Implementation.Holder { + /// + /// Holds every alias in a tree structure, indexed by area + /// + public interface IAliasHolder : ISingletonDependency { + + /// + /// Returns an for a specific area + /// + AliasMap GetMap(string areaName); + + /// + /// Returns all instances + /// + IEnumerable GetMaps(); + + /// + /// Adds or updates an alias in the tree + /// + void SetAlias(AliasInfo alias); + + /// + /// Adds or updates a set of aliases in the tree + /// + void SetAliases(IEnumerable aliases); + + /// + /// Removes an alias from the tree based on its path + /// + void RemoveAlias(AliasInfo aliasInfo); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Map/AliasMap.cs b/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Map/AliasMap.cs new file mode 100644 index 000000000..bb0a631f4 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Map/AliasMap.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Web.Routing; +using Orchard.Alias.Implementation.Holder; +using System.Collections.Concurrent; + +namespace Orchard.Alias.Implementation.Map { + public class AliasMap { + private readonly string _area; + private readonly ConcurrentDictionary> _aliases; + private readonly Node _root; + + public AliasMap(string area) { + _area = area; + _aliases = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); + _root = new Node(); + } + + public IEnumerable GetAliases() { + return _aliases.Select(x => new AliasInfo {Area = _area, Path = x.Key, RouteValues = x.Value}); + } + + public bool TryGetAlias(string virtualPath, out IDictionary routeValues) { + return _aliases.TryGetValue(virtualPath, out routeValues); + } + + public Tuple, string> Locate(RouteValueDictionary routeValues) { + return Traverse(_root, routeValues, _area); + } + + /// + /// Adds an to the map + /// + /// The intance to add + public void Insert(AliasInfo info) { + if(info == null) { + throw new ArgumentNullException(); + } + + _aliases[info.Path] = info.RouteValues; + ExpandTree(_root, info.Path, info.RouteValues); + } + + /// + /// Removes an alias from the map + /// + /// + public void Remove(AliasInfo info) { + IDictionary values; + _aliases.TryRemove(info.Path, out values); + CollapseTree(_root, info.Path, info.RouteValues); + } + + private static void CollapseTree(Node root, string path, IDictionary routeValues) { + foreach (var expanded in Expand(routeValues)) { + var focus = root; + foreach (var routeValue in expanded.OrderBy(kv => kv.Key, StringComparer.InvariantCultureIgnoreCase)) { + // See if we already have a stem for this route key (i.e. "controller") and create if not + var stem = focus.Stems.GetOrAdd(routeValue.Key, key => new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase)); + // See if the stem has a node for this value (i.e. "Item") and create if not + var node = stem.GetOrAdd(routeValue.Value, key => new Node()); + // Keep switching to new node until we reach deepest match + // TODO: (PH) Thread safety: at this point something could techincally traverse and find an empty node with a blank path ... not fatal + // since it will simply not match and therefore return a default-looking route instead of the aliased one. And the changes of that route + // being the same one which is just being added are very low. + focus = node; + } + // Set the path at the end of the tree + object takenPath; + focus.Paths.TryRemove(path,out takenPath); + } + } + + private static void ExpandTree(Node root, string path, IDictionary routeValues) { + foreach(var expanded in Expand(routeValues)) { + var focus = root; + foreach (var routeValue in expanded.OrderBy(kv => kv.Key, StringComparer.InvariantCultureIgnoreCase)) { + // See if we already have a stem for this route key (i.e. "controller") and create if not + var stem = focus.Stems.GetOrAdd(routeValue.Key,key=>new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase)); + // See if the stem has a node for this value (i.e. "Item") and create if not + var node = stem.GetOrAdd(routeValue.Value, key=>new Node()); + // Keep switching to new node until we reach deepest match + // TODO: (PH) Thread safety: at this point something could techincally traverse and find an empty node with a blank path ... not fatal + // since it will simply not match and therefore return a default-looking route instead of the aliased one. And the changes of that route + // being the same one which is just being added are very low. + focus = node; + } + // Set the path at the end of the tree + focus.Paths.TryAdd(path, null); + } + } + + private static IEnumerable Product(IEnumerable source1, IEnumerable source2, Func produce) { + return from item1 in source1 from item2 in source2 select produce(item1, item2); + } + + private static IEnumerable Single(T t) { + yield return t; + } + + private static IEnumerable Empty() { + return Enumerable.Empty(); + } + + /// + /// Expand the route values into all possible combinations of keys + /// + /// + /// + private static IEnumerable>> Expand(IDictionary routeValues) { + var ordered = routeValues.OrderBy(kv => kv.Key, StringComparer.InvariantCultureIgnoreCase); + var empty = Empty>(); + + // For each key/value pair, we want a list containing a single list with either the term, or the term and the "default" value + var termSets = ordered.Select(term => { + if (term.Key.EndsWith("-")) { + var termKey = term.Key.Substring(0, term.Key.Length - 1); + return new[] { + // This entry will auto-match in some cases because it was omitted from the route values + Single(new KeyValuePair(termKey, "\u0000")), + Single(new KeyValuePair(termKey, term.Value)) + }; + } + return new[] {new[] {term}}; + }); + + // Run each of those lists through an aggregation function, by taking the product of each set, so producting a tree of possibilities + var produced = termSets.Aggregate(Single(empty), (coords, termSet) => Product(coords, termSet, (coord, term) => coord.Concat(term))); + return produced; + } + + + private static Tuple, string> Traverse(Node focus, RouteValueDictionary routeValues, string areaName) { + + // Initialize a match variable + Tuple, string> match = null; + + // Check each stem to try and find a match for all the route values + // TODO: (PH) We know it's concurrent; but what happens if a new element is added during this loop? + // TODO: (PH) Also, this could be optimised more. Need to see how many stems are typically being looped - could arrange things for a + // much quicker matchup against the routeValues and using the fact that the stems have keys. + foreach (var stem in focus.Stems) { + var routeValue = "\u0000"; // Represents the default value when not provided in the route values + + object value; + // Area has been stripped out of routeValues (because the route is IRouteWithArea MVC assumes the area is inferred, I think) + // but we still need to match it in the stem (another way would be to strip it out of the nodes altogether). PH + // TODO: Actually was this supposed to be the behaviour of the hyphens; by adding the hyphen MVC will no longer strip it out? PH + if (stem.Key == "area") { + routeValue = areaName; + } + // See if the route we're checking contains the key + if (routeValues.TryGetValue(stem.Key, out value)) { + routeValue = Convert.ToString(value, CultureInfo.InvariantCulture); + } + + // Must find a value on the stem, matching the route's value + Node node; + if (!stem.Value.TryGetValue(routeValue, out node)) { + continue; + } + + // Continue traversing with the new node + var deeper = Traverse(node, routeValues, areaName); + if (deeper == null) { + continue; + } + + // Create a key in the dictionary + deeper.Item1.Add(stem.Key, null); + // If it's better than a current match (more items), take it + if (match == null || deeper.Item1.Count > match.Item1.Count) { + match = deeper; + } + } + + if (match == null) { + var foundPath = focus.Paths.Keys.FirstOrDefault(); + if (foundPath != null) { + // Here the deepest match is being created, which will be populated as it rises back up the stack, but save the path here. + // Within this function it's used to count how many items match so we get the best one; but when it's returned + // to AliasRoute it will also need the key lookup for speed + match = Tuple.Create((IDictionary)new Dictionary(StringComparer.InvariantCultureIgnoreCase), foundPath); + } + } + return match; + } + + private class Node { + public Node() { + Stems = new ConcurrentDictionary>(StringComparer.InvariantCultureIgnoreCase); + Paths = new ConcurrentDictionary(); + } + + public ConcurrentDictionary> Stems { get; set; } + public ConcurrentDictionary Paths { get; set; } + } + + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Storage/AliasStorage.cs b/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Storage/AliasStorage.cs new file mode 100644 index 000000000..04278bfe2 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Storage/AliasStorage.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using Orchard.Alias.Records; +using Orchard.Data; +using Orchard.Alias.Implementation.Holder; + +namespace Orchard.Alias.Implementation.Storage { + public interface IAliasStorage : IDependency { + void Set(string path, IDictionary routeValues, string source); + IDictionary Get(string aliasPath); + void Remove(string path); + void Remove(string path, string aliasSource); + void RemoveBySource(string aliasSource); + IEnumerable, string>> List(); + IEnumerable, string>> List(string sourceStartsWith); + } + + public class AliasStorage : IAliasStorage { + private readonly IRepository _aliasRepository; + private readonly IRepository _actionRepository; + private readonly IAliasHolder _aliasHolder; + public AliasStorage(IRepository aliasRepository, IRepository actionRepository, IAliasHolder aliasHolder) { + _aliasRepository = aliasRepository; + _actionRepository = actionRepository; + _aliasHolder = aliasHolder; + } + + public void Set(string path, IDictionary routeValues, string source) { + if (path == null) { + throw new ArgumentNullException("path"); + } + + var aliasRecord = _aliasRepository.Fetch(r => r.Path == path, o => o.Asc(r => r.Id), 0, 1).FirstOrDefault(); + aliasRecord = aliasRecord ?? new AliasRecord { Path = path }; + + string areaName = null; + string controllerName = null; + string actionName = null; + var values = new XElement("v"); + foreach (var routeValue in routeValues.OrderBy(kv => kv.Key, StringComparer.InvariantCultureIgnoreCase)) { + if (string.Equals(routeValue.Key, "area", StringComparison.InvariantCultureIgnoreCase) + || string.Equals(routeValue.Key, "area-", StringComparison.InvariantCultureIgnoreCase)) { + areaName = routeValue.Value; + } + else if (string.Equals(routeValue.Key, "controller", StringComparison.InvariantCultureIgnoreCase)) { + controllerName = routeValue.Value; + } + else if (string.Equals(routeValue.Key, "action", StringComparison.InvariantCultureIgnoreCase)) { + actionName = routeValue.Value; + } + else { + values.SetAttributeValue(routeValue.Key, routeValue.Value); + } + } + + aliasRecord.Action = _actionRepository.Fetch( + r => r.Area == areaName && r.Controller == controllerName && r.Action == actionName, + o => o.Asc(r => r.Id), 0, 1).FirstOrDefault(); + aliasRecord.Action = aliasRecord.Action ?? new ActionRecord { Area = areaName, Controller = controllerName, Action = actionName }; + + aliasRecord.RouteValues = values.ToString(); + aliasRecord.Source = source; + if (aliasRecord.Action.Id == 0 || aliasRecord.Id == 0) { + if (aliasRecord.Action.Id == 0) { + _actionRepository.Create(aliasRecord.Action); + } + if (aliasRecord.Id == 0) { + _aliasRepository.Create(aliasRecord); + } + // Bulk updates might go wrong if we don't flush + _aliasRepository.Flush(); + } + // Transform and push into AliasHolder + var dict = ToDictionary(aliasRecord); + _aliasHolder.SetAlias(new AliasInfo { Path = dict.Item1, Area = dict.Item2, RouteValues = dict.Item3 }); + } + + public IDictionary Get(string path) { + return _aliasRepository + .Fetch(r => r.Path == path, o => o.Asc(r => r.Id), 0, 1) + .Select(ToDictionary) + .Select(item => item.Item3) + .SingleOrDefault(); + } + + public void Remove(string path) { + + if (path == null) { + throw new ArgumentNullException("path"); + } + + foreach (var aliasRecord in _aliasRepository.Fetch(r => r.Path == path)) { + _aliasRepository.Delete(aliasRecord); + // Bulk updates might go wrong if we don't flush + _aliasRepository.Flush(); + var dict = ToDictionary(aliasRecord); + _aliasHolder.RemoveAlias(new AliasInfo() { Path = dict.Item1, Area = dict.Item2, RouteValues = dict.Item3 }); + } + } + public void Remove(string path, string aliasSource) { + + if (path == null) { + throw new ArgumentNullException("path"); + } + + foreach (var aliasRecord in _aliasRepository.Fetch(r => r.Path == path && r.Source == aliasSource)) { + _aliasRepository.Delete(aliasRecord); + // Bulk updates might go wrong if we don't flush + _aliasRepository.Flush(); + var dict = ToDictionary(aliasRecord); + _aliasHolder.RemoveAlias(new AliasInfo() { Path = dict.Item1, Area = dict.Item2, RouteValues = dict.Item3 }); + } + } + + public void RemoveBySource(string aliasSource) { + foreach (var aliasRecord in _aliasRepository.Fetch(r => r.Source == aliasSource)) { + _aliasRepository.Delete(aliasRecord); + // Bulk updates might go wrong if we don't flush + _aliasRepository.Flush(); + var dict = ToDictionary(aliasRecord); + _aliasHolder.RemoveAlias(new AliasInfo() { Path = dict.Item1, Area = dict.Item2, RouteValues = dict.Item3 }); + } + } + + public IEnumerable, string>> List() { + return _aliasRepository.Table.OrderBy(a => a.Id).Select(ToDictionary).ToList(); + } + + public IEnumerable, string>> List(string sourceStartsWith) { + return _aliasRepository.Table.Where(a => a.Source.StartsWith(sourceStartsWith)).OrderBy(a => a.Id).Select(ToDictionary).ToList(); + } + + private static Tuple, string> ToDictionary(AliasRecord aliasRecord) { + IDictionary routeValues = new Dictionary(); + if (aliasRecord.Action.Area != null) { + routeValues.Add("area", aliasRecord.Action.Area); + } + if (aliasRecord.Action.Controller != null) { + routeValues.Add("controller", aliasRecord.Action.Controller); + } + if (aliasRecord.Action.Action != null) { + routeValues.Add("action", aliasRecord.Action.Action); + } + if (!string.IsNullOrEmpty(aliasRecord.RouteValues)) { + foreach (var attr in XElement.Parse(aliasRecord.RouteValues).Attributes()) { + routeValues.Add(attr.Name.LocalName, attr.Value); + } + } + return Tuple.Create(aliasRecord.Path, aliasRecord.Action.Area, routeValues, aliasRecord.Source); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Updater/AliasUpdater.cs b/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Updater/AliasUpdater.cs new file mode 100644 index 000000000..fde8ae476 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Updater/AliasUpdater.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using Orchard.Alias.Implementation.Holder; +using Orchard.Alias.Implementation.Storage; +using Orchard.Data; +using Orchard.Environment; +using Orchard.Tasks; +using Orchard.Logging; + +namespace Orchard.Alias.Implementation.Updater +{ + public class AliasHolderUpdater : IOrchardShellEvents, IBackgroundTask + { + private readonly IAliasHolder _aliasHolder; + private readonly IAliasStorage _storage; + + public ILogger Logger { get; set; } + + public AliasHolderUpdater(IAliasHolder aliasHolder, IAliasStorage storage) + { + _aliasHolder = aliasHolder; + _storage = storage; + Logger = NullLogger.Instance; + } + + void IOrchardShellEvents.Activated() + { + Refresh(); + } + + void IOrchardShellEvents.Terminating() + { + } + + private void Refresh() { + try { + var aliases = _storage.List(); + _aliasHolder.SetAliases(aliases.Select(alias=>new AliasInfo{Path = alias.Item1, Area = alias.Item2, RouteValues=alias.Item3})); + } + catch(Exception ex) { + Logger.Error(ex,"Exception during Alias refresh"); + } + } + + public void Sweep() { + Refresh(); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Utils.cs b/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Utils.cs new file mode 100644 index 000000000..c8c1a6681 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Implementation/Utils.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Web; +using System.Web.Routing; +using Orchard.Mvc.Extensions; +using Orchard.Mvc.Routes; +using Orchard.Mvc.Wrappers; + +namespace Orchard.Alias.Implementation +{ + public static class Utils + { + public static IDictionary LookupRouteValues + (HttpContextBase httpContext, IEnumerable + routeDescriptors, + string routePath) + { + var queryStringIndex = routePath.IndexOf('?'); + var routePathNoQueryString = queryStringIndex == -1 ? routePath : routePath.Substring(0, queryStringIndex); + var queryString = queryStringIndex == -1 ? null : routePath.Substring(queryStringIndex + 1); + + var lookupContext = new LookupHttpContext(httpContext, routePathNoQueryString); + var matches = routeDescriptors + .Select(routeDescriptor => routeDescriptor.Route.GetRouteData(lookupContext)) + .Where(routeData => routeData != null) + .Select(data => ToRouteValues(data, queryString)); + + return matches.FirstOrDefault(); + } + + public static IEnumerable LookupVirtualPaths( + HttpContextBase httpContext, + IEnumerable routeDescriptors, + RouteValueDictionary routeValues) { + + var areaName = ""; + object value; + if (routeValues.TryGetValue("area", out value)) + areaName = Convert.ToString(value, CultureInfo.InvariantCulture); + + var virtualPathDatas = routeDescriptors.Where(r2 => r2.Route.GetAreaName() == areaName) + .Select(r2 => r2.Route.GetVirtualPath(httpContext.Request.RequestContext, routeValues)) + .Where(vp => vp != null) + .ToArray(); + + return virtualPathDatas; + } + + public static IEnumerable LookupVirtualPaths( + HttpContextBase httpContext, + IEnumerable routeDescriptors, + string areaName, + IDictionary routeValues) { + + var routeValueDictionary = new RouteValueDictionary(routeValues.ToDictionary(kv => RemoveDash(kv.Key), kv => (object)kv.Value)); + var virtualPathDatas = routeDescriptors.Where(r2 => r2.Route.GetAreaName() == areaName) + .Select(r2 => r2.Route.GetVirtualPath(httpContext.Request.RequestContext, routeValueDictionary)) + .Where(vp => vp != null) + .ToArray(); + + return virtualPathDatas; + } + + private static string RemoveDash(string key) { + return key.EndsWith("-", StringComparison.InvariantCulture) ? key.Substring(0, key.Length - 1) : key; + } + + + private static Dictionary ToRouteValues(RouteData routeData, string queryString) + { + var routeValues = routeData.Values + .Select(kv => + { + var value = Convert.ToString(kv.Value, CultureInfo.InvariantCulture); + var defaultValue = FindDefault(routeData.Route, kv.Key); + if (defaultValue != null && string.Equals(defaultValue, value, StringComparison.InvariantCultureIgnoreCase)) + { + return new { Key = kv.Key + "-", Value = value }; + } + return new { kv.Key, Value = value }; + }) + .ToDictionary(kv => kv.Key, kv => kv.Value); + if (queryString != null) + { + foreach (var term in queryString + .Split(new[] { "&" }, StringSplitOptions.RemoveEmptyEntries) + .Select(ParseTerm)) + { + if (!routeValues.ContainsKey(term[0])) + routeValues[term[0]] = term[1]; + } + } + return routeValues; + } + + private static string[] ParseTerm(string term) + { + var equalsIndex = term.IndexOf('='); + if (equalsIndex == -1) + { + return new[] { Uri.UnescapeDataString(term), null }; + } + return new[] { Uri.UnescapeDataString(term.Substring(0, equalsIndex)), Uri.UnescapeDataString(term.Substring(equalsIndex + 1)) }; + } + + private static string FindDefault(RouteBase route, string key) + { + var route2 = route as Route; + if (route2 == null) + { + return null; + } + + object defaultValue; + if (!route2.Defaults.TryGetValue(key, out defaultValue)) + { + return null; + } + return Convert.ToString(defaultValue, CultureInfo.InvariantCulture); + } + + public class LookupHttpContext : HttpContextBaseWrapper + { + private readonly string _path; + + public LookupHttpContext(HttpContextBase httpContext, string path) + : base(httpContext) + { + _path = path; + } + + public override HttpRequestBase Request + { + get { return new LookupHttpRequest(this, base.Request, _path); } + } + + private class LookupHttpRequest : HttpRequestBaseWrapper + { + private readonly string _path; + + public LookupHttpRequest(HttpContextBase httpContextBase, HttpRequestBase httpRequestBase, string path) + : base( /*httpContextBase,*/ httpRequestBase) + { + _path = path; + } + + + public override string AppRelativeCurrentExecutionFilePath + { + get { return "~/" + _path; } + } + + public override string ApplicationPath + { + get { return "/"; } + } + + public override string Path + { + get { return "/" + _path; } + } + + public override string PathInfo + { + get { return ""; } + } + } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Migrations.cs b/src/Orchard.Web/Modules/Orchard.Alias/Migrations.cs new file mode 100644 index 000000000..1b36b99ea --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Migrations.cs @@ -0,0 +1,24 @@ +using Orchard.Data.Migration; + +namespace Orchard.Alias { + public class Migrations : DataMigrationImpl { + public int Create() { + SchemaBuilder + .CreateTable("AliasRecord", + table => table + .Column("Id", column => column.PrimaryKey().Identity()) + .Column("Path", c => c.WithLength(2048)) + .Column("Action_id") + .Column("RouteValues", c => c.Unlimited()) + .Column("Source", c => c.WithLength(256))) + .CreateTable("ActionRecord", + table => table + .Column("Id", column => column.PrimaryKey().Identity()) + .Column("Area") + .Column("Controller") + .Column("Action")); + return 1; + } + + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Module.txt b/src/Orchard.Web/Modules/Orchard.Alias/Module.txt new file mode 100644 index 000000000..15c9d22ce --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Module.txt @@ -0,0 +1,15 @@ +Name: Alias +AntiForgery: enabled +Author: The Orchard Team +Website: http://orchardproject.net +Version: 1.5 +OrchardVersion: 1.0 +Description: Description for the module +FeatureDescription: Description for feature Orchard.Alias. +Category: Content +Features: + Orchard.Alias.UI: + Name: Alias UI + Description: Admin user interace for Orchard.Alias. + Dependencies: Orchard.Alias + Category: Content diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Orchard.Alias.csproj b/src/Orchard.Web/Modules/Orchard.Alias/Orchard.Alias.csproj new file mode 100644 index 000000000..83cbde8c7 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Orchard.Alias.csproj @@ -0,0 +1,147 @@ + + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {475B6C45-B27C-438B-8966-908B9D6D1077} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + Orchard.Alias + Orchard.Alias + v4.0 + false + + + 4.0 + + + + false + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + AllRules.ruleset + + + pdbonly + true + bin\ + TRACE + prompt + 4 + AllRules.ruleset + + + + + + + 3.5 + + + + False + ..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll + + + + + + + + + + + + + + + + + + + + + {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6} + Orchard.Framework + + + {9916839C-39FC-4CEB-A5AF-89CA7E87119F} + Orchard.Core + + + + + + + + + + + + + + + + + + + + + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + $(ProjectDir)\..\Manifests + + + + + + + + + + + + False + True + 45979 + / + + + False + True + http://orchard.codeplex.com + False + + + + + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Properties/AssemblyInfo.cs b/src/Orchard.Web/Modules/Orchard.Alias/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..1fe715a99 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security; + +// 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("Orchard.Alias")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyProduct("Orchard")] +[assembly: AssemblyCopyright("Copyright © Outercurve Foundation 2009")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// 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("ca6e5d9b-833e-4c3f-b7b9-65e1929f6302")] + +// 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("1.5")] +[assembly: AssemblyFileVersion("1.5")] + diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Records/ActionRecord.cs b/src/Orchard.Web/Modules/Orchard.Alias/Records/ActionRecord.cs new file mode 100644 index 000000000..f975431cf --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Records/ActionRecord.cs @@ -0,0 +1,8 @@ +namespace Orchard.Alias.Records { + public class ActionRecord { + public virtual int Id { get; set; } + public virtual string Area { get; set; } + public virtual string Controller { get; set; } + public virtual string Action { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Records/AliasRecord.cs b/src/Orchard.Web/Modules/Orchard.Alias/Records/AliasRecord.cs new file mode 100644 index 000000000..258667261 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Records/AliasRecord.cs @@ -0,0 +1,9 @@ +namespace Orchard.Alias.Records { + public class AliasRecord { + public virtual int Id { get; set; } + public virtual string Path { get; set; } + public virtual ActionRecord Action { get; set; } + public virtual string RouteValues { get; set; } + public virtual string Source { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Routes.cs b/src/Orchard.Web/Modules/Orchard.Alias/Routes.cs new file mode 100644 index 000000000..f09bcac46 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Routes.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; +using System.Web.Mvc; +using Orchard.Alias.Implementation; +using Orchard.Alias.Implementation.Holder; +using Orchard.Environment.ShellBuilders.Models; +using Orchard.Mvc.Routes; + +namespace Orchard.Alias { + public class Routes : IRouteProvider { + private readonly ShellBlueprint _blueprint; + private readonly IAliasHolder _aliasHolder; + + public Routes(ShellBlueprint blueprint, IAliasHolder aliasHolder) { + _blueprint = blueprint; + _aliasHolder = aliasHolder; + } + + public void GetRoutes(ICollection routes) { + foreach (RouteDescriptor routeDescriptor in GetRoutes()) { + routes.Add(routeDescriptor); + } + } + + public IEnumerable GetRoutes() { + var distinctAreaNames = _blueprint.Controllers + .Select(controllerBlueprint => controllerBlueprint.AreaName) + .Distinct(); + + return distinctAreaNames.Select(areaName => + new RouteDescriptor { + Priority = 80, + Route = new AliasRoute(_aliasHolder, areaName, new MvcRouteHandler()) + }).ToList(); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/ViewModels/AdminIndexViewModel.cs b/src/Orchard.Web/Modules/Orchard.Alias/ViewModels/AdminIndexViewModel.cs new file mode 100644 index 000000000..da53408b8 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/ViewModels/AdminIndexViewModel.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Orchard.Alias.Implementation.Holder; + +namespace Orchard.Alias.ViewModels { + + public class AdminIndexViewModel { + public IList AliasEntries { get; set; } + public AdminIndexOptions Options { get; set; } + public dynamic Pager { get; set; } + } + + public class AliasEntry { + public AliasInfo Alias { get; set; } + public bool IsChecked { get; set; } + } + public class AdminIndexOptions { + public string Search { get; set; } + public AliasOrder Order { get; set; } + public AliasFilter Filter { get; set; } + public AliasBulkAction BulkAction { get; set; } + } + + public enum AliasOrder { + Path + } + + public enum AliasFilter { + All + } + + public enum AliasBulkAction { + None, + Delete + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Views/Admin/Add.cshtml b/src/Orchard.Web/Modules/Orchard.Alias/Views/Admin/Add.cshtml new file mode 100644 index 000000000..31a15ccd1 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Views/Admin/Add.cshtml @@ -0,0 +1,28 @@ +@{ + Layout.Title = T("Create Alias"); +} + +@using (Html.BeginFormAntiForgeryPost()) { + @Html.ValidationSummary() +
+ @T("Create Alias") +
+ + @Html.TextBox("aliasPath", (object)ViewBag.Path, new { @class = "large text" }) + @Html.ValidationMessage("aliasPath") + @T("The path of the alias e.g., my-blog/my-post") +
+
+ + @Html.TextBox("routePath", (object)ViewBag.Route, new { @class = "textMedium" }) + @Html.ValidationMessage("routePath") + @T("The actual route Orchard should call when the path is requested e.g., Blogs/Blog/Item?blogId=18") +
+
+
+
+ + @Html.ActionLink(T("Cancel").ToString(), "Index", new { }, new { @class = "button" }) +
+
+ } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Views/Admin/Delete.cshtml b/src/Orchard.Web/Modules/Orchard.Alias/Views/Admin/Delete.cshtml new file mode 100644 index 000000000..bda927365 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Views/Admin/Delete.cshtml @@ -0,0 +1,13 @@ +@{ + Layout.Title = T("Delete Alias"); +} + +@using (Html.BeginFormAntiForgeryPost()) { +
+ @T("Delete alias") +

@T("Removing alias '{0}'. Are you sure?", ViewBag.Path)

+ @Html.Hidden("path") + @Html.Hidden("confirmed", true) + +
+} diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Views/Admin/Edit.cshtml b/src/Orchard.Web/Modules/Orchard.Alias/Views/Admin/Edit.cshtml new file mode 100644 index 000000000..d186ac1e8 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Views/Admin/Edit.cshtml @@ -0,0 +1,28 @@ +@{ + Layout.Title = T("Edit Alias"); +} + +@using (Html.BeginFormAntiForgeryPost()) { + @Html.ValidationSummary() +
+ @T("Edit alias") +
+ + @Html.TextBox("aliasPath", null, new { @class = "large text" }) + @Html.ValidationMessage("aliasPath") + @T("The path of the alias e.g., my-blog/my-post") +
+
+ + @Html.TextBox("routePath", null, new { @class = "textMedium" }) + @Html.ValidationMessage("routePath") + @T("The actual route Orchard should call when the path is requested e.g., Blogs/Blog/Item?blogId=18") +
+
+
+
+ + @Html.ActionLink(T("Cancel").ToString(), "Index", new { }, new { @class = "button" }) +
+
+ } diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Views/Admin/Index.cshtml b/src/Orchard.Web/Modules/Orchard.Alias/Views/Admin/Index.cshtml new file mode 100644 index 000000000..2d099cfa2 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Views/Admin/Index.cshtml @@ -0,0 +1,89 @@ +@model AdminIndexViewModel + +@using Orchard.Alias +@using Orchard.Alias.ViewModels +@using Orchard.Utility.Extensions + +@{ + Layout.Title = T("Manage Aliases").Text; + var aliasService = WorkContext.Resolve(); + AdminIndexOptions options = Model.Options; + int index = -1; + + var pageSizes = new List() { 10, 50, 100 }; + var defaultPageSize = WorkContext.CurrentSite.PageSize; + if (!pageSizes.Contains(defaultPageSize)) { + pageSizes.Add(defaultPageSize); + } +} + +@using (Html.BeginFormAntiForgeryPost()) { + @Html.ValidationSummary() +
@Html.ActionLink(T("Add new Alias").Text, "Add", new { returnurl = HttpContext.Current.Request.RawUrl }, new { @class = "button primaryAction" })
+ +
+ + + +
+
+ + + + + + +
+
+ + + + + + + + + + @foreach (var aliasEntry in Model.AliasEntries) { + var alias = aliasEntry.Alias; + index++; + var virtualPathData = aliasService.LookupVirtualPaths(alias.RouteValues.ToRouteValueDictionary(), ViewContext.HttpContext).FirstOrDefault(); + + if (virtualPathData == null) { + continue; + } + + var url = virtualPathData.VirtualPath; + + + + + + + } +
 ↓@T("Alias")@T("Route") 
+ + + + @Html.Link(alias.Path == String.Empty ? "/" : alias.Path, Href("~/" + alias.Path)) + + @Html.Link(url, Href("~/" + url)) + + @Html.ActionLink(T("Edit").Text, "Edit", new { path = alias.Path == String.Empty ? "/" : alias.Path }) + | + @Html.ActionLink(T("Delete").Text, "Delete", new { path = alias.Path }, new { itemprop = "UnsafeUrl RemoveUrl" }) +
+ + @Display(Model.Pager) +
+} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Views/Web.config b/src/Orchard.Web/Modules/Orchard.Alias/Views/Web.config new file mode 100644 index 000000000..b7d215131 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Views/Web.config @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.Alias/Web.config b/src/Orchard.Web/Modules/Orchard.Alias/Web.config new file mode 100644 index 000000000..5884c5879 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Alias/Web.config @@ -0,0 +1,39 @@ + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +