using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Orchard.ContentManagement; using Orchard.Core.Routable.Models; namespace Orchard.Core.Routable.Services { public class RoutableService : IRoutableService { private readonly IContentManager _contentManager; public RoutableService(IContentManager contentManager) { _contentManager = contentManager; } public void FillSlugFromTitle(TModel model) where TModel : RoutePart { if (!string.IsNullOrEmpty(model.Slug) || string.IsNullOrEmpty(model.Title)) return; var slug = model.Title; var dissallowed = new Regex(@"[/:?#\[\]@!$&'()*+,;=\s]+"); slug = dissallowed.Replace(slug, "-"); slug = slug.Trim('-'); if (slug.Length > 1000) slug = slug.Substring(0, 1000); // dots are not allowed at the begin and the end of routes slug = slug.Trim('.'); model.Slug = slug.ToLowerInvariant(); } public string GenerateUniqueSlug(RoutePart part, IEnumerable existingPaths) { var slugCandidate = part.Slug; if (existingPaths == null || !existingPaths.Contains(part.Path)) return slugCandidate; int? version = existingPaths.Select(s => GetSlugVersion(slugCandidate, s)).OrderBy(i => i).LastOrDefault(); return version != null ? string.Format("{0}-{1}", slugCandidate, version) : slugCandidate; } private static int? GetSlugVersion(string slugCandidate, string slug) { int v; string[] slugParts = slug.Split(new []{slugCandidate}, StringSplitOptions.RemoveEmptyEntries); if (slugParts.Length == 0) return 2; return int.TryParse(slugParts[0].TrimStart('-'), out v) ? (int?)++v : null; } public IEnumerable GetSimilarPaths(string path) { return _contentManager.Query().Join() .List() .Select(i => i.As()) .Where(routable => routable.Path != null && routable.Path.StartsWith(path, StringComparison.OrdinalIgnoreCase)) // todo: for some reason the filter doesn't work within the query, even without StringComparison or StartsWith .ToArray(); } public bool IsSlugValid(string slug) { return String.IsNullOrWhiteSpace(slug) || Regex.IsMatch(slug, @"^[^:?#\[\]@!$&'()*+,;=\s]+$") && !(slug.StartsWith(".") || slug.EndsWith(".")); } public bool ProcessSlug(RoutePart part) { FillSlugFromTitle(part); if (string.IsNullOrEmpty(part.Slug)) return true; part.Path = part.GetPathWithSlug(part.Slug); var pathsLikeThis = GetSimilarPaths(part.Path); // 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); if (pathsLikeThis.Count() > 0) { var originalSlug = part.Slug; //todo: (heskew) make auto-uniqueness optional part.Slug = GenerateUniqueSlug(part, pathsLikeThis.Select(p => p.Path)); if (originalSlug != part.Slug) { return false; } } return true; } } }