mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2026-02-09 09:16:41 +08:00
Removing Regex usage
--HG-- branch : 1.x
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Utility.Extensions;
|
||||
|
||||
@@ -34,24 +35,24 @@ namespace Orchard.Tests.Utility.Extensions {
|
||||
[Test]
|
||||
public void Ellipsize_ShouldTuncateToTheExactNumber() {
|
||||
const string toEllipsize = "Lorem ipsum";
|
||||
Assert.That(toEllipsize.Ellipsize(2, ""), Is.StringMatching("Lo"));
|
||||
Assert.That(toEllipsize.Ellipsize(1, ""), Is.StringMatching("L"));
|
||||
Assert.That(toEllipsize.Ellipsize(0, ""), Is.StringMatching(""));
|
||||
Assert.That(toEllipsize.Ellipsize(2, ""), Is.EqualTo("Lo"));
|
||||
Assert.That(toEllipsize.Ellipsize(1, ""), Is.EqualTo("L"));
|
||||
Assert.That(toEllipsize.Ellipsize(0, ""), Is.EqualTo(""));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Ellipsize_TruncatedToWordBoundary() {
|
||||
const string toEllipsize = "Lorem ipsum";
|
||||
Assert.That(toEllipsize.Ellipsize(8, ""), Is.StringMatching("Lorem"));
|
||||
Assert.That(toEllipsize.Ellipsize(6, ""), Is.StringMatching("Lorem"));
|
||||
Assert.That(toEllipsize.Ellipsize(5, ""), Is.StringMatching("Lorem"));
|
||||
Assert.That(toEllipsize.Ellipsize(4, ""), Is.StringMatching(""));
|
||||
Assert.That(toEllipsize.Ellipsize(8, ""), Is.EqualTo("Lorem"));
|
||||
Assert.That(toEllipsize.Ellipsize(6, ""), Is.EqualTo("Lorem"));
|
||||
Assert.That(toEllipsize.Ellipsize(5, ""), Is.EqualTo("Lorem"));
|
||||
Assert.That(toEllipsize.Ellipsize(4, ""), Is.EqualTo("Lore"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Ellipsize_LongStringTruncatedToNearestWord() {
|
||||
const string toEllipsize = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas sed purus quis purus orci aliquam.";
|
||||
Assert.That(toEllipsize.Ellipsize(45), Is.StringMatching("Lorem ipsum dolor sit amet, consectetur …"));
|
||||
Assert.That(toEllipsize.Ellipsize(46), Is.StringMatching("Lorem ipsum dolor sit amet, consectetur …"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -74,6 +75,12 @@ namespace Orchard.Tests.Utility.Extensions {
|
||||
const string toEllipsize = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas sed purus quis purus orci aliquam.";
|
||||
Assert.That(toEllipsize.Ellipsize(45, "........"), Is.StringMatching("Lorem ipsum dolor sit amet, consectetur........"));
|
||||
}
|
||||
[Test]
|
||||
public void Ellipsize_WordBoundary() {
|
||||
const string toEllipsize = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas sed purus quis purus orci aliquam.";
|
||||
Assert.That(toEllipsize.Ellipsize(43, "..."), Is.StringMatching("Lorem ipsum dolor sit amet, consectet..."));
|
||||
Assert.That(toEllipsize.Ellipsize(43, "...", true), Is.StringMatching("Lorem ipsum dolor sit amet, ..."));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HtmlClassify_ValidReallySimpleClassNameReturnsSame() {
|
||||
@@ -172,7 +179,7 @@ namespace Orchard.Tests.Utility.Extensions {
|
||||
[Test]
|
||||
public void ReplaceNewLinesWith_ReplaceCRLFWithHtmlPsAndCRLF() {
|
||||
const string lotsOfLineFeeds = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.\r\nMaecenas sed purus quis purus orci aliquam.";
|
||||
Assert.That(lotsOfLineFeeds.ReplaceNewLinesWith(@"</p>$1<p>"), Is.StringMatching("Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>\r\n<p>Maecenas sed purus quis purus orci aliquam."));
|
||||
Assert.That(lotsOfLineFeeds.ReplaceNewLinesWith(@"</p>{0}<p>"), Is.StringMatching("Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>\r\n<p>Maecenas sed purus quis purus orci aliquam."));
|
||||
}
|
||||
[Test]
|
||||
public void ReplaceNewLinesWith_EmptyStringReturnsEmptyString() {
|
||||
@@ -184,5 +191,86 @@ namespace Orchard.Tests.Utility.Extensions {
|
||||
const string lotsOfLineFeeds = null;
|
||||
Assert.That(lotsOfLineFeeds.ReplaceNewLinesWith("<br />"), Is.StringMatching(""));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StripShouldRemoveStart() {
|
||||
Assert.That("abc".Strip('a'), Is.StringMatching("bc"));
|
||||
Assert.That("abc".Strip("ab".ToCharArray()), Is.StringMatching("c"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StripShouldRemoveInside() {
|
||||
Assert.That("abc".Strip('b'), Is.StringMatching("ac"));
|
||||
Assert.That("abc".Strip("abc".ToCharArray()), Is.StringMatching(""));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StripShouldRemoveEnd() {
|
||||
Assert.That("abc".Strip('c'), Is.StringMatching("ab"));
|
||||
Assert.That("abc".Strip("bc".ToCharArray()), Is.StringMatching("a"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StripShouldReturnIfEmpty() {
|
||||
Assert.That("".Strip('a'), Is.StringMatching(""));
|
||||
Assert.That("a".Strip("".ToCharArray()), Is.StringMatching("a"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnyShouldReturnTrueAtStart() {
|
||||
Assert.That("abc".Any('a'), Is.True);
|
||||
Assert.That("abc".Any("ab".ToCharArray()), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnyShouldReturnTrueAtEnd() {
|
||||
Assert.That("abc".Any('c'), Is.True);
|
||||
Assert.That("abc".Any("bc".ToCharArray()), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnyShouldReturnTrueAtMiddle() {
|
||||
Assert.That("abc".Any('b'), Is.True);
|
||||
Assert.That("abc".Any("abc".ToCharArray()), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnyShouldReturnFalseIfNotPresent() {
|
||||
Assert.That("abc".Any("".ToCharArray()), Is.False);
|
||||
Assert.That("abc".Any("d".ToCharArray()), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AllShouldReturnTrueIfAllArePresent() {
|
||||
Assert.That("abc".All("abc".ToCharArray()), Is.True);
|
||||
Assert.That("abc".All("abcd".ToCharArray()), Is.True);
|
||||
Assert.That("".All("a".ToCharArray()), Is.True);
|
||||
Assert.That("abc".All("abcd".ToCharArray()), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AllShouldReturnFalseIfAnyIsNotPresent() {
|
||||
Assert.That("abc".All("".ToCharArray()), Is.False);
|
||||
Assert.That("abc".All("a".ToCharArray()), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TranslateShouldThrowException() {
|
||||
Assert.Throws<ArgumentNullException>(() => "a".Translate("".ToCharArray(), "a".ToCharArray()));
|
||||
Assert.Throws<ArgumentNullException>(() => "a".Translate("a".ToCharArray(), "".ToCharArray()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TranslateShouldReturnSource() {
|
||||
Assert.That("a".Translate("".ToCharArray(), "".ToCharArray()), Is.StringMatching(""));
|
||||
Assert.That("".Translate("abc".ToCharArray(), "abc".ToCharArray()), Is.StringMatching(""));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TranslateShouldReplaceChars() {
|
||||
Assert.That("abc".Translate("a".ToCharArray(), "d".ToCharArray()), Is.StringMatching("dbc"));
|
||||
Assert.That("abc".Translate("d".ToCharArray(), "d".ToCharArray()), Is.StringMatching("abc"));
|
||||
Assert.That("abc".Translate("abc".ToCharArray(), "def".ToCharArray()), Is.StringMatching("def"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<span class="when">@Display.CommentMetadata(ContentPart: comment)</span>
|
||||
</h4>
|
||||
</header>
|
||||
<p class="text">@(new MvcHtmlString(Html.Encode(comment.Record.CommentText).ReplaceNewLinesWith("<br />$1")))</p>
|
||||
<p class="text">@(new MvcHtmlString(Html.Encode(comment.Record.CommentText).Replace("\r\n", "<br />\r\n")))</p>
|
||||
</article>
|
||||
</li>
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using Orchard.Environment.Extensions;
|
||||
using Orchard.Environment.Extensions.Models;
|
||||
using Orchard.FileSystems.VirtualPath;
|
||||
using Orchard.UI.Resources;
|
||||
using Orchard.Utility.Extensions;
|
||||
|
||||
namespace Orchard.DisplayManagement.Descriptors.ResourceBindingStrategy {
|
||||
// discovers .css files and turns them into Style__<filename> shapes.
|
||||
@@ -16,7 +17,7 @@ namespace Orchard.DisplayManagement.Descriptors.ResourceBindingStrategy {
|
||||
private readonly IExtensionManager _extensionManager;
|
||||
private readonly ShellDescriptor _shellDescriptor;
|
||||
private readonly IVirtualPathProvider _virtualPathProvider;
|
||||
private static readonly Regex _safeName = new Regex(@"[/:?#\[\]@!&'()*+,;=\s\""<>\.\-_]+", RegexOptions.Compiled);
|
||||
private static readonly char[] unsafeCharList = "/:?#[]@!&'()*+,;=\r\n\t\f\" <>.-_".ToCharArray();
|
||||
|
||||
public StylesheetBindingStrategy(IExtensionManager extensionManager, ShellDescriptor shellDescriptor, IVirtualPathProvider virtualPathProvider) {
|
||||
_extensionManager = extensionManager;
|
||||
@@ -27,7 +28,8 @@ namespace Orchard.DisplayManagement.Descriptors.ResourceBindingStrategy {
|
||||
private static string SafeName(string name) {
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return String.Empty;
|
||||
return _safeName.Replace(name, String.Empty).ToLowerInvariant();
|
||||
|
||||
return name.Strip(unsafeCharList).ToLowerInvariant();
|
||||
}
|
||||
|
||||
public static string GetAlternateShapeNameFromFileName(string fileName) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Web;
|
||||
using Orchard.Caching;
|
||||
using Orchard.Data;
|
||||
@@ -83,11 +82,21 @@ namespace Orchard.Localization.Services {
|
||||
// "<languagecode2>-<country/regioncode2>" or
|
||||
// "<languagecode2>-<scripttag>-<country/regioncode2>"
|
||||
public bool IsValidCulture(string cultureName) {
|
||||
Regex cultureRegex = new Regex(@"\w{2}(-\w{2,})*");
|
||||
if (cultureRegex.IsMatch(cultureName)) {
|
||||
return true;
|
||||
var segments = cultureName.Split('-');
|
||||
|
||||
if(segments.Length == 0) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
if (segments.Length > 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (segments.Any(s => s.Length < 2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Orchard.Localization;
|
||||
|
||||
namespace Orchard.Utility.Extensions {
|
||||
public static class StringExtensions {
|
||||
private static readonly Regex humps = new Regex("(?:^[a-zA-Z][^A-Z]*|[A-Z][^A-Z]*)");
|
||||
private static readonly Regex safe = new Regex(@"[^_\-a-zA-Z\d]+");
|
||||
|
||||
public static string CamelFriendly(this string camel) {
|
||||
if (String.IsNullOrWhiteSpace(camel))
|
||||
return "";
|
||||
|
||||
var matches = humps.Matches(camel).OfType<Match>().Select(m => m.Value).ToArray();
|
||||
return matches.Any()
|
||||
? matches.Aggregate((a, b) => a + " " + b).TrimStart(' ')
|
||||
: camel;
|
||||
var sb = new StringBuilder(camel);
|
||||
|
||||
for (int i = camel.Length-1; i>=0; i--) {
|
||||
var current = sb[i];
|
||||
if('A' <= current && current <= 'Z') {
|
||||
sb.Insert(i, ' ');
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string Ellipsize(this string text, int characterCount) {
|
||||
@@ -31,12 +34,23 @@ namespace Orchard.Utility.Extensions {
|
||||
if (characterCount < 0 || text.Length <= characterCount)
|
||||
return text;
|
||||
|
||||
var trimmed = Regex.Replace(text.Substring(0, characterCount), @"\s+\S*$", "") ;
|
||||
|
||||
if(wordBoundary) {
|
||||
trimmed = Regex.Replace(trimmed + ".", @"\W*\w*$", "");
|
||||
// search beginning of word
|
||||
var backup = characterCount;
|
||||
while (characterCount > 0 && text[characterCount-1].IsLetter()) {
|
||||
characterCount--;
|
||||
}
|
||||
|
||||
// search previous word
|
||||
while (characterCount > 0 && text[characterCount - 1].IsSpace()) {
|
||||
characterCount--;
|
||||
}
|
||||
|
||||
// if it was the last word, recover it, unless boundary is requested
|
||||
if(characterCount == 0 && !wordBoundary) {
|
||||
characterCount = backup;
|
||||
}
|
||||
|
||||
var trimmed = text.Substring(0, characterCount);
|
||||
return trimmed + ellipsis;
|
||||
}
|
||||
|
||||
@@ -45,7 +59,27 @@ namespace Orchard.Utility.Extensions {
|
||||
return "";
|
||||
|
||||
var friendlier = text.CamelFriendly();
|
||||
return Regex.Replace(friendlier, @"[^a-zA-Z]+", m => m.Index == 0 ? "" : "-").ToLowerInvariant();
|
||||
|
||||
var result = new char[friendlier.Length];
|
||||
|
||||
var cursor = 0;
|
||||
var previousIsNotLetter = false;
|
||||
for (var i = 0; i < friendlier.Length; i++) {
|
||||
char current = friendlier[i];
|
||||
if (IsLetter(current)) {
|
||||
if(previousIsNotLetter && i != 0) {
|
||||
result[cursor++] = '-';
|
||||
}
|
||||
|
||||
result[cursor++] = Char.ToLowerInvariant(current);
|
||||
previousIsNotLetter = false;
|
||||
}
|
||||
else {
|
||||
previousIsNotLetter = true;
|
||||
}
|
||||
}
|
||||
|
||||
return new string(result, 0, cursor);
|
||||
}
|
||||
|
||||
public static LocalizedString OrDefault(this string text, LocalizedString defaultValue) {
|
||||
@@ -55,16 +89,42 @@ namespace Orchard.Utility.Extensions {
|
||||
}
|
||||
|
||||
public static string RemoveTags(this string html) {
|
||||
return String.IsNullOrEmpty(html)
|
||||
? ""
|
||||
: Regex.Replace(html, "<[^<>]*>", "", RegexOptions.Singleline);
|
||||
if (String.IsNullOrEmpty(html)) {
|
||||
return String.Empty;
|
||||
}
|
||||
|
||||
var result = new char[html.Length];
|
||||
|
||||
var cursor = 0;
|
||||
var inside = false;
|
||||
for (var i = 0; i < html.Length; i++) {
|
||||
char current = html[i];
|
||||
|
||||
switch(current) {
|
||||
case '<':
|
||||
inside = true;
|
||||
continue;
|
||||
case '>':
|
||||
inside = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inside) {
|
||||
result[cursor++] = current;
|
||||
}
|
||||
}
|
||||
|
||||
return new string(result, 0, cursor);
|
||||
}
|
||||
|
||||
// not accounting for only \r (e.g. Apple OS 9 carriage return only new lines)
|
||||
public static string ReplaceNewLinesWith(this string text, string replacement) {
|
||||
return String.IsNullOrWhiteSpace(text)
|
||||
? ""
|
||||
: Regex.Replace(text, @"(\r?\n)", replacement, RegexOptions.Singleline);
|
||||
? String.Empty
|
||||
: text
|
||||
.Replace("\r\n", "\r\r")
|
||||
.Replace("\n", String.Format(replacement, "\r\n"))
|
||||
.Replace("\r\r", String.Format(replacement, "\r\n"));
|
||||
}
|
||||
|
||||
public static string ToHexString(this byte[] bytes) {
|
||||
@@ -78,6 +138,7 @@ namespace Orchard.Utility.Extensions {
|
||||
ToArray();
|
||||
}
|
||||
|
||||
private static readonly char[] validSegmentChars = @"/?#[]@""^{}|`<>\t\r\n\f ".ToCharArray();
|
||||
public static bool IsValidUrlSegment(this string segment) {
|
||||
// valid isegment from rfc3987 - http://tools.ietf.org/html/rfc3987#page-8
|
||||
// the relevant bits:
|
||||
@@ -90,7 +151,7 @@ namespace Orchard.Utility.Extensions {
|
||||
//
|
||||
// rough blacklist regex == m/^[^/?#[]@"^{}|\s`<>]+$/ (leaving off % to keep the regex simple)
|
||||
|
||||
return Regex.IsMatch(segment, @"^[^/?#[\]@""^{}|`<>\s]+$");
|
||||
return !segment.Any(validSegmentChars);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -104,7 +165,13 @@ namespace Orchard.Utility.Extensions {
|
||||
return String.Empty;
|
||||
|
||||
name = RemoveDiacritics(name);
|
||||
name = safe.Replace(name, String.Empty);
|
||||
name = name.Strip(c =>
|
||||
c != '_'
|
||||
&& c != '-'
|
||||
&& !c.IsLetter()
|
||||
&& !Char.IsDigit(c)
|
||||
);
|
||||
|
||||
name = name.Trim();
|
||||
|
||||
// don't allow non A-Z chars as first letter, as they are not allowed in prefixes
|
||||
@@ -125,6 +192,9 @@ namespace Orchard.Utility.Extensions {
|
||||
return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z');
|
||||
}
|
||||
|
||||
public static bool IsSpace(this char c) {
|
||||
return (c == '\r' || c == '\n' || c == '\t' || c == '\f' || c == ' ');
|
||||
}
|
||||
|
||||
public static string RemoveDiacritics(string name) {
|
||||
string stFormD = name.Normalize(NormalizationForm.FormD);
|
||||
@@ -139,5 +209,110 @@ namespace Orchard.Utility.Extensions {
|
||||
|
||||
return (sb.ToString().Normalize(NormalizationForm.FormC));
|
||||
}
|
||||
|
||||
public static string Strip(this string subject, params char[] stripped) {
|
||||
if(stripped == null || stripped.Length == 0 || String.IsNullOrEmpty(subject)) {
|
||||
return subject;
|
||||
}
|
||||
|
||||
Array.Sort(stripped);
|
||||
var result = new char[subject.Length];
|
||||
|
||||
var cursor = 0;
|
||||
for (var i = 0; i < subject.Length; i++) {
|
||||
char current = subject[i];
|
||||
if (Array.BinarySearch(stripped, current) < 0) {
|
||||
result[cursor++] = current;
|
||||
}
|
||||
}
|
||||
|
||||
return new string(result, 0, cursor);
|
||||
}
|
||||
|
||||
public static string Strip(this string subject, Func<char, bool> predicate) {
|
||||
|
||||
var result = new char[subject.Length];
|
||||
|
||||
var cursor = 0;
|
||||
for (var i = 0; i < subject.Length; i++) {
|
||||
char current = subject[i];
|
||||
if (!predicate(current)) {
|
||||
result[cursor++] = current;
|
||||
}
|
||||
}
|
||||
|
||||
return new string(result, 0, cursor);
|
||||
}
|
||||
|
||||
public static bool Any(this string subject, params char[] chars) {
|
||||
if (string.IsNullOrEmpty(subject) || chars == null || chars.Length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Array.Sort(chars);
|
||||
|
||||
for (var i = 0; i < subject.Length; i++) {
|
||||
char current = subject[i];
|
||||
if (Array.BinarySearch(chars, current) >= 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool All(this string subject, params char[] chars) {
|
||||
if (string.IsNullOrEmpty(subject)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(chars == null || chars.Length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Array.Sort(chars);
|
||||
|
||||
for (var i = 0; i < subject.Length; i++) {
|
||||
char current = subject[i];
|
||||
if (Array.BinarySearch(chars, current) < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string Translate(this string subject, char[] from, char[] to) {
|
||||
if (string.IsNullOrEmpty(subject)) {
|
||||
return subject;
|
||||
}
|
||||
|
||||
if (from == null || to == null) {
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
if (from.Length != to.Length) {
|
||||
throw new ArgumentNullException("from", "Parameters must have the same length");
|
||||
}
|
||||
|
||||
var map = new Dictionary<char, char>(from.Length);
|
||||
for (var i = 0; i < from.Length; i++) {
|
||||
map[from[i]] = to[i];
|
||||
}
|
||||
|
||||
var result = new char[subject.Length];
|
||||
|
||||
for (var i = 0; i < subject.Length; i++) {
|
||||
var current = subject[i];
|
||||
if (map.ContainsKey(current)) {
|
||||
result[i] = map[current];
|
||||
}
|
||||
else {
|
||||
result[i] = current;
|
||||
}
|
||||
}
|
||||
|
||||
return new string(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user