mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-12-03 03:58:13 +08:00
Remove Routable module
--HG-- branch : autoroute
This commit is contained in:
@@ -1,315 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Autofac;
|
||||
using JetBrains.Annotations;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ContentManagement.Aspects;
|
||||
using Orchard.ContentManagement.Handlers;
|
||||
using Orchard.ContentManagement.MetaData;
|
||||
using Orchard.ContentManagement.Records;
|
||||
using Orchard.Core.Common.Models;
|
||||
using Orchard.Core.Routable;
|
||||
using Orchard.Core.Routable.Handlers;
|
||||
using Orchard.Core.Routable.Models;
|
||||
using Orchard.Core.Routable.Services;
|
||||
using Orchard.Data;
|
||||
using Orchard.DisplayManagement;
|
||||
using Orchard.DisplayManagement.Descriptors;
|
||||
using Orchard.DisplayManagement.Implementation;
|
||||
using Orchard.Environment;
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.Mvc;
|
||||
using Orchard.Security;
|
||||
using Orchard.Tests.Modules;
|
||||
using System.Web.Mvc;
|
||||
using System.Web.Routing;
|
||||
using Orchard.Tests.Stubs;
|
||||
using Orchard.UI.Notify;
|
||||
|
||||
namespace Orchard.Core.Tests.Routable.Services {
|
||||
|
||||
// TODO: (PH)
|
||||
[TestFixture]
|
||||
public class RoutableServiceTests : DatabaseEnabledTestsBase {
|
||||
[SetUp]
|
||||
public override void Init() {
|
||||
base.Init();
|
||||
_routableService = _container.Resolve<IRoutableService>();
|
||||
_contentManager = _container.Resolve<IContentManager>();
|
||||
}
|
||||
|
||||
public override void Register(ContainerBuilder builder) {
|
||||
builder.RegisterType<DefaultContentManager>().As<IContentManager>();
|
||||
builder.RegisterType<DefaultContentManagerSession>().As<IContentManagerSession>();
|
||||
builder.RegisterInstance(new Mock<IContentDefinitionManager>().Object);
|
||||
builder.RegisterInstance(new Mock<ITransactionManager>().Object);
|
||||
builder.RegisterInstance(new Mock<IAuthorizer>().Object);
|
||||
builder.RegisterInstance(new Mock<INotifier>().Object);
|
||||
builder.RegisterInstance(new Mock<IContentDisplay>().Object);
|
||||
builder.RegisterType<StubHttpContextAccessor>().As<IHttpContextAccessor>();
|
||||
builder.RegisterType<WorkContextAccessor>().As<IWorkContextAccessor>();
|
||||
builder.RegisterType<OrchardServices>().As<IOrchardServices>();
|
||||
|
||||
builder.RegisterType<ThingHandler>().As<IContentHandler>();
|
||||
builder.RegisterType<StuffHandler>().As<IContentHandler>();
|
||||
builder.RegisterType<RoutableService>().As<IRoutableService>();
|
||||
builder.RegisterType<RoutablePathConstraint>().As<IRoutablePathConstraint>();
|
||||
builder.RegisterType<DefaultShapeTableManager>().As<IShapeTableManager>();
|
||||
builder.RegisterType<ShapeTableLocator>().As<IShapeTableLocator>();
|
||||
builder.RegisterType<DefaultShapeFactory>().As<IShapeFactory>();
|
||||
|
||||
builder.RegisterType<DefaultContentQuery>().As<IContentQuery>();
|
||||
builder.RegisterInstance(new UrlHelper(new RequestContext(new StubHttpContext("~/"), new RouteData()))).As<UrlHelper>();
|
||||
builder.RegisterType<RoutePartHandler>().As<IContentHandler>();
|
||||
|
||||
builder.RegisterType<StubExtensionManager>().As<IExtensionManager>();
|
||||
builder.RegisterType<DefaultContentDisplay>().As<IContentDisplay>();
|
||||
}
|
||||
|
||||
private IRoutableService _routableService;
|
||||
private IContentManager _contentManager;
|
||||
|
||||
[Test]
|
||||
public void InvalidCharactersShouldBeReplacedByADash() {
|
||||
var thing = _contentManager.Create<Thing>("thing", t => {
|
||||
t.As<RoutePart>().Record = new RoutePartRecord();
|
||||
t.Title = "Please do not use any of the following characters in your permalink: \":\", \"?\", \"#\", \"[\", \"]\", \"@\", \"!\", \"$\", \"&\", \"'\", \"(\", \")\", \"*\", \"+\", \",\", \";\", \"=\", \"\"\", \"<\", \">\", \"\\\"";
|
||||
});
|
||||
|
||||
_routableService.FillSlugFromTitle(thing.As<RoutePart>());
|
||||
|
||||
Assert.That(thing.Slug, Is.EqualTo("please-do-not-use-any-of-the-following-characters-in-your-permalink"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SpacesSlugShouldBeTreatedAsEmpty() {
|
||||
var contentManager = _container.Resolve<IContentManager>();
|
||||
|
||||
var thing = contentManager.Create<Thing>("thing", t => {
|
||||
t.As<RoutePart>().Record = new RoutePartRecord();
|
||||
t.Title = "My Title";
|
||||
t.Slug = " ";
|
||||
});
|
||||
|
||||
_routableService.FillSlugFromTitle(thing.As<RoutePart>());
|
||||
|
||||
Assert.That(thing.Slug, Is.EqualTo("my-title"));
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void SlashInSlugIsAllowed() {
|
||||
Assert.That(_routableService.IsSlugValid("some/page"), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DotsAroundSlugAreAllowed() {
|
||||
Assert.That(_routableService.IsSlugValid(".slug"), Is.False);
|
||||
Assert.That(_routableService.IsSlugValid("slug."), Is.False);
|
||||
Assert.That(_routableService.IsSlugValid("slug.slug"), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EmptySlugsShouldBeConsideredValid() {
|
||||
// so that automatic generation on Publish occurs
|
||||
Assert.That(_routableService.IsSlugValid(null), Is.True);
|
||||
Assert.That(_routableService.IsSlugValid(String.Empty), Is.True);
|
||||
Assert.That(_routableService.IsSlugValid(" "), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InvalidCharacterShouldBeRefusedInSlugs() {
|
||||
Assert.That(_routableService.IsSlugValid("aaaa-_aaaa"), Is.True);
|
||||
|
||||
foreach (var c in @":?#[]@!$&'()*+,;= \") {
|
||||
Assert.That(_routableService.IsSlugValid("a" + c + "b"), Is.False);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VeryLongStringTruncatedTo1000Chars() {
|
||||
var veryVeryLongTitle = "this is a very long title...";
|
||||
for (var i = 0; i < 100; i++)
|
||||
veryVeryLongTitle += "aaaaaaaaaa";
|
||||
|
||||
var thing = CreateRoutePartFromScratch(veryVeryLongTitle);
|
||||
_routableService.FillSlugFromTitle(thing);
|
||||
|
||||
Assert.That(veryVeryLongTitle.Length, Is.AtLeast(1001));
|
||||
Assert.That(thing.Slug.Length, Is.EqualTo(1000));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NoExistingLikeSlugsGeneratesSameSlug() {
|
||||
string slug = _routableService.GenerateUniqueSlug(CreateRoutePartFromScratch("woohoo"), null);
|
||||
Assert.That(slug, Is.EqualTo("woohoo"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExistingSingleLikeSlugThatsAConflictGeneratesADash2() {
|
||||
string slug = _routableService.GenerateUniqueSlug(CreateRoutePartFromScratch("woohoo"), new List<string> { "woohoo" });
|
||||
Assert.That(slug, Is.EqualTo("woohoo-2"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExistingSingleLikeSlugThatsNotAConflictGeneratesSameSlug() {
|
||||
string slug = _routableService.GenerateUniqueSlug(CreateRoutePartFromScratch("woohoo"), new List<string> { "woohoo-2" });
|
||||
Assert.That(slug, Is.EqualTo("woohoo"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExistingLikeSlugsWithAConflictGeneratesADashVNext() {
|
||||
string slug = _routableService.GenerateUniqueSlug(CreateRoutePartFromScratch("woohoo"), new List<string> { "woohoo", "woohoo-2" });
|
||||
Assert.That(slug, Is.EqualTo("woohoo-3"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExistingSlugsWithVersionGapsAndNoMatchGeneratesSameSlug() {
|
||||
string slug = _routableService.GenerateUniqueSlug(CreateRoutePartFromScratch("woohoo"), new List<string> { "woohoo-2", "woohoo-4", "woohoo-5" });
|
||||
Assert.That(slug, Is.EqualTo("woohoo"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExistingSlugsWithVersionGapsAndAMatchGeneratesADash2() {
|
||||
string slug = _routableService.GenerateUniqueSlug(CreateRoutePartFromScratch("woohoo-2"), new List<string> { "woohoo-2", "woohoo-4", "woohoo-5" });
|
||||
Assert.That(slug, Is.EqualTo("woohoo-2-2"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SlugIsGeneratedLowerCased() {
|
||||
var thing = CreateRoutePartFromScratch("This Is Some Interesting Title");
|
||||
_routableService.FillSlugFromTitle(thing);
|
||||
Assert.That(thing.Slug, Is.EqualTo("this-is-some-interesting-title"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SlugInConflictWithAnExistingItemsPathIsVersioned() {
|
||||
CreateRoutePartFromScratch("bar", "bar", "foo");
|
||||
_contentManager.Flush();
|
||||
|
||||
var thing2 = CreateRoutePartFromScratch("fooslashbar", "foo/bar");
|
||||
Assert.That(thing2.Path, Is.EqualTo("foo/bar-2"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GeneratedSlugInConflictInSameContaierPathIsVersioned() {
|
||||
var thing1 = CreateRoutePartFromScratch("Foo", "", "bar");
|
||||
_contentManager.Flush();
|
||||
var thing2 = CreateRoutePartWithExistingContainer("Foo", thing1.As<ICommonPart>().Container);
|
||||
Assert.That(thing2.Path, Is.EqualTo("bar/foo-2"));
|
||||
Assert.That(thing2.Slug, Is.EqualTo("foo-2"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenSlugInConflictInSameContaierPathIsVersioned() {
|
||||
var thing1 = CreateRoutePartFromScratch("Hi", "foo", "bar");
|
||||
_contentManager.Flush();
|
||||
var thing2 = CreateRoutePartWithExistingContainer("There", thing1.As<ICommonPart>().Container, "foo");
|
||||
Assert.That(thing2.Path, Is.EqualTo("bar/foo-2"));
|
||||
Assert.That(thing2.Slug, Is.EqualTo("foo-2"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GeneratedSlugInConflictInDifferentContaierPathIsNotVersioned() {
|
||||
var thing1 = CreateRoutePartFromScratch("Foo", "", "rab");
|
||||
var thing2 = CreateRoutePartFromScratch("Foo", "", "bar");
|
||||
Assert.That(thing1.Path, Is.EqualTo("rab/foo"));
|
||||
Assert.That(thing2.Path, Is.EqualTo("bar/foo"));
|
||||
Assert.That(thing1.Slug, Is.EqualTo("foo"));
|
||||
Assert.That(thing2.Slug, Is.EqualTo("foo"));
|
||||
}
|
||||
|
||||
private RoutePart CreateRoutePartWithExistingContainer(string title, IContent container, string slug = "") {
|
||||
var contentManager = _container.Resolve<IContentManager>();
|
||||
return contentManager.Create<Thing>("thing", t => {
|
||||
t.As<RoutePart>().Record = new RoutePartRecord();
|
||||
t.Title = title;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(slug))
|
||||
t.As<RoutePart>().Slug = slug;
|
||||
|
||||
if (container != null)
|
||||
t.As<ICommonPart>().Container = container;
|
||||
}).As<RoutePart>();
|
||||
}
|
||||
|
||||
private RoutePart CreateRoutePartFromScratch(string title, string slug = "", string containerPath = "") {
|
||||
var contentManager = _container.Resolve<IContentManager>();
|
||||
return contentManager.Create<Thing>("thing", t => {
|
||||
t.As<RoutePart>().Record = new RoutePartRecord();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(slug))
|
||||
t.As<RoutePart>().Slug = slug;
|
||||
|
||||
t.Title = title;
|
||||
if (!string.IsNullOrWhiteSpace(containerPath)) {
|
||||
t.As<ICommonPart>().Container = contentManager.Create<Thing>("thing", tt => {
|
||||
tt.As<RoutePart>().Path = containerPath;
|
||||
tt.As<RoutePart>().Slug = containerPath;
|
||||
tt.As<RoutePart>().Title = "Test Container";
|
||||
});
|
||||
}
|
||||
}).As<RoutePart>();
|
||||
}
|
||||
|
||||
|
||||
protected override IEnumerable<Type> DatabaseTypes {
|
||||
get {
|
||||
return new[] {
|
||||
typeof(RoutePartRecord),
|
||||
typeof(ContentTypeRecord),
|
||||
typeof(ContentItemRecord),
|
||||
typeof(ContentItemVersionRecord),
|
||||
typeof(CommonPartRecord),
|
||||
typeof(CommonPartVersionRecord),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
public class ThingHandler : ContentHandler {
|
||||
public ThingHandler() {
|
||||
Filters.Add(new ActivatingFilter<Thing>("thing"));
|
||||
Filters.Add(new ActivatingFilter<ContentPart<CommonPartVersionRecord>>("thing"));
|
||||
Filters.Add(new ActivatingFilter<CommonPart>("thing"));
|
||||
Filters.Add(new ActivatingFilter<RoutePart>("thing"));
|
||||
}
|
||||
}
|
||||
|
||||
public class Thing : ContentPart {
|
||||
public string Title {
|
||||
get { return this.As<RoutePart>().Title; }
|
||||
set { this.As<RoutePart>().Title = value; }
|
||||
}
|
||||
|
||||
public string Slug {
|
||||
get { return this.As<RoutePart>().Slug; }
|
||||
set { this.As<RoutePart>().Slug = value; }
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
public class StuffHandler : ContentHandler {
|
||||
public StuffHandler() {
|
||||
Filters.Add(new ActivatingFilter<Stuff>("stuff"));
|
||||
Filters.Add(new ActivatingFilter<ContentPart<CommonPartVersionRecord>>("stuff"));
|
||||
Filters.Add(new ActivatingFilter<CommonPart>("stuff"));
|
||||
Filters.Add(new ActivatingFilter<RoutePart>("stuff"));
|
||||
}
|
||||
}
|
||||
|
||||
public class Stuff : ContentPart {
|
||||
public string Title {
|
||||
get { return this.As<RoutePart>().Title; }
|
||||
set { this.As<RoutePart>().Title = value; }
|
||||
}
|
||||
|
||||
public string Slug {
|
||||
get { return this.As<RoutePart>().Slug; }
|
||||
set { this.As<RoutePart>().Slug = value; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,8 +130,6 @@
|
||||
<Compile Include="Navigation\Services\AdminMenuNavigationProvider.cs" />
|
||||
<Compile Include="Navigation\Services\MainMenuNavigationProvider.cs" />
|
||||
<Compile Include="Navigation\Settings\AdminMenuPartTypeSettings.cs" />
|
||||
<Compile Include="Routable\Events\ISlugEventHandler.cs" />
|
||||
<Compile Include="Routable\ResourceManifest.cs" />
|
||||
<Compile Include="Contents\ViewModels\ListContentsViewModel.cs" />
|
||||
<Compile Include="Contents\ViewModels\ListContentTypesViewModel.cs" />
|
||||
<Compile Include="Reports\AdminMenu.cs" />
|
||||
@@ -140,12 +138,6 @@
|
||||
<Compile Include="Navigation\Migrations.cs" />
|
||||
<Compile Include="Reports\ViewModels\DisplayReportViewModel.cs" />
|
||||
<Compile Include="Reports\ViewModels\ReportsAdminIndexViewModel.cs" />
|
||||
<Compile Include="Routable\Controllers\ItemController.cs" />
|
||||
<Compile Include="Routable\Migrations.cs" />
|
||||
<Compile Include="Routable\Drivers\RoutePartDriver.cs" />
|
||||
<Compile Include="Routable\Handlers\RoutePartHandler.cs" />
|
||||
<Compile Include="Routable\IRoutablePathConstraint.cs" />
|
||||
<Compile Include="Routable\Models\RoutePart.cs" />
|
||||
<Compile Include="Common\Utilities\LazyField.cs" />
|
||||
<Compile Include="Common\Handlers\CommonPartHandler.cs" />
|
||||
<Compile Include="Common\Models\CommonPart.cs" />
|
||||
@@ -197,14 +189,6 @@
|
||||
<Compile Include="Navigation\ViewModels\MenuItemEntry.cs" />
|
||||
<Compile Include="Navigation\ViewModels\NavigationManagementViewModel.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Routable\Models\RoutePartRecord.cs" />
|
||||
<Compile Include="Routable\Routes.cs" />
|
||||
<Compile Include="Routable\Services\IRoutableService.cs" />
|
||||
<Compile Include="Routable\Services\RoutablePathConstraint.cs" />
|
||||
<Compile Include="Routable\Services\RoutablePathConstraintUpdator.cs" />
|
||||
<Compile Include="Routable\Services\RoutableService.cs" />
|
||||
<Compile Include="Routable\ViewModels\RoutableEditorViewModel.cs" />
|
||||
<Compile Include="Routable\ViewModels\RoutableDisplayViewModel.cs" />
|
||||
<Compile Include="Scheduling\Migrations.cs" />
|
||||
<Compile Include="Scheduling\Models\ScheduledTaskRecord.cs" />
|
||||
<Compile Include="Scheduling\Services\ScheduledTaskManager.cs" />
|
||||
@@ -294,9 +278,6 @@
|
||||
<Content Include="Reports\Styles\menu.reports-admin.css" />
|
||||
<Content Include="Reports\Views\Admin\Display.cshtml" />
|
||||
<Content Include="Reports\Views\Admin\Index.cshtml" />
|
||||
<Content Include="Routable\Module.txt" />
|
||||
<Content Include="Routable\Scripts\jquery.slugify.js" />
|
||||
<Content Include="Routable\Views\EditorTemplates\Parts.Routable.RoutePart.cshtml" />
|
||||
<Content Include="Settings\Module.txt" />
|
||||
<Content Include="Settings\Styles\admin.css" />
|
||||
<Content Include="Settings\Styles\images\menu.settings.png" />
|
||||
@@ -304,9 +285,6 @@
|
||||
<Content Include="Settings\Views\Admin\Index.cshtml" />
|
||||
<Content Include="Settings\Views\Admin\Culture.cshtml" />
|
||||
<Content Include="Contents\Views\Content.Edit.cshtml" />
|
||||
<Content Include="Routable\Placement.info">
|
||||
<SubType>Designer</SubType>
|
||||
</Content>
|
||||
<Content Include="Settings\Placement.info" />
|
||||
<Content Include="Settings\Views\DisplayTemplates\CurrentCulture.cshtml" />
|
||||
<Content Include="Settings\Views\DisplayTemplates\RemovableCulture.cshtml" />
|
||||
@@ -369,7 +347,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Contents\Views\Web.config" />
|
||||
<Content Include="Routable\Views\Web.config" />
|
||||
<Content Include="Reports\Views\Web.config" />
|
||||
<Content Include="Contents\Views\Content.cshtml" />
|
||||
<Content Include="Contents\Views\Content.SummaryAdmin.cshtml" />
|
||||
@@ -387,7 +364,6 @@
|
||||
</Content>
|
||||
<Content Include="Contents\Views\Content.ControlWrapper.cshtml" />
|
||||
<Content Include="Navigation\Placement.info" />
|
||||
<Content Include="Routable\Views\Parts.RoutableTitle.cshtml" />
|
||||
<Content Include="Contents\Views\Content.Summary.cshtml" />
|
||||
<Content Include="Contents\Views\Content.SaveButton.cshtml" />
|
||||
<Content Include="Contents\Views\Content.PublishButton.cshtml" />
|
||||
@@ -397,9 +373,6 @@
|
||||
<Content Include="Shapes\Styles\Web.config">
|
||||
<SubType>Designer</SubType>
|
||||
</Content>
|
||||
<Content Include="Routable\Scripts\Web.config">
|
||||
<SubType>Designer</SubType>
|
||||
</Content>
|
||||
<Content Include="Settings\Styles\Web.config">
|
||||
<SubType>Designer</SubType>
|
||||
</Content>
|
||||
@@ -411,8 +384,6 @@
|
||||
<Content Include="Containers\Views\EditorTemplates\Containable.cshtml" />
|
||||
<Content Include="Containers\Views\Parts.ContainerWidget.cshtml" />
|
||||
<Content Include="Containers\Views\EditorTemplates\CustomProperties.cshtml" />
|
||||
<Content Include="Routable\Views\Parts.RoutableTitle_Summary.cshtml" />
|
||||
<Content Include="Routable\Views\Parts.RoutableTitle_SummaryAdmin.cshtml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Shapes\Views\ShapeResult\Display.cshtml" />
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
using System.Linq;
|
||||
using System.Web.Mvc;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ContentManagement.Aspects;
|
||||
using Orchard.Core.Contents;
|
||||
using Orchard.Core.Routable.Models;
|
||||
using Orchard.Core.Routable.Services;
|
||||
using Orchard.Data;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Mvc;
|
||||
using Orchard.Themes;
|
||||
|
||||
namespace Orchard.Core.Routable.Controllers {
|
||||
[ValidateInput(false)]
|
||||
public class ItemController : Controller, IUpdateModel {
|
||||
private readonly ITransactionManager _transactionManager;
|
||||
private readonly IRoutablePathConstraint _routablePathConstraint;
|
||||
|
||||
public ItemController(
|
||||
ITransactionManager transactionManager,
|
||||
IRoutablePathConstraint routablePathConstraint,
|
||||
IOrchardServices services
|
||||
) {
|
||||
_transactionManager = transactionManager;
|
||||
_routablePathConstraint = routablePathConstraint;
|
||||
Services = services;
|
||||
T = NullLocalizer.Instance;
|
||||
}
|
||||
|
||||
public Localizer T { get; set; }
|
||||
public IOrchardServices Services { get; private set; }
|
||||
|
||||
[Themed]
|
||||
public ActionResult Display(string path) {
|
||||
var matchedPath = _routablePathConstraint.FindPath(path);
|
||||
|
||||
if (matchedPath == null) {
|
||||
return HttpNotFound(T("Should not have passed path constraint").Text);
|
||||
}
|
||||
|
||||
var hits = Services.ContentManager
|
||||
.Query<RoutePart, RoutePartRecord>(VersionOptions.Published)
|
||||
.Where(r => r.Path == matchedPath)
|
||||
.Slice(0, 2)
|
||||
.ToList();
|
||||
|
||||
if (hits.Count() == 0) {
|
||||
return HttpNotFound(T("Should not have passed path constraint").Text);
|
||||
}
|
||||
|
||||
if (hits.Count() != 1) {
|
||||
return HttpNotFound(T("Ambiguous content").Text);
|
||||
}
|
||||
|
||||
dynamic model = Services.ContentManager.BuildDisplay(hits.Single());
|
||||
return new ShapeResult(this, model);
|
||||
}
|
||||
|
||||
public ActionResult Slugify(string contentType, int? id, int? containerId) {
|
||||
const string slug = "";
|
||||
ContentItem contentItem = null;
|
||||
|
||||
if (string.IsNullOrEmpty(contentType))
|
||||
return Json(slug);
|
||||
|
||||
if (id != null)
|
||||
contentItem = Services.ContentManager.Get((int)id, VersionOptions.Latest);
|
||||
|
||||
if (contentItem == null) {
|
||||
contentItem = Services.ContentManager.Create(contentType, VersionOptions.Draft);
|
||||
|
||||
if (containerId != null) {
|
||||
var containerItem = Services.ContentManager.Get((int)containerId);
|
||||
contentItem.As<ICommonPart>().Container = containerItem;
|
||||
}
|
||||
}
|
||||
|
||||
Services.ContentManager.UpdateEditor(contentItem, this);
|
||||
Services.ContentManager.Publish(contentItem);
|
||||
_transactionManager.Cancel();
|
||||
|
||||
return Json(contentItem.As<IRoutableAspect>().GetEffectiveSlug() ?? slug);
|
||||
}
|
||||
|
||||
|
||||
bool IUpdateModel.TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties) {
|
||||
return TryUpdateModel(model, prefix, includeProperties, excludeProperties);
|
||||
}
|
||||
|
||||
void IUpdateModel.AddModelError(string key, LocalizedString errorMessage) {
|
||||
ModelState.AddModelError(key, errorMessage.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ContentManagement.Aspects;
|
||||
using Orchard.ContentManagement.Drivers;
|
||||
using Orchard.ContentManagement.Handlers;
|
||||
using Orchard.Core.Routable.Models;
|
||||
using Orchard.Core.Routable.Services;
|
||||
using Orchard.Core.Routable.ViewModels;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Mvc;
|
||||
using Orchard.Services;
|
||||
using Orchard.Utility.Extensions;
|
||||
|
||||
namespace Orchard.Core.Routable.Drivers {
|
||||
public class RoutePartDriver : ContentPartDriver<RoutePart> {
|
||||
private readonly IOrchardServices _services;
|
||||
private readonly IRoutableService _routableService;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public RoutePartDriver(IOrchardServices services,
|
||||
IRoutableService routableService,
|
||||
IHttpContextAccessor httpContextAccessor) {
|
||||
_services = services;
|
||||
_routableService = routableService;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
T = NullLocalizer.Instance;
|
||||
}
|
||||
|
||||
private const string TemplateName = "Parts.Routable.RoutePart";
|
||||
|
||||
public Localizer T { get; set; }
|
||||
|
||||
protected override string Prefix {
|
||||
get { return "Routable"; }
|
||||
}
|
||||
|
||||
static int? GetContainerId(IContent item) {
|
||||
var commonPart = item.As<ICommonPart>();
|
||||
if (commonPart != null && commonPart.Container != null) {
|
||||
return commonPart.Container.ContentItem.Id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override DriverResult Display(RoutePart part, string displayType, dynamic shapeHelper) {
|
||||
return Combined(
|
||||
ContentShape("Parts_RoutableTitle",
|
||||
() => shapeHelper.Parts_RoutableTitle(ContentPart: part, Title: part.Title, Path: part.Path)),
|
||||
ContentShape("Parts_RoutableTitle_Summary",
|
||||
() => shapeHelper.Parts_RoutableTitle_Summary(ContentPart: part, Title: part.Title, Path: part.Path)),
|
||||
ContentShape("Parts_RoutableTitle_SummaryAdmin",
|
||||
() => shapeHelper.Parts_RoutableTitle_SummaryAdmin(ContentPart: part, Title: part.Title, Path: part.Path))
|
||||
);
|
||||
}
|
||||
|
||||
protected override DriverResult Editor(RoutePart part, dynamic shapeHelper) {
|
||||
var model = new RoutableEditorViewModel {
|
||||
ContentType = part.ContentItem.ContentType,
|
||||
Id = part.ContentItem.Id,
|
||||
Slug = part.Slug,
|
||||
Title = part.Title,
|
||||
ContainerId = GetContainerId(part),
|
||||
};
|
||||
|
||||
var request = _httpContextAccessor.Current().Request;
|
||||
var containerUrl = new UriBuilder(request.ToRootUrlString()) { Path = (request.ApplicationPath ?? "").TrimEnd('/') + "/" + (part.GetContainerPath() ?? "") };
|
||||
model.ContainerAbsoluteUrl = containerUrl.Uri.ToString().TrimEnd('/');
|
||||
|
||||
return ContentShape("Parts_Routable_Edit",
|
||||
() => shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: model, Prefix: Prefix));
|
||||
}
|
||||
|
||||
protected override DriverResult Editor(RoutePart part, IUpdateModel updater, dynamic shapeHelper) {
|
||||
var model = new RoutableEditorViewModel();
|
||||
updater.TryUpdateModel(model, Prefix, null, null);
|
||||
|
||||
part.Title = model.Title;
|
||||
part.Slug = model.Slug;
|
||||
part.PromoteToHomePage = model.PromoteToHomePage;
|
||||
|
||||
if ( !_routableService.IsSlugValid(part.Slug) ) {
|
||||
var slug = (part.Slug ?? String.Empty);
|
||||
if ( slug.StartsWith(".") || slug.EndsWith(".") )
|
||||
updater.AddModelError("Routable.Slug", T("The \".\" can't be used at either end of the permalink."));
|
||||
else
|
||||
updater.AddModelError("Routable.Slug", T("Please do not use any of the following characters in your permalink: \":\", \"?\", \"#\", \"[\", \"]\", \"@\", \"!\", \"$\", \"&\", \"'\", \"(\", \")\", \"*\", \"+\", \",\", \";\", \"=\", \", \"<\", \">\", \"\\\". No spaces are allowed (please use dashes or underscores instead)."));
|
||||
}
|
||||
|
||||
return Editor(part, shapeHelper);
|
||||
}
|
||||
|
||||
protected override void Importing(RoutePart part, ImportContentContext context) {
|
||||
var title = context.Attribute(part.PartDefinition.Name, "Title");
|
||||
if (title != null) {
|
||||
part.Title = title;
|
||||
}
|
||||
|
||||
var slug = context.Attribute(part.PartDefinition.Name, "Slug");
|
||||
if (slug != null) {
|
||||
part.Slug = slug;
|
||||
}
|
||||
|
||||
var path = context.Attribute(part.PartDefinition.Name, "Path");
|
||||
if (path != null) {
|
||||
part.Path = path;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected override void Exporting(RoutePart part, ExportContentContext context) {
|
||||
context.Element(part.PartDefinition.Name).SetAttributeValue("Title", part.Title);
|
||||
context.Element(part.PartDefinition.Name).SetAttributeValue("Slug", part.Slug);
|
||||
context.Element(part.PartDefinition.Name).SetAttributeValue("Path", part.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using Orchard.Events;
|
||||
|
||||
namespace Orchard.Core.Routable.Events {
|
||||
public interface ISlugEventHandler : IEventHandler {
|
||||
void FillingSlugFromTitle(FillSlugContext context);
|
||||
void FilledSlugFromTitle(FillSlugContext context);
|
||||
}
|
||||
|
||||
public class FillSlugContext {
|
||||
public FillSlugContext(string slug) {
|
||||
Slug = slug;
|
||||
}
|
||||
|
||||
public string Slug { get; set; }
|
||||
public bool Adjusted { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Web.Routing;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ContentManagement.Handlers;
|
||||
using Orchard.Core.Routable.Models;
|
||||
using Orchard.Core.Routable.Services;
|
||||
using Orchard.Data;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Services;
|
||||
using Orchard.UI.Notify;
|
||||
|
||||
namespace Orchard.Core.Routable.Handlers {
|
||||
public class RoutePartHandler : ContentHandler {
|
||||
private readonly IOrchardServices _services;
|
||||
private readonly IRoutablePathConstraint _routablePathConstraint;
|
||||
private readonly IRoutableService _routableService;
|
||||
private readonly IContentManager _contentManager;
|
||||
private readonly IWorkContextAccessor _workContextAccessor;
|
||||
|
||||
public RoutePartHandler(
|
||||
IOrchardServices services,
|
||||
IRepository<RoutePartRecord> repository,
|
||||
IRoutablePathConstraint routablePathConstraint,
|
||||
IRoutableService routableService,
|
||||
IContentManager contentManager,
|
||||
IWorkContextAccessor workContextAccessor) {
|
||||
_services = services;
|
||||
_routablePathConstraint = routablePathConstraint;
|
||||
_routableService = routableService;
|
||||
_contentManager = contentManager;
|
||||
_workContextAccessor = workContextAccessor;
|
||||
T = NullLocalizer.Instance;
|
||||
|
||||
Filters.Add(StorageFilter.For(repository));
|
||||
|
||||
Action<RoutePart> processSlug = (
|
||||
routable => {
|
||||
if (!_routableService.ProcessSlug(routable))
|
||||
_services.Notifier.Warning(T("Permalinks in conflict. \"{0}\" is already set for a previously created {2} so now it has the slug \"{1}\"",
|
||||
routable.Slug, routable.GetEffectiveSlug(), routable.ContentItem.ContentType));
|
||||
});
|
||||
|
||||
OnGetDisplayShape<RoutePart>(SetModelProperties);
|
||||
OnGetEditorShape<RoutePart>(SetModelProperties);
|
||||
OnUpdateEditorShape<RoutePart>(SetModelProperties);
|
||||
|
||||
Action<PublishContentContext, RoutePart> handler = (context, route) => {
|
||||
FinalizePath(route, context, processSlug);
|
||||
};
|
||||
|
||||
OnPublished<RoutePart>(handler);
|
||||
OnUnpublished<RoutePart>(handler);
|
||||
|
||||
OnRemoved<RoutePart>((context, route) => {
|
||||
if (!string.IsNullOrWhiteSpace(route.Path))
|
||||
_routablePathConstraint.RemovePath(route.Path);
|
||||
});
|
||||
|
||||
OnIndexing<RoutePart>((context, part) => context.DocumentIndex.Add("title", part.Record.Title).RemoveTags().Analyze());
|
||||
}
|
||||
|
||||
protected override void GetItemMetadata(GetContentItemMetadataContext context) {
|
||||
var part = context.ContentItem.As<RoutePart>();
|
||||
|
||||
if (part != null) {
|
||||
context.Metadata.Identity.Add("Route.Slug", part.Slug);
|
||||
}
|
||||
}
|
||||
|
||||
private void FinalizePath(RoutePart route, PublishContentContext context, Action<RoutePart> processSlug) {
|
||||
var path = route.Path;
|
||||
route.Path = route.GetPathWithSlug(route.Slug);
|
||||
|
||||
if (context.PublishingItemVersionRecord != null)
|
||||
processSlug(route);
|
||||
|
||||
// if the path has changed by having the slug changed on the way in (e.g. user input) or to avoid conflict
|
||||
// then update and publish all contained items
|
||||
if (path != route.Path) {
|
||||
_routablePathConstraint.RemovePath(path);
|
||||
_routableService.FixContainedPaths(route);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(route.Path))
|
||||
_routablePathConstraint.AddPath(route.Path);
|
||||
}
|
||||
|
||||
private static void SetModelProperties(BuildShapeContext context, RoutePart routable) {
|
||||
var item = context.Shape;
|
||||
item.Title = routable.Title;
|
||||
item.Slug = routable.Slug;
|
||||
item.Path = routable.Path;
|
||||
}
|
||||
|
||||
public Localizer T { get; set; }
|
||||
}
|
||||
|
||||
public class RoutePartHandlerBase : ContentHandlerBase {
|
||||
private readonly IWorkContextAccessor _workContextAccessor;
|
||||
|
||||
public RoutePartHandlerBase(IWorkContextAccessor workContextAccessor) {
|
||||
_workContextAccessor = workContextAccessor;
|
||||
}
|
||||
|
||||
public override void GetContentItemMetadata(GetContentItemMetadataContext context) {
|
||||
var routable = context.ContentItem.As<RoutePart>();
|
||||
|
||||
if (routable == null)
|
||||
return;
|
||||
|
||||
// set the display route values if it hasn't been set or only has been set by the Contents module.
|
||||
// allows other modules to set their own display. probably not common enough to warrant some priority implementation
|
||||
if (context.Metadata.DisplayRouteValues == null || context.Metadata.DisplayRouteValues["Area"] as string == "Contents") {
|
||||
var itemPath = routable.Path;
|
||||
|
||||
context.Metadata.DisplayRouteValues = new RouteValueDictionary {
|
||||
{"Area", "Routable"},
|
||||
{"Controller", "Item"},
|
||||
{"Action", "Display"},
|
||||
{"path", itemPath}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Web.Routing;
|
||||
|
||||
namespace Orchard.Core.Routable {
|
||||
public interface IRoutablePathConstraint : IRouteConstraint, ISingletonDependency {
|
||||
void SetPaths(IEnumerable<string> paths);
|
||||
string FindPath(string path);
|
||||
void AddPath(string path);
|
||||
void RemovePath(string path);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using Orchard.ContentManagement.MetaData;
|
||||
using Orchard.Core.Contents.Extensions;
|
||||
using Orchard.Data.Migration;
|
||||
|
||||
namespace Orchard.Core.Routable {
|
||||
public class Migrations : DataMigrationImpl {
|
||||
|
||||
public int Create() {
|
||||
SchemaBuilder.CreateTable("RoutePartRecord",
|
||||
table => table
|
||||
.ContentPartVersionRecord()
|
||||
.Column<string>("Title", column => column.WithLength(1024))
|
||||
.Column<string>("Slug", column => column.WithLength(1024))
|
||||
.Column<string>("Path", column => column.WithLength(2048))
|
||||
);
|
||||
|
||||
ContentDefinitionManager.AlterPartDefinition("RoutePart", builder => builder.Attachable());
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ContentManagement.Aspects;
|
||||
|
||||
namespace Orchard.Core.Routable.Models {
|
||||
public class RoutePart : ContentPart<RoutePartRecord>, IRoutableAspect {
|
||||
public string Title {
|
||||
get { return Record.Title; }
|
||||
set { Record.Title = value; }
|
||||
}
|
||||
|
||||
public string Slug {
|
||||
get { return Record.Slug; }
|
||||
set { Record.Slug = value; }
|
||||
}
|
||||
|
||||
public string Path {
|
||||
get { return Record.Path; }
|
||||
set { Record.Path = value; }
|
||||
}
|
||||
|
||||
public bool PromoteToHomePage { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Orchard.ContentManagement.Records;
|
||||
|
||||
namespace Orchard.Core.Routable.Models {
|
||||
public class RoutePartRecord : ContentPartVersionRecord {
|
||||
[StringLength(1024)]
|
||||
public virtual string Title { get; set; }
|
||||
|
||||
[StringLength(1024)]
|
||||
public virtual string Slug { get; set; }
|
||||
|
||||
[StringLength(2048)]
|
||||
public virtual string Path { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
Name: Routable
|
||||
AntiForgery: enabled
|
||||
Author: The Orchard Team
|
||||
Website: http://orchardproject.net
|
||||
Version: 1.3.0
|
||||
OrchardVersion: 1.3.0
|
||||
Description: The routable module enables content items to be accessed through a friendly human-readable URL.
|
||||
FeatureDescription: Routable content part.
|
||||
Dependencies: Settings
|
||||
Category: Core
|
||||
@@ -1,15 +0,0 @@
|
||||
<Placement>
|
||||
<!-- available display shapes -->
|
||||
<!--
|
||||
Parts_RoutableTitle
|
||||
Parts_RoutableTitle_Summary
|
||||
Parts_RoutableTitle_SummaryAdmin
|
||||
-->
|
||||
<Place Parts_Routable_Edit="Content:before.5"/>
|
||||
<Match DisplayType="Detail">
|
||||
<Place Parts_RoutableTitle="Header:5"/>
|
||||
</Match>
|
||||
<Match DisplayType="Summary">
|
||||
<Place Parts_RoutableTitle_Summary="Header:5"/>
|
||||
</Match>
|
||||
</Placement>
|
||||
@@ -1,9 +0,0 @@
|
||||
using Orchard.UI.Resources;
|
||||
|
||||
namespace Orchard.Core.Routable {
|
||||
public class ResourceManifest : IResourceManifestProvider {
|
||||
public void BuildManifests(ResourceManifestBuilder builder) {
|
||||
builder.Add().DefineScript("Slugify").SetUrl("jquery.slugify.js").SetDependencies("jQuery");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Web.Mvc;
|
||||
using System.Web.Routing;
|
||||
using Orchard.Mvc.Routes;
|
||||
|
||||
namespace Orchard.Core.Routable {
|
||||
public class Routes : IRouteProvider {
|
||||
private readonly IRoutablePathConstraint _routablePathConstraint;
|
||||
|
||||
public Routes(IRoutablePathConstraint routablePathConstraint) {
|
||||
_routablePathConstraint = routablePathConstraint;
|
||||
}
|
||||
|
||||
public void GetRoutes(ICollection<RouteDescriptor> routes) {
|
||||
foreach (var routeDescriptor in GetRoutes())
|
||||
routes.Add(routeDescriptor);
|
||||
}
|
||||
|
||||
public IEnumerable<RouteDescriptor> GetRoutes() {
|
||||
return new[] {
|
||||
new RouteDescriptor {
|
||||
Route = new Route(
|
||||
"Admin/Common/Routable/Slugify",
|
||||
new RouteValueDictionary {
|
||||
{"area", "Routable"},
|
||||
{"controller", "Item"},
|
||||
{"action", "Slugify"}
|
||||
},
|
||||
new RouteValueDictionary(),
|
||||
new RouteValueDictionary {
|
||||
{"area", "Routable"}
|
||||
},
|
||||
new MvcRouteHandler())
|
||||
},
|
||||
new RouteDescriptor {
|
||||
Priority = 10,
|
||||
Route = new Route(
|
||||
"{*path}",
|
||||
new RouteValueDictionary {
|
||||
{"area", "Routable"},
|
||||
{"controller", "Item"},
|
||||
{"action", "Display"}
|
||||
},
|
||||
new RouteValueDictionary {
|
||||
{"path", _routablePathConstraint}
|
||||
},
|
||||
new RouteValueDictionary {
|
||||
{"area", "Routable"}
|
||||
},
|
||||
new MvcRouteHandler())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appSettings>
|
||||
<add key="webpages:Enabled" value="false" />
|
||||
</appSettings>
|
||||
<system.web>
|
||||
<httpHandlers>
|
||||
<!-- iis6 - for any request in this location, return via managed static file handler -->
|
||||
<add path="*" verb="*" type="System.Web.StaticFileHandler" />
|
||||
</httpHandlers>
|
||||
</system.web>
|
||||
<system.webServer>
|
||||
<handlers accessPolicy="Script,Read">
|
||||
<!--
|
||||
iis7 - for any request to a file exists on disk, return it via native http module.
|
||||
accessPolicy 'Script' is to allow for a managed 404 page.
|
||||
-->
|
||||
<add name="StaticFile" path="*" verb="*" modules="StaticFileModule" preCondition="integratedMode" resourceType="File" requireAccess="Read" />
|
||||
</handlers>
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
@@ -1,24 +0,0 @@
|
||||
jQuery.fn.extend({
|
||||
slugify: function(options) {
|
||||
//todo: (heskew) need messaging system
|
||||
if (!options.target || !options.url)
|
||||
return;
|
||||
|
||||
var args = {
|
||||
"contentType": options.contentType,
|
||||
"id": options.id,
|
||||
"containerId": options.containerId,
|
||||
__RequestVerificationToken: $("input[name=__RequestVerificationToken]").val()
|
||||
};
|
||||
args[$(this).attr("name")] = $(this).val();
|
||||
|
||||
jQuery.post(
|
||||
options.url,
|
||||
args,
|
||||
function(data) {
|
||||
options.target.val(data);
|
||||
},
|
||||
"json"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Orchard.ContentManagement.Aspects;
|
||||
|
||||
namespace Orchard.Core.Routable.Services {
|
||||
public interface IRoutableService : IDependency {
|
||||
void FillSlugFromTitle<TModel>(TModel model) where TModel : IRoutableAspect;
|
||||
string GenerateUniqueSlug(IRoutableAspect part, IEnumerable<string> existingPaths);
|
||||
|
||||
/// <summary>
|
||||
/// Returns any content item with similar path
|
||||
/// </summary>
|
||||
IEnumerable<IRoutableAspect> GetSimilarPaths(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Validates the given slug
|
||||
/// </summary>
|
||||
bool IsSlugValid(string slug);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the slug of a RoutableAspect and validate its unicity
|
||||
/// </summary>
|
||||
/// <returns>True if the slug has been created, False if a conflict occured</returns>
|
||||
bool ProcessSlug(IRoutableAspect part);
|
||||
|
||||
/// <summary>
|
||||
/// Updated the paths of all contained items to reflect the current path of this item
|
||||
/// </summary>
|
||||
void FixContainedPaths(IRoutableAspect part);
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Web;
|
||||
using System.Web.Routing;
|
||||
using JetBrains.Annotations;
|
||||
using Orchard.Logging;
|
||||
|
||||
namespace Orchard.Core.Routable.Services {
|
||||
[UsedImplicitly]
|
||||
public class RoutablePathConstraint : IRoutablePathConstraint {
|
||||
/// <summary>
|
||||
/// Singleton object, per Orchard Shell instance. We need to protect concurrent access to the dictionary.
|
||||
/// </summary>
|
||||
private readonly object _syncLock = new object();
|
||||
private IDictionary<string, string> _paths = new Dictionary<string, string>();
|
||||
|
||||
public RoutablePathConstraint() {
|
||||
Logger = NullLogger.Instance;
|
||||
}
|
||||
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public void SetPaths(IEnumerable<string> paths) {
|
||||
// Make a copy to avoid performing potential lazy computation inside the lock
|
||||
var slugsArray = paths.ToArray();
|
||||
|
||||
lock (_syncLock) {
|
||||
_paths = slugsArray.Distinct(StringComparer.OrdinalIgnoreCase).ToDictionary(value => value, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public string FindPath(string path) {
|
||||
lock (_syncLock) {
|
||||
string actual;
|
||||
return _paths.TryGetValue(path, out actual) ? actual : path;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddPath(string path) {
|
||||
lock (_syncLock) {
|
||||
_paths[path] = path;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemovePath(string path) {
|
||||
lock (_syncLock) {
|
||||
if (path != null && _paths.ContainsKey(path))
|
||||
_paths.Remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) {
|
||||
if (routeDirection == RouteDirection.UrlGeneration)
|
||||
return true;
|
||||
|
||||
object value;
|
||||
if (values.TryGetValue(parameterName, out value)) {
|
||||
var parameterValue = Convert.ToString(value);
|
||||
|
||||
lock (_syncLock) {
|
||||
return _paths.ContainsKey(parameterValue);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Orchard.Core.Routable.Models;
|
||||
using Orchard.Data;
|
||||
using Orchard.Environment;
|
||||
using Orchard.Tasks;
|
||||
|
||||
namespace Orchard.Core.Routable.Services {
|
||||
[UsedImplicitly]
|
||||
public class RoutablePathConstraintUpdator : IOrchardShellEvents, IBackgroundTask {
|
||||
private readonly IRoutablePathConstraint _pageSlugConstraint;
|
||||
private readonly IRepository<RoutePartRecord> _repository;
|
||||
|
||||
public RoutablePathConstraintUpdator(IRoutablePathConstraint pageSlugConstraint, IRepository<RoutePartRecord> repository) {
|
||||
_pageSlugConstraint = pageSlugConstraint;
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
void IOrchardShellEvents.Activated() {
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void IOrchardShellEvents.Terminating() {
|
||||
}
|
||||
|
||||
void IBackgroundTask.Sweep() {
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void Refresh() {
|
||||
var slugs = _repository.Fetch(r => r.ContentItemVersionRecord.Published && r.Path != "" && r.Path != null).Select(r => r.Path);
|
||||
|
||||
_pageSlugConstraint.SetPaths(slugs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ContentManagement.Aspects;
|
||||
using Orchard.Core.Common.Models;
|
||||
using Orchard.Core.Routable.Events;
|
||||
using Orchard.Core.Routable.Models;
|
||||
using Orchard.Utility.Extensions;
|
||||
|
||||
namespace Orchard.Core.Routable.Services {
|
||||
public class RoutableService : IRoutableService {
|
||||
private readonly IContentManager _contentManager;
|
||||
private readonly IEnumerable<ISlugEventHandler> _slugEventHandlers;
|
||||
private readonly IRoutablePathConstraint _routablePathConstraint;
|
||||
|
||||
public RoutableService(IContentManager contentManager, IEnumerable<ISlugEventHandler> slugEventHandlers, IRoutablePathConstraint routablePathConstraint) {
|
||||
_contentManager = contentManager;
|
||||
_slugEventHandlers = slugEventHandlers;
|
||||
_routablePathConstraint = routablePathConstraint;
|
||||
}
|
||||
|
||||
public void FixContainedPaths(IRoutableAspect part) {
|
||||
var items = _contentManager.Query(VersionOptions.Published)
|
||||
.Join<CommonPartRecord>().Where(cr => cr.Container.Id == part.Id)
|
||||
.List()
|
||||
.Select(item => item.As<IRoutableAspect>()).Where(item => item != null);
|
||||
|
||||
foreach (var itemRoute in items) {
|
||||
var route = itemRoute.As<IRoutableAspect>();
|
||||
|
||||
if(route == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var path = route.Path;
|
||||
route.Path = route.GetPathWithSlug(route.Slug);
|
||||
|
||||
// if the path has changed by having the slug changed on the way in (e.g. user input) or to avoid conflict
|
||||
// then update and publish all contained items
|
||||
if (path != route.Path) {
|
||||
_routablePathConstraint.RemovePath(path);
|
||||
FixContainedPaths(route);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(route.Path))
|
||||
_routablePathConstraint.AddPath(route.Path);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void FillSlugFromTitle<TModel>(TModel model) where TModel : IRoutableAspect {
|
||||
if ((model.Slug != null && !string.IsNullOrEmpty(model.Slug.Trim())) || string.IsNullOrEmpty(model.Title))
|
||||
return;
|
||||
|
||||
var slugContext = new FillSlugContext(model.Title);
|
||||
|
||||
foreach (ISlugEventHandler slugEventHandler in _slugEventHandlers) {
|
||||
slugEventHandler.FillingSlugFromTitle(slugContext);
|
||||
}
|
||||
|
||||
if (!slugContext.Adjusted) {
|
||||
var disallowed = new Regex(@"[/:?#\[\]@!$&'()*+,;=\s\""\<\>\\]+");
|
||||
|
||||
slugContext.Slug = disallowed.Replace(slugContext.Slug, "-").Trim('-');
|
||||
|
||||
if (slugContext.Slug.Length > 1000)
|
||||
slugContext.Slug = slugContext.Slug.Substring(0, 1000);
|
||||
|
||||
// dots are not allowed at the begin and the end of routes
|
||||
slugContext.Slug = StringExtensions.RemoveDiacritics(slugContext.Slug.Trim('.').ToLower());
|
||||
}
|
||||
|
||||
foreach (ISlugEventHandler slugEventHandler in _slugEventHandlers) {
|
||||
slugEventHandler.FilledSlugFromTitle(slugContext);
|
||||
}
|
||||
|
||||
model.Slug = slugContext.Slug;
|
||||
}
|
||||
|
||||
public string GenerateUniqueSlug(IRoutableAspect part, IEnumerable<string> existingPaths) {
|
||||
|
||||
if (existingPaths == null) {
|
||||
return part.Slug;
|
||||
}
|
||||
|
||||
// materializing the enumeration
|
||||
existingPaths = existingPaths.ToArray();
|
||||
|
||||
if(!existingPaths.Contains(part.Path)) {
|
||||
return part.Slug;
|
||||
}
|
||||
|
||||
int? version = existingPaths.Select(s => GetSlugVersion(part.Path, s)).OrderBy(i => i).LastOrDefault();
|
||||
|
||||
return version != null
|
||||
? string.Format("{0}-{1}", part.Slug, version)
|
||||
: part.Slug;
|
||||
}
|
||||
|
||||
private static int? GetSlugVersion(string path, string potentialConflictingPath) {
|
||||
int v;
|
||||
string[] slugParts = potentialConflictingPath.Split(new[] { path }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (slugParts.Length == 0)
|
||||
return 2;
|
||||
|
||||
return int.TryParse(slugParts[0].TrimStart('-'), out v)
|
||||
? (int?)++v
|
||||
: null;
|
||||
}
|
||||
|
||||
public IEnumerable<IRoutableAspect> GetSimilarPaths(string path) {
|
||||
return
|
||||
_contentManager.Query<RoutePart, RoutePartRecord>()
|
||||
.Where(routable => routable.Path != null && routable.Path.StartsWith(path, StringComparison.OrdinalIgnoreCase))
|
||||
.List()
|
||||
.Select(i => i.As<RoutePart>())
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public bool IsSlugValid(string slug) {
|
||||
return String.IsNullOrWhiteSpace(slug) || Regex.IsMatch(slug, @"^[^:?#\[\]@!$&'()*+,;=\s\""\<\>\\]+$") && !(slug.StartsWith(".") || slug.EndsWith("."));
|
||||
}
|
||||
|
||||
public bool ProcessSlug(IRoutableAspect part) {
|
||||
FillSlugFromTitle(part);
|
||||
|
||||
if (string.IsNullOrEmpty(part.Slug))
|
||||
return true;
|
||||
|
||||
part.Path = part.GetPathWithSlug(part.Slug);
|
||||
var pathsLikeThis = GetSimilarPaths(part.Path).ToArray();
|
||||
|
||||
// Don't include *this* part in the list
|
||||
// of slugs to consider for conflict detection
|
||||
pathsLikeThis = pathsLikeThis.Where(p => p.ContentItem.Id != part.ContentItem.Id).ToArray();
|
||||
|
||||
if (pathsLikeThis.Any()) {
|
||||
var originalSlug = part.Slug;
|
||||
var newSlug = GenerateUniqueSlug(part, pathsLikeThis.Select(p => p.Path));
|
||||
part.Path = part.GetPathWithSlug(newSlug);
|
||||
part.Slug = newSlug;
|
||||
|
||||
if (originalSlug != newSlug)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class RoutableAspectExtensions {
|
||||
public static string GetContainerPath(this IRoutableAspect routableAspect) {
|
||||
var commonAspect = routableAspect.As<ICommonPart>();
|
||||
if (commonAspect != null && commonAspect.Container != null) {
|
||||
var routable = commonAspect.Container.As<IRoutableAspect>();
|
||||
if (routable != null)
|
||||
return routable.Path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string GetPathWithSlug(this IRoutableAspect routableAspect, string slug) {
|
||||
var containerPath = routableAspect.GetContainerPath();
|
||||
return !string.IsNullOrEmpty(containerPath)
|
||||
? string.Format("{0}/{1}", containerPath, slug)
|
||||
: slug;
|
||||
}
|
||||
|
||||
public static string GetChildPath(this IRoutableAspect routableAspect, string slug) {
|
||||
return string.Format("{0}/{1}", routableAspect.Path, slug);
|
||||
}
|
||||
|
||||
public static string GetEffectiveSlug(this IRoutableAspect routableAspect) {
|
||||
var containerPath = routableAspect.GetContainerPath();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(routableAspect.Path))
|
||||
return "";
|
||||
|
||||
var slugParts = routableAspect.Path.Split(new []{string.Format("{0}/", containerPath)}, StringSplitOptions.RemoveEmptyEntries);
|
||||
return slugParts.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using Orchard.Core.Routable.Models;
|
||||
|
||||
namespace Orchard.Core.Routable.ViewModels {
|
||||
public class RoutableDisplayViewModel {
|
||||
public string Title { get { return RoutePart.Title; } }
|
||||
public RoutePart RoutePart { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Orchard.Core.Routable.ViewModels {
|
||||
public class RoutableEditorViewModel {
|
||||
|
||||
public int Id { get; set; }
|
||||
public string ContentType { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(1024)]
|
||||
public string Title { get; set; }
|
||||
[StringLength(1024)]
|
||||
public string Slug { get; set; }
|
||||
public int? ContainerId { get; set; }
|
||||
public bool PromoteToHomePage { get; set; }
|
||||
|
||||
public string ContainerAbsoluteUrl { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
@model Orchard.Core.Routable.ViewModels.RoutableEditorViewModel
|
||||
@using Orchard.Utility.Extensions;
|
||||
@{ Script.Require("Slugify"); }
|
||||
|
||||
<fieldset>
|
||||
@Html.LabelFor(m => m.Title, T("Title"))
|
||||
@Html.TextBoxFor(m => m.Title, new { @class = "large text" })
|
||||
</fieldset>
|
||||
<fieldset class="permalink">
|
||||
<label class="sub" for="Slug">@T("Permalink")<br /><span>@Model.ContainerAbsoluteUrl/</span></label>
|
||||
<span>@Html.TextBoxFor(m => m.Slug, new { @class = "text" })</span>
|
||||
<span class="checkbox-and-label">
|
||||
@Html.EditorFor(m => m.PromoteToHomePage)
|
||||
<label for="@ViewData.TemplateInfo.GetFullHtmlFieldId("PromoteToHomePage")" class="forcheckbox">@T("Set as home page")</label>
|
||||
</span>
|
||||
</fieldset>
|
||||
@using(Script.Foot()){
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
$(function(){
|
||||
//pull slug input from tab order
|
||||
$("#@Html.FieldIdFor(m=>m.Slug)").attr("tabindex",-1);
|
||||
$("#@Html.FieldIdFor(m=>m.Title)").blur(function(){
|
||||
var slug = $("#@Html.FieldIdFor(m=>m.Slug)");
|
||||
if (slug.val()) { return true; }
|
||||
$(this).slugify({
|
||||
target:slug,
|
||||
contentType:"@Model.ContentType",
|
||||
id:"@Model.Id",
|
||||
@if (Model.ContainerId != null) {<text>containerId:@Model.ContainerId,</text>}
|
||||
url:"@Url.Action("Slugify","Item",new RouteValueDictionary{{"Area","Routable"}})"
|
||||
})
|
||||
})
|
||||
})
|
||||
//]]>
|
||||
</script>
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<h1>@Model.Title</h1>
|
||||
@@ -1,6 +0,0 @@
|
||||
@{
|
||||
Orchard.ContentManagement.ContentItem contentItem = Model.ContentPart.ContentItem;
|
||||
string title = Model.Title.ToString();
|
||||
}
|
||||
|
||||
<h1>@Html.ItemDisplayLink(title, contentItem)</h1>
|
||||
@@ -1,6 +0,0 @@
|
||||
@{
|
||||
Orchard.ContentManagement.ContentItem contentItem = Model.ContentPart.ContentItem;
|
||||
string title = Model.Title.ToString();
|
||||
}
|
||||
|
||||
<h1>@Html.ItemEditLink(title, contentItem)</h1>
|
||||
@@ -1,41 +0,0 @@
|
||||
<?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>
|
||||
Reference in New Issue
Block a user