mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-14 10:54:50 +08:00
Including Orchard.Alias
--HG-- branch : 1.x
This commit is contained in:
21
src/Orchard.Web/Modules/Orchard.Alias/AdminMenu.cs
Normal file
21
src/Orchard.Web/Modules/Orchard.Alias/AdminMenu.cs
Normal file
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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<AliasEntry>(), 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
src/Orchard.Web/Modules/Orchard.Alias/IAliasService.cs
Normal file
31
src/Orchard.Web/Modules/Orchard.Alias/IAliasService.cs
Normal file
@@ -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);
|
||||||
|
/// <summary>
|
||||||
|
/// Delete Alias from a particular source
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="aliasSource"></param>
|
||||||
|
void DeleteBySource(string aliasSource);
|
||||||
|
|
||||||
|
IEnumerable<string> Lookup(RouteValueDictionary routeValues);
|
||||||
|
IEnumerable<string> Lookup(string routePath);
|
||||||
|
|
||||||
|
void Replace(string aliasPath, RouteValueDictionary routeValues, string aliasSource);
|
||||||
|
void Replace(string aliasPath, string routePath, string aliasSource);
|
||||||
|
|
||||||
|
IEnumerable<Tuple<string, RouteValueDictionary>> List();
|
||||||
|
IEnumerable<Tuple<string, RouteValueDictionary,string>> List(string sourceStartsWith);
|
||||||
|
IEnumerable<VirtualPathData> LookupVirtualPaths(RouteValueDictionary routeValues, System.Web.HttpContextBase HttpContext);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -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<string, string> 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; }
|
||||||
|
}
|
||||||
|
}
|
@@ -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<IRouteProvider> _routeProviders;
|
||||||
|
private readonly Lazy<IEnumerable<RouteDescriptor>> _routeDescriptors;
|
||||||
|
|
||||||
|
public DefaultAliasService(
|
||||||
|
IAliasStorage aliasStorage,
|
||||||
|
IEnumerable<IRouteProvider> routeProviders) {
|
||||||
|
_aliasStorage = aliasStorage;
|
||||||
|
_routeProviders = routeProviders;
|
||||||
|
|
||||||
|
_routeDescriptors = new Lazy<IEnumerable<RouteDescriptor>>(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<string> 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<string> Lookup(RouteValueDictionary routeValues) {
|
||||||
|
return List().Where(item => item.Item2.Match(routeValues)).Select(item=>item.Item1).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Tuple<string, RouteValueDictionary>> List() {
|
||||||
|
return _aliasStorage.List().Select(item => Tuple.Create(item.Item1, item.Item3.ToRouteValueDictionary()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Tuple<string, RouteValueDictionary, string>> List(string sourceStartsWith) {
|
||||||
|
return _aliasStorage.List(sourceStartsWith).Select(item => Tuple.Create(item.Item1, item.Item3.ToRouteValueDictionary(), item.Item4));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<VirtualPathData> LookupVirtualPaths(RouteValueDictionary routeValues,HttpContextBase httpContext) {
|
||||||
|
return Utils.LookupVirtualPaths(httpContext, _routeDescriptors.Value, routeValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IDictionary<string, string> ToDictionary(string routePath) {
|
||||||
|
if (routePath == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return Utils.LookupRouteValues(new StubHttpContext(), _routeDescriptors.Value, routePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IDictionary<string, string> ToDictionary(IEnumerable<KeyValuePair<string, object>> routeValues) {
|
||||||
|
if (routeValues == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return routeValues.ToDictionary(kv => kv.Key, kv => Convert.ToString(kv.Value, CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<RouteDescriptor> GetRouteDescriptors() {
|
||||||
|
return _routeProviders
|
||||||
|
.SelectMany(routeProvider => {
|
||||||
|
var routes = new List<RouteDescriptor>();
|
||||||
|
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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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<string, AliasMap>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, AliasMap> _aliasMaps;
|
||||||
|
|
||||||
|
public void SetAliases(IEnumerable<AliasInfo> 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<AliasMap> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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<string, string> RouteValues { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Orchard.Alias.Implementation.Map;
|
||||||
|
|
||||||
|
namespace Orchard.Alias.Implementation.Holder {
|
||||||
|
/// <summary>
|
||||||
|
/// Holds every alias in a tree structure, indexed by area
|
||||||
|
/// </summary>
|
||||||
|
public interface IAliasHolder : ISingletonDependency {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an <see cref="AliasMap"/> for a specific area
|
||||||
|
/// </summary>
|
||||||
|
AliasMap GetMap(string areaName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all <see cref="AliasMap"/> instances
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<AliasMap> GetMaps();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds or updates an alias in the tree
|
||||||
|
/// </summary>
|
||||||
|
void SetAlias(AliasInfo alias);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds or updates a set of aliases in the tree
|
||||||
|
/// </summary>
|
||||||
|
void SetAliases(IEnumerable<AliasInfo> aliases);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes an alias from the tree based on its path
|
||||||
|
/// </summary>
|
||||||
|
void RemoveAlias(AliasInfo aliasInfo);
|
||||||
|
}
|
||||||
|
}
|
@@ -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<string, IDictionary<string, string>> _aliases;
|
||||||
|
private readonly Node _root;
|
||||||
|
|
||||||
|
public AliasMap(string area) {
|
||||||
|
_area = area;
|
||||||
|
_aliases = new ConcurrentDictionary<string, IDictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
_root = new Node();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<AliasInfo> GetAliases() {
|
||||||
|
return _aliases.Select(x => new AliasInfo {Area = _area, Path = x.Key, RouteValues = x.Value});
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetAlias(string virtualPath, out IDictionary<string, string> routeValues) {
|
||||||
|
return _aliases.TryGetValue(virtualPath, out routeValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Tuple<IDictionary<string, object>, string> Locate(RouteValueDictionary routeValues) {
|
||||||
|
return Traverse(_root, routeValues, _area);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an <see cref="AliasInfo"/> to the map
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="info">The <see cref="AliasInfo"/> intance to add</param>
|
||||||
|
public void Insert(AliasInfo info) {
|
||||||
|
if(info == null) {
|
||||||
|
throw new ArgumentNullException();
|
||||||
|
}
|
||||||
|
|
||||||
|
_aliases[info.Path] = info.RouteValues;
|
||||||
|
ExpandTree(_root, info.Path, info.RouteValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes an alias from the map
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="info"></param>
|
||||||
|
public void Remove(AliasInfo info) {
|
||||||
|
IDictionary<string,string> values;
|
||||||
|
_aliases.TryRemove(info.Path, out values);
|
||||||
|
CollapseTree(_root, info.Path, info.RouteValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CollapseTree(Node root, string path, IDictionary<string, string> 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<string, Node>(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<string, string> 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<string, Node>(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<TResult> Product<T1, T2, TResult>(IEnumerable<T1> source1, IEnumerable<T2> source2, Func<T1, T2, TResult> produce) {
|
||||||
|
return from item1 in source1 from item2 in source2 select produce(item1, item2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<T> Single<T>(T t) {
|
||||||
|
yield return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<T> Empty<T>() {
|
||||||
|
return Enumerable.Empty<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Expand the route values into all possible combinations of keys
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="routeValues"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private static IEnumerable<IEnumerable<KeyValuePair<string, string>>> Expand(IDictionary<string, string> routeValues) {
|
||||||
|
var ordered = routeValues.OrderBy(kv => kv.Key, StringComparer.InvariantCultureIgnoreCase);
|
||||||
|
var empty = Empty<KeyValuePair<string, string>>();
|
||||||
|
|
||||||
|
// 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<string, string>(termKey, "\u0000")),
|
||||||
|
Single(new KeyValuePair<string, string>(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<IDictionary<string, object>, string> Traverse(Node focus, RouteValueDictionary routeValues, string areaName) {
|
||||||
|
|
||||||
|
// Initialize a match variable
|
||||||
|
Tuple<IDictionary<string, object>, 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<string, object>)new Dictionary<string, object>(StringComparer.InvariantCultureIgnoreCase), foundPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Node {
|
||||||
|
public Node() {
|
||||||
|
Stems = new ConcurrentDictionary<string, ConcurrentDictionary<string, Node>>(StringComparer.InvariantCultureIgnoreCase);
|
||||||
|
Paths = new ConcurrentDictionary<string, object>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConcurrentDictionary<string, ConcurrentDictionary<string, Node>> Stems { get; set; }
|
||||||
|
public ConcurrentDictionary<string,object> Paths { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -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<string, string> routeValues, string source);
|
||||||
|
IDictionary<string, string> Get(string aliasPath);
|
||||||
|
void Remove(string path);
|
||||||
|
void Remove(string path, string aliasSource);
|
||||||
|
void RemoveBySource(string aliasSource);
|
||||||
|
IEnumerable<Tuple<string, string, IDictionary<string, string>, string>> List();
|
||||||
|
IEnumerable<Tuple<string, string, IDictionary<string, string>, string>> List(string sourceStartsWith);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AliasStorage : IAliasStorage {
|
||||||
|
private readonly IRepository<AliasRecord> _aliasRepository;
|
||||||
|
private readonly IRepository<ActionRecord> _actionRepository;
|
||||||
|
private readonly IAliasHolder _aliasHolder;
|
||||||
|
public AliasStorage(IRepository<AliasRecord> aliasRepository, IRepository<ActionRecord> actionRepository, IAliasHolder aliasHolder) {
|
||||||
|
_aliasRepository = aliasRepository;
|
||||||
|
_actionRepository = actionRepository;
|
||||||
|
_aliasHolder = aliasHolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set(string path, IDictionary<string, string> 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<string, string> 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<Tuple<string, string, IDictionary<string, string>, string>> List() {
|
||||||
|
return _aliasRepository.Table.OrderBy(a => a.Id).Select(ToDictionary).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Tuple<string, string, IDictionary<string, string>, string>> List(string sourceStartsWith) {
|
||||||
|
return _aliasRepository.Table.Where(a => a.Source.StartsWith(sourceStartsWith)).OrderBy(a => a.Id).Select(ToDictionary).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Tuple<string, string, IDictionary<string, string>, string> ToDictionary(AliasRecord aliasRecord) {
|
||||||
|
IDictionary<string, string> routeValues = new Dictionary<string, string>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
172
src/Orchard.Web/Modules/Orchard.Alias/Implementation/Utils.cs
Normal file
172
src/Orchard.Web/Modules/Orchard.Alias/Implementation/Utils.cs
Normal file
@@ -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<string, string> LookupRouteValues
|
||||||
|
(HttpContextBase httpContext, IEnumerable<RouteDescriptor>
|
||||||
|
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<VirtualPathData> LookupVirtualPaths(
|
||||||
|
HttpContextBase httpContext,
|
||||||
|
IEnumerable<RouteDescriptor> 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<VirtualPathData> LookupVirtualPaths(
|
||||||
|
HttpContextBase httpContext,
|
||||||
|
IEnumerable<RouteDescriptor> routeDescriptors,
|
||||||
|
string areaName,
|
||||||
|
IDictionary<string,string> 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<string, string> 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 ""; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
src/Orchard.Web/Modules/Orchard.Alias/Migrations.cs
Normal file
24
src/Orchard.Web/Modules/Orchard.Alias/Migrations.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using Orchard.Data.Migration;
|
||||||
|
|
||||||
|
namespace Orchard.Alias {
|
||||||
|
public class Migrations : DataMigrationImpl {
|
||||||
|
public int Create() {
|
||||||
|
SchemaBuilder
|
||||||
|
.CreateTable("AliasRecord",
|
||||||
|
table => table
|
||||||
|
.Column<int>("Id", column => column.PrimaryKey().Identity())
|
||||||
|
.Column<string>("Path", c => c.WithLength(2048))
|
||||||
|
.Column<int>("Action_id")
|
||||||
|
.Column<string>("RouteValues", c => c.Unlimited())
|
||||||
|
.Column<string>("Source", c => c.WithLength(256)))
|
||||||
|
.CreateTable("ActionRecord",
|
||||||
|
table => table
|
||||||
|
.Column<int>("Id", column => column.PrimaryKey().Identity())
|
||||||
|
.Column<string>("Area")
|
||||||
|
.Column<string>("Controller")
|
||||||
|
.Column<string>("Action"));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
15
src/Orchard.Web/Modules/Orchard.Alias/Module.txt
Normal file
15
src/Orchard.Web/Modules/Orchard.Alias/Module.txt
Normal file
@@ -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
|
147
src/Orchard.Web/Modules/Orchard.Alias/Orchard.Alias.csproj
Normal file
147
src/Orchard.Web/Modules/Orchard.Alias/Orchard.Alias.csproj
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProductVersion>9.0.30729</ProductVersion>
|
||||||
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
|
<ProjectGuid>{475B6C45-B27C-438B-8966-908B9D6D1077}</ProjectGuid>
|
||||||
|
<ProjectTypeGuids>{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||||
|
<RootNamespace>Orchard.Alias</RootNamespace>
|
||||||
|
<AssemblyName>Orchard.Alias</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
|
||||||
|
<MvcBuildViews>false</MvcBuildViews>
|
||||||
|
<FileUpgradeFlags>
|
||||||
|
</FileUpgradeFlags>
|
||||||
|
<OldToolsVersion>4.0</OldToolsVersion>
|
||||||
|
<UpgradeBackupLocation>
|
||||||
|
</UpgradeBackupLocation>
|
||||||
|
<TargetFrameworkProfile />
|
||||||
|
<UseIISExpress>false</UseIISExpress>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>bin\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.CSharp" />
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.Data" />
|
||||||
|
<Reference Include="System.ComponentModel.DataAnnotations">
|
||||||
|
<RequiredTargetFramework>3.5</RequiredTargetFramework>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Web.DynamicData" />
|
||||||
|
<Reference Include="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||||
|
<SpecificVersion>False</SpecificVersion>
|
||||||
|
<HintPath>..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Web" />
|
||||||
|
<Reference Include="System.Web.Abstractions" />
|
||||||
|
<Reference Include="System.Web.Routing" />
|
||||||
|
<Reference Include="System.Xml" />
|
||||||
|
<Reference Include="System.Configuration" />
|
||||||
|
<Reference Include="System.Xml.Linq" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Views\Admin\Add.cshtml" />
|
||||||
|
<Content Include="Views\Admin\Delete.cshtml" />
|
||||||
|
<Content Include="Views\Admin\Edit.cshtml" />
|
||||||
|
<Content Include="Views\Admin\Index.cshtml" />
|
||||||
|
<Content Include="Web.config" />
|
||||||
|
<Content Include="Views\Web.config" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
<Content Include="Module.txt" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\Orchard\Orchard.Framework.csproj">
|
||||||
|
<Project>{2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}</Project>
|
||||||
|
<Name>Orchard.Framework</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
<ProjectReference Include="..\..\Core\Orchard.Core.csproj">
|
||||||
|
<Project>{9916839C-39FC-4CEB-A5AF-89CA7E87119F}</Project>
|
||||||
|
<Name>Orchard.Core</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="AdminMenu.cs" />
|
||||||
|
<Compile Include="Implementation\AliasRoute.cs" />
|
||||||
|
<Compile Include="Controllers\AdminController.cs" />
|
||||||
|
<Compile Include="Implementation\DefaultAliasService.cs" />
|
||||||
|
<Compile Include="IAliasService.cs" />
|
||||||
|
<Compile Include="Implementation\Holder\AliasInfo.cs" />
|
||||||
|
<Compile Include="Implementation\Holder\AliasHolder.cs" />
|
||||||
|
<Compile Include="Implementation\Holder\IAliasHolder.cs" />
|
||||||
|
<Compile Include="Implementation\Map\AliasMap.cs" />
|
||||||
|
<Compile Include="Implementation\Storage\AliasStorage.cs" />
|
||||||
|
<Compile Include="Implementation\Updater\AliasUpdater.cs" />
|
||||||
|
<Compile Include="Implementation\Utils.cs" />
|
||||||
|
<Compile Include="Migrations.cs" />
|
||||||
|
<Compile Include="Records\ActionRecord.cs" />
|
||||||
|
<Compile Include="Records\AliasRecord.cs" />
|
||||||
|
<Compile Include="Routes.cs" />
|
||||||
|
<Compile Include="ViewModels\AdminIndexViewModel.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup />
|
||||||
|
<PropertyGroup>
|
||||||
|
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
|
||||||
|
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
|
||||||
|
<Import Project="$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||||
|
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" Condition="false" />
|
||||||
|
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||||
|
Other similar extension points exist, see Microsoft.Common.targets.
|
||||||
|
<Target Name="BeforeBuild">
|
||||||
|
</Target> -->
|
||||||
|
<Target Name="AfterBuild" DependsOnTargets="AfterBuildCompiler">
|
||||||
|
<PropertyGroup>
|
||||||
|
<AreasManifestDir>$(ProjectDir)\..\Manifests</AreasManifestDir>
|
||||||
|
</PropertyGroup>
|
||||||
|
<!-- If this is an area child project, uncomment the following line:
|
||||||
|
<CreateAreaManifest AreaName="$(AssemblyName)" AreaType="Child" AreaPath="$(ProjectDir)" ManifestPath="$(AreasManifestDir)" ContentFiles="@(Content)" />
|
||||||
|
-->
|
||||||
|
<!-- If this is an area parent project, uncomment the following lines:
|
||||||
|
<CreateAreaManifest AreaName="$(AssemblyName)" AreaType="Parent" AreaPath="$(ProjectDir)" ManifestPath="$(AreasManifestDir)" ContentFiles="@(Content)" />
|
||||||
|
<CopyAreaManifests ManifestPath="$(AreasManifestDir)" CrossCopy="false" RenameViews="true" />
|
||||||
|
-->
|
||||||
|
</Target>
|
||||||
|
<Target Name="AfterBuildCompiler" Condition="'$(MvcBuildViews)'=='true'">
|
||||||
|
<AspNetCompiler VirtualPath="temp" PhysicalPath="$(ProjectDir)\..\$(ProjectName)" />
|
||||||
|
</Target>
|
||||||
|
<ProjectExtensions>
|
||||||
|
<VisualStudio>
|
||||||
|
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
|
||||||
|
<WebProjectProperties>
|
||||||
|
<UseIIS>False</UseIIS>
|
||||||
|
<AutoAssignPort>True</AutoAssignPort>
|
||||||
|
<DevelopmentServerPort>45979</DevelopmentServerPort>
|
||||||
|
<DevelopmentServerVPath>/</DevelopmentServerVPath>
|
||||||
|
<IISUrl>
|
||||||
|
</IISUrl>
|
||||||
|
<NTLMAuthentication>False</NTLMAuthentication>
|
||||||
|
<UseCustomServer>True</UseCustomServer>
|
||||||
|
<CustomServerUrl>http://orchard.codeplex.com</CustomServerUrl>
|
||||||
|
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
|
||||||
|
</WebProjectProperties>
|
||||||
|
</FlavorProperties>
|
||||||
|
</VisualStudio>
|
||||||
|
</ProjectExtensions>
|
||||||
|
</Project>
|
@@ -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 <20> 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")]
|
||||||
|
|
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
37
src/Orchard.Web/Modules/Orchard.Alias/Routes.cs
Normal file
37
src/Orchard.Web/Modules/Orchard.Alias/Routes.cs
Normal file
@@ -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<RouteDescriptor> routes) {
|
||||||
|
foreach (RouteDescriptor routeDescriptor in GetRoutes()) {
|
||||||
|
routes.Add(routeDescriptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<RouteDescriptor> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Orchard.Alias.Implementation.Holder;
|
||||||
|
|
||||||
|
namespace Orchard.Alias.ViewModels {
|
||||||
|
|
||||||
|
public class AdminIndexViewModel {
|
||||||
|
public IList<AliasEntry> 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
|
||||||
|
}
|
||||||
|
}
|
28
src/Orchard.Web/Modules/Orchard.Alias/Views/Admin/Add.cshtml
Normal file
28
src/Orchard.Web/Modules/Orchard.Alias/Views/Admin/Add.cshtml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
@{
|
||||||
|
Layout.Title = T("Create Alias");
|
||||||
|
}
|
||||||
|
|
||||||
|
@using (Html.BeginFormAntiForgeryPost()) {
|
||||||
|
@Html.ValidationSummary()
|
||||||
|
<fieldset>
|
||||||
|
<legend>@T("Create Alias")</legend>
|
||||||
|
<div>
|
||||||
|
<label for="aliasPath">@T("Alias Path")</label>
|
||||||
|
@Html.TextBox("aliasPath", (object)ViewBag.Path, new { @class = "large text" })
|
||||||
|
@Html.ValidationMessage("aliasPath")
|
||||||
|
<span class="hint">@T("The path of the alias e.g., my-blog/my-post")</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="routePath">@T("Route Path")</label>
|
||||||
|
@Html.TextBox("routePath", (object)ViewBag.Route, new { @class = "textMedium" })
|
||||||
|
@Html.ValidationMessage("routePath")
|
||||||
|
<span class="hint">@T("The actual route Orchard should call when the path is requested e.g., Blogs/Blog/Item?blogId=18")</span>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<div>
|
||||||
|
<button class="primaryAction" type="submit">@T("Save")</button>
|
||||||
|
@Html.ActionLink(T("Cancel").ToString(), "Index", new { }, new { @class = "button" })
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
@{
|
||||||
|
Layout.Title = T("Delete Alias");
|
||||||
|
}
|
||||||
|
|
||||||
|
@using (Html.BeginFormAntiForgeryPost()) {
|
||||||
|
<fieldset>
|
||||||
|
<legend>@T("Delete alias")</legend>
|
||||||
|
<p>@T("Removing alias '{0}'. Are you sure?", ViewBag.Path)</p>
|
||||||
|
@Html.Hidden("path")
|
||||||
|
@Html.Hidden("confirmed", true)
|
||||||
|
<button class="primaryAction" type="submit">@T("Yes")</button>
|
||||||
|
</fieldset>
|
||||||
|
}
|
@@ -0,0 +1,28 @@
|
|||||||
|
@{
|
||||||
|
Layout.Title = T("Edit Alias");
|
||||||
|
}
|
||||||
|
|
||||||
|
@using (Html.BeginFormAntiForgeryPost()) {
|
||||||
|
@Html.ValidationSummary()
|
||||||
|
<fieldset>
|
||||||
|
<legend>@T("Edit alias")</legend>
|
||||||
|
<div>
|
||||||
|
<label for="aliasPath">@T("Alias Path")</label>
|
||||||
|
@Html.TextBox("aliasPath", null, new { @class = "large text" })
|
||||||
|
@Html.ValidationMessage("aliasPath")
|
||||||
|
<span class="hint">@T("The path of the alias e.g., my-blog/my-post")</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="routePath">@T("Route Path")</label>
|
||||||
|
@Html.TextBox("routePath", null, new { @class = "textMedium" })
|
||||||
|
@Html.ValidationMessage("routePath")
|
||||||
|
<span class="hint">@T("The actual route Orchard should call when the path is requested e.g., Blogs/Blog/Item?blogId=18")</span>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<div>
|
||||||
|
<button class="primaryAction" type="submit">@T("Save")</button>
|
||||||
|
@Html.ActionLink(T("Cancel").ToString(), "Index", new { }, new { @class = "button" })
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
}
|
@@ -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<IAliasService>();
|
||||||
|
AdminIndexOptions options = Model.Options;
|
||||||
|
int index = -1;
|
||||||
|
|
||||||
|
var pageSizes = new List<int?>() { 10, 50, 100 };
|
||||||
|
var defaultPageSize = WorkContext.CurrentSite.PageSize;
|
||||||
|
if (!pageSizes.Contains(defaultPageSize)) {
|
||||||
|
pageSizes.Add(defaultPageSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@using (Html.BeginFormAntiForgeryPost()) {
|
||||||
|
@Html.ValidationSummary()
|
||||||
|
<div class="manage">@Html.ActionLink(T("Add new Alias").Text, "Add", new { returnurl = HttpContext.Current.Request.RawUrl }, new { @class = "button primaryAction" })</div>
|
||||||
|
|
||||||
|
<fieldset class="bulk-actions">
|
||||||
|
<label for="publishActions">@T("Actions:")</label>
|
||||||
|
<select id="publishActions" name="@Html.NameOf(m => m.Options.BulkAction)">
|
||||||
|
@Html.SelectOption(options.BulkAction, AliasBulkAction.None, T("Choose action...").ToString())
|
||||||
|
@Html.SelectOption(options.BulkAction, AliasBulkAction.Delete, T("Delete").ToString())
|
||||||
|
</select>
|
||||||
|
<button type="submit" name="submit.BulkEdit" value="@T("Apply")">@T("Apply")</button>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="bulk-actions">
|
||||||
|
<label for="filterResults">@T("Sort by:")</label>
|
||||||
|
<select id="filterResults" name="@Html.NameOf(m => m.Options.Order)">
|
||||||
|
@Html.SelectOption(options.Order, AliasOrder.Path, T("Path").ToString())
|
||||||
|
</select>
|
||||||
|
<input type="hidden" name="Page" value="1" />
|
||||||
|
<label for="pageSize">@T("Show:")</label>
|
||||||
|
<select id="pageSize" name="PageSize">
|
||||||
|
@Html.SelectOption((int)Model.Pager.PageSize, 0, T("All").ToString())
|
||||||
|
@foreach(int size in pageSizes.OrderBy(p => p)) {
|
||||||
|
@Html.SelectOption((int)Model.Pager.PageSize, size, size.ToString())
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<button type="submit" name="submit.Filter" value="@T("Filter")">@T("Filter")</button>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<table class="items">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="checkbox"> ↓</th>
|
||||||
|
<th scope="col">@T("Alias")</th>
|
||||||
|
<th scope="col">@T("Route")</th>
|
||||||
|
<th scope="col"> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
@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;
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="hidden" value="@alias.Path" name="@Html.FieldNameFor(m => Model.AliasEntries[index].Alias.Path)"/>
|
||||||
|
<input type="checkbox" value="true" name="@Html.FieldNameFor(m => Model.AliasEntries[index].IsChecked)"/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@Html.Link(alias.Path == String.Empty ? "/" : alias.Path, Href("~/" + alias.Path))
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@Html.Link(url, Href("~/" + url))
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@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" })
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
@Display(Model.Pager)
|
||||||
|
</fieldset>
|
||||||
|
}
|
41
src/Orchard.Web/Modules/Orchard.Alias/Views/Web.config
Normal file
41
src/Orchard.Web/Modules/Orchard.Alias/Views/Web.config
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<configuration>
|
||||||
|
<appSettings>
|
||||||
|
<add key="webpages:Enabled" value="false" />
|
||||||
|
</appSettings>
|
||||||
|
<system.web>
|
||||||
|
<httpHandlers>
|
||||||
|
</httpHandlers>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Enabling request validation in view pages would cause validation to occur
|
||||||
|
after the input has already been processed by the controller. By default
|
||||||
|
MVC performs request validation before a controller processes the input.
|
||||||
|
To change this behavior apply the ValidateInputAttribute to a
|
||||||
|
controller or action.
|
||||||
|
-->
|
||||||
|
<pages
|
||||||
|
validateRequest="false"
|
||||||
|
pageParserFilterType="System.Web.Mvc.ViewTypeParserFilter, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"
|
||||||
|
pageBaseType="System.Web.Mvc.ViewPage, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"
|
||||||
|
userControlBaseType="System.Web.Mvc.ViewUserControl, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||||
|
<controls>
|
||||||
|
<add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" namespace="System.Web.Mvc" tagPrefix="mvc" />
|
||||||
|
</controls>
|
||||||
|
</pages>
|
||||||
|
</system.web>
|
||||||
|
|
||||||
|
<system.webServer>
|
||||||
|
<validation validateIntegratedModeConfiguration="false"/>
|
||||||
|
<handlers>
|
||||||
|
</handlers>
|
||||||
|
</system.webServer>
|
||||||
|
<runtime>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
|
||||||
|
<bindingRedirect oldVersion="2.0.0.0" newVersion="3.0.0.0" />
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
</runtime>
|
||||||
|
</configuration>
|
39
src/Orchard.Web/Modules/Orchard.Alias/Web.config
Normal file
39
src/Orchard.Web/Modules/Orchard.Alias/Web.config
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<configuration>
|
||||||
|
|
||||||
|
<configSections>
|
||||||
|
<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
|
||||||
|
<remove name="host" />
|
||||||
|
<remove name="pages" />
|
||||||
|
<section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
|
||||||
|
<section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
|
||||||
|
</sectionGroup>
|
||||||
|
</configSections>
|
||||||
|
|
||||||
|
<system.web.webPages.razor>
|
||||||
|
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
|
||||||
|
<pages pageBaseType="Orchard.Mvc.ViewEngines.Razor.WebViewPage">
|
||||||
|
<namespaces>
|
||||||
|
<add namespace="System.Web.Mvc" />
|
||||||
|
<add namespace="System.Web.Mvc.Ajax" />
|
||||||
|
<add namespace="System.Web.Mvc.Html" />
|
||||||
|
<add namespace="System.Web.Routing" />
|
||||||
|
<add namespace="System.Linq"/>
|
||||||
|
<add namespace="System.Collections.Generic"/>
|
||||||
|
<add namespace="Orchard.Mvc.Html"/>
|
||||||
|
</namespaces>
|
||||||
|
</pages>
|
||||||
|
</system.web.webPages.razor>
|
||||||
|
|
||||||
|
<system.web>
|
||||||
|
<compilation targetFramework="4.0">
|
||||||
|
<assemblies>
|
||||||
|
<add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
|
||||||
|
<add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
|
||||||
|
<add assembly="System.Data.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
|
||||||
|
<add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
|
||||||
|
</assemblies>
|
||||||
|
</compilation>
|
||||||
|
</system.web>
|
||||||
|
|
||||||
|
</configuration>
|
Reference in New Issue
Block a user