Files
Umbraco-CMS/src/Umbraco.Core/Extensions/StringExtensions.cs
Ronald Barendse 6dc874147f v11: Merge v10/feature/project-cleanup into v11/dev (#13112)
* New backoffice/add system text json configuration attribute (#12998)

* Add SystemTextJsonConfigurationAttribute

* Fix up formatting

* Rename classes for clearer purpose

Co-authored-by: Zeegaan <nge@umbraco.dk>

* UmbracoPath should no longer be configurable (#13032)

* UmbracoPath should no longer be configurable

* Remove UmbracoPath configuration from all tests

* Only contain style instead of full layout (#13033)

* Only contain style instead of full layout (#13033)

* Fix CodeQL duplicate "permissions"  node and reformat

* add an extra check to ensure the pips exist before adding a class to them

* improve pip classList add/remove with no intermediary variable

* Only contain style instead of full layout (#13033)

* Ensure consistent margin on headings in tree root (#12992)

* Ensure consistent margin on headings in tree root (#12992)

(cherry picked from commit 88bfef9e0d)

* Bump version to 10.2.1

* Translate "User permissions for languages" feature to dutch (#12971)

* Translate 'sectionsHelp' to Dutch

* Translate 'selectLanguages' to Dutch

* Transkate 'allowAccessToAllLanguages' to Dutch

* Translate "User permissions for languages" feature to español (#12975)

* Translate 'selectLanguages' to Español

* Translate 'languagesHelp' to español

* Translate 'allowAccessToAllLanguages' to spanish

* Updated project references for Forms and Deploy in the JsonSchema project. (#13047)

* Updated project references for Forms and Deploy in the JsonSchema project. (#13047)

* UmbracoPath has been removed from the official schema store, remove temporary workaround from our schema generator as well (#13043)

* add an extra check to ensure the pips exist before adding a class to them

* improve pip classList add/remove with no intermediary variable

* pass in parameters needed to member service (#13020)

* Missing methods from IMemberService (#13022)

* Add back methods to interface

* Add default implementations to avoid breaking changes

Co-authored-by: Zeegaan <nge@umbraco.dk>

* New endpoint for web profiling dashboard (#13038)

* Endpoint for web profiling dashboard

* Add profiling API contract

* New Backoffice: Published cache controller (#13034)

* Add published cache controller (endpoints for the Published Status dashboard)

* Update OpenAPI contract for published cache endpoints

* Fix OpenApi spec

Co-authored-by: Zeegaan <nge@umbraco.dk>

* Bug fix for datepicker with offset time (#12847)

* https://github.com/umbraco/Umbraco-CMS/issues/12844

* remove "X" from ng-attr

Doing the test I killed the ng-if attr. But forgot it was there doing the commit

Co-authored-by: Lucas Bach Bisgaard <lom@novicell.dk>

* Make sure swagger tags and operations are sorted alphabetically (#13055)

* Add spellcheck false to password inputs (#13059)

* Add null check for variants in Grid Layout (#13060)

This fixes a regression from 10.2.0 where the `variants` property was removed.

* Add null check for variants in Grid Layout (#13060)

This fixes a regression from 10.2.0 where the `variants` property was removed.

* Fixes #12972 for validating legacy member passwords (#12973)

* Fixes #12972 for validating legacy member passwords

* Removed unused variable

* removed unused variable

* Fix issue toggling boolean between true/false after Save without refreshing

* New backoffice: examine management controller (#12981)

* Add ExamineManagementControllerBase

* Add ExamineIndexModelFactory

* Add IndexesExamineManagementController

* Add proper attributes

* Implement ExamineIndexViewModel.cs

* formatting

* Add comment about it working in .NET 7

* Add SearchersExamineManagementController.cs

* Update comments about why it might throw errors

* Add SearchResultViewModel

* Add SearchExamineManagementController

* Add ExamineSearcherValidationService

* Rename ExamineSearcherValidationService.cs to ExamineSearcherFinderService.cs

* Rename interface aswell

* Add SearchExamineManagementController

* Refactor ExamineSearcherFinderService

* Add HasIndexRebuiltExamineManagementController.cs

* Fix up formatting

* Async actions

* Add RebuildIndexExamineManagementController.cs

* Rename IExamineIndexModelFactory to IExamineIndexViewModelFactory

* Refactor HasIndexRebuilt endpoint to Index endpoint

* Remove unused usings

* Fix up DetailsExplanation

* Create dedicated SearchersViewModel

* Rename action

* Rename RebuildIndex to Rebuild

* Dont have changes in ExamineIndexModel

* Make values strongly typed instead of generic lists of strings

* Rename to non-plural

* Rename to non-plural

* Rename controller

* Introduce ITemporaryIndexingService

* Introduce ITemporaryIndexingService

* Add SearcherName to view model

* Move to new ExamineManagementControllerBase.cs

* Refactor ExamineManagerService

* Use init instead of setters

* Make properties explicitly on models

* Add DI

* Apply suggestions from code review

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>

* Rename to IndexExamineManagementController

* Return ViewModel instead of exception

* Make view models non-nullable

* Add examine management extension point

* Rename to IndexingRebuilderService

* Move rebuild logic to service

* Fix up usages in IIndexingRebuilderService

* Fix up DI

* Fix OpenApi contract

* Implement CanRebuild on IIndexingRebuilderService.cs

Co-authored-by: Zeegaan <nge@umbraco.dk>
Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>

* Update build script

* Add BuildProjectReferences=false to dotnet pack

* Internalize Umbraco.Cms.ManagementApi references

* Make Searchers endpoint return ActionResult (#13068)

* New backoffice - trees design (#12963)

* Refactor: Add default versioned back office route attribute

* Tree controller bases and first draft implementations for document, media and doctype

* Move tree item view models to appropriate location

* Fix missing parent

* Refactor user entity access for testability

* A bit of clean-up + handle user start nodes for items endpoint

* Implement foldersOnly for folder tree

* Items endpoint for document type tree

* Strongly typed action results

* Content + media recycle bin

* Correct return type for swagger

* Member type tree

* Rename user start node handling to make a little more sense

* Revert to faked admin start nodes in document tree

* Media type tree

* Data type tree

* Relation type tree

* Remove unused dependency from member type tree

* Correct documentation for member type tree endpoint response types

* Use icon constants

* Add templates tree

* Member group tree

* Document blueprint tree

* Partial views, scripts and stylesheets trees

* Static files tree

* Clarify "folders only" state

* Comments and improved readability

* Rename TreeControllerBase and TreeItemViewModel

* Move recycle bin controller base to its own namespace

* Moved tree base controllers to their own namespace

* Common base class for tree view models

* Remove ProblemDetails response type declaration from all actions

* Add OpenApiTag

* Various review comments

* Dictionary item tree

* Renamed all tree controllers to follow action/feature naming convention

* Handle client culture state for document tree

* Support "ignore user start nodes" for content and media + refactor how tree states work to make things more explicit

* Fix or postpone a few TODOs

* Make entity service able to paginate trashed children

* Handle sorting explicitly

* Re-apply VersionedApiBackOfficeRoute to install and upgrade controllers after merge

* Use PagedViewModel instead of PagedResult for all trees

* Explain the usage of UmbracoObjectTypes.Unknown

* Introduce and apply GetMany pattern for dictionary items

* Add a note about relation type caching

* Fix broken test build + add unit tests for new localization service methods

* Use new management API controller base

* Entity repository should build document entities for document blueprints when getting paged entities (same as it does when getting specific entities)

* Use Media type for Media recycle bin

Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>

* Move shared relation service to concrete implementations

* Use inclusive language

* Add 401 response type documentation to applicable trees

* Refactor entity load for folder tree controller base + ensure that folders are only included in the first result page

* Add (in-memory) pagination to dictionary tree

* Make file system controller honor paging parameters

* Support pagination in relation type tree

* Clarify method name a bit for detecting tree root path requests

* Update Open API schema to match new trees

* Move from page number and page size to skip/take (with temporary workaround for lack of property skip/take pagination in current DB implementation)

* Update OpenAPI schema to match skip/take

* Update OpenAPI schema

* Don't return paginated view models from "items" endpoints

* Update OpenApi schema

Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>

* Use pattern matching to check if items is not `JArray`

* Bump Smidge up to v4.1.1

* Removing X-XSS-Protection healthcheck

* Redirect to a return URL if one is present on the querystring when logging into the back office

* Fix issue 13023 - Cannot read properties of undefined (reading 'allowedActions')

* Implement playwright acceptance tests instead of cypress (#13069)

* fix up dependencies in package.json

* Change configs to playwright

* update types

* remove cypress

* add playwright

* Fix up imports

* Move up test files

* Fix up scripts

* Update user import

* Remove allowEditInvariantFromNonDefault=True.spec.ts

* Update docker container

* Run playwright on pipelines

* Install playwright

* change urls

* change to run playwright

* Update reporter

* create .env file when installing

* update pipelines

* Remove @in yml

* Update Yaml script to use New-Item

* Pipe object to Value

* Update yaml to use "" not {}

* Update localhost to proper port

* Push package-lock.json

* include dotenv in package.json

* Add back umbraco.config.ts

* Dont change launchSettings.json

* Fix up pipelines

* Change working directory

* Add logging

* Actually name the file

* Remove .env from path

* Add working directory

* Add working directory to script

* check env content

* Update more working dir

* Try making newline in YAML

* add quotes

* Try multiline infront of script

* Move top statement

* use https

* Update to localhost 44331

* ignore https in config

* Change linux to https

* add timeouts for tests

* Fix up url for linux

* Update docker to use port 44331 -> 5000

* increase timeout

* Update yaml

* Remove Cypress references and fix URL/port mapping

* Update umbraco-linux.docker

* Generate ASP.NET HTTPS development certificate

* Enable HTTPS development certificate in Docker

* Dont run failing tests

* Update HTTPS development certificate generation

* Copy nuget.config to correct location in Docker file

* do not run flaky test

* update outputdir

* Remove flaky tests

* Update to dot reporter

* Update to json-models-builders package

* Check if results folder exists

* Remove logging

* Use bash to find folder

* Dont use junit to report

* only publish if folder exists

* Add 5 retries for CI

* search correct folder

* Remove unused json file

* Use line reporter

* Remove umbraco.config.ts

* Remove more flaky test

* Add waits so we dont bombard SQLite

* Add page as parameter

* add page as parameter

* Remove flaky macro test

Co-authored-by: Zeegaan <nge@umbraco.dk>
Co-authored-by: Ronald Barendse <ronald@barend.se>

* Fix comment of view property in `ConfigurationFieldAttribute` (#13077)

* Fix comment of view property in `ConfigurationFieldAttribute`

* Update description of key property as well

* Use char instead of string for DetectJson (#13019)

* Use char instead of string for DetectJson

* Use faster method

* Change DetectIsJon method

* Update acceptance test readme & scripts (#13078)

* Update readme to reflect the change to playwright

* Update test scripts

* update README

* update pipelines to run new script

* update package.json scripts

* dont include demo test in package.json

* Add creation of blueprint test

* Implement create test script

Co-authored-by: Zeegaan <nge@umbraco.dk>

* Refactor event handler away from keyup to ng-change

* Don't use legacy icon for action delete

* Added nullability attribute to IsNullOrWhitespace

* Fixes umbraco/Umbraco-CMS.Accessibility.Issues #63 and #61

* Revert "Removing X-XSS-Protection healthcheck" (#13096)

This reverts commit 696475ebf2.

Co-authored-by: Zeegaan <nge@umbraco.dk>

* New backoffice: New Api controllers (#12983)

* Create migrate Language controller to Umbraco.Cms:ManagementApi

* Add proper language mapping

* Update mapping to handle if language name is null

* Uncomment code

* Add new language view model

* Add LanguageViewModelMapping

* Add mapper registration

* Fix up AddMappers extension method

* Implement mapping IEnumerable of languages

* Change action signatures to ViewModel instead of model

* Seperate logic from controller into service

* Move LanguageService.cs

* Register service

* Fix up mapping

* Add null check to mapping instead of controller

* Map to ILanguage instead of implementation

* Fix up null check

* Implement ProblemDetailsBuilder.cs

* Rename duplicate method

* Use builder in actions

* Implement new Paged models

* Create language controller base

* Use pagedModel for GetAllLanguages

* Create GetAllLanguagesPaged method

* Split language controller into single APIs

* Fix up controllers with API versions

* Map Total property

* Fix up route and naming for GetLanguageController.cs

* Fix up naming for language controllers

* New folder structure

* Add culture controllers

* Map CultureInfos to paged CultureViewModel

* Remove wierd include in csproj

* Refactor controller to return pagedViewModel instead of dictionary

* Fix up mapping to map single and enumerable

* Register mapping

* Add apiversion to controller

* Add inheritdoc

* Create DictionaryControllerBase.cs

* Add delete controller

* Only use HttpDelete for deletes

* Check also if language exists in service

* Split Save action into Create & Update actions

* Update Http attributes on Create and update controllers

* Proper routing for delete controller

* Add api version

* Make action async

* Implement CreateDictionaryController

* Create DictionaryViewModel.cs

* Use viewmodel instead of values

* Create get by int DictionaryController

* Add view models

* Rename controller

* Rename DictionaryViewModel to DictionaryItemViewModel

* use created instead of ID

* Apply DataContract/Datamember to view model

* change to guid instead of Guid

* Use proper responses instead of return models when creating/updating

* Implement new IDictionaryService

* Implement new MoveController

* Use new service in mapper

* Remove unused method

* Add DictionaryViewModelMapDefinition

* Create MoveOrCopyViewModel

* Proper Http action

* Create UpdateDictionary controller

* Map IDictionaryItem to DictionaryViewModel

* Add JsonPatching

* Add UpdateDictionaryController.cs

* Map DictionaryTranslationsDisplays properly

* ParentId should be nullable

* Add new DictionaryTranslationViewModel.cs

* Remove translationViewModel

* Add Id and Key to DictionaryTranslationDisplay.cs

* Implement IDictionaryFactory.cs

* Create DictionaryViewModels and do not use IEntity

* Map to new view models instead of displays

* Register the factiories in the service container

* Remove newtonsoft

* Add serializing to and from PatchDocs

* Use JsonPatchViewModel instead of object type

* Add JsonPatch.Net to csproj

* Implement JsonPatchService.cs

* Register JsonPatchService

* Make model non-nullable

* Update controllers to use new attributes

* Rename MoveViewModel.cs

* Remove NameIsDirty as that is legacy from how we used to handle updating

* Add GetAllDictionaryController

* Add todo to DictionaryControllerBase

* Add ExportDictionaryController

* Add ImportDictionaryController

* Remove unused umbraco mapper

* Add upload dictionary controller

* Create Dictionary import view models

* Update UploadDictionaryController with view models

* Remove unused using

* Implement pagedviewmodel for GetAllDictionaryController.cs

* Add dictionary overview viewmodels

* Add mapping for DictionaryOverViewModel

* Update Dictionary controller to use new viewmodel

* Fix up attributes for UploadDictionaryController

* Make actions async

* Make controller bases abstract

* Fix after merge

* New backoffice: Analytics controller (#12957)

* Add AnalyticsControllerBase

* Add AnalyticsViewModel

* Add GetAnalyticsController

* Update ViewModel to use System.Text.Json

* Add SetAnalyticsController

* Add AnalyticsLevelViewModel

* Add GetAllAnalyticsController

* Add viewmodel factory

* Register factory for DI

* Use factory for creation of ViewModel

* Fix up AnalyticsLevelViewModel.cs

* Use analyticsLevelViewModel

* Add Enum validation to controller

* Add OpenApi attributes

* Add routing to GetAllAnalyticsController

* Rename IPagedViewModelFactory

* use new renamed PagedViewModelFactory

* Make actions async

* Make controller base abstract

* Fix up after merge

Co-authored-by: Zeegaan <nge@umbraco.dk>

* New backoffice: Help controller (#12958)

* Add HelpControllerBase

* Add HelpPageViewModel

* Add GetHelpController

* Add viewmodel factory

* Register factory for DI

* Use PagedViewModelFactory for controller

* Update baseurl to be nullable

* Rename IPagedViewModelFactory

* Use new renamed IPagedViewModelFactory

* Dont use httpclient as field

Co-authored-by: Zeegaan <nge@umbraco.dk>

* New backoffice: Relation controller (#12961)

* Add relation controller base

* Add commen about auth

* Add GetRelationController

* Use mapping to viewmodel

* Add RelationViewModel

* Add RelationViewModelsMapDefinition.cs

* Add viewmodel factory

* Register factory for DI

* Rename IPagedViewModelFactory

* Add RelationViewModelFactory

* Remove unused service

* Add GetByChildRelationController

* Add relationViewModelFactory to DI

* Add MappingBuilderExtensions

* Add relationViewModelsMapDefinition to DI

* Use PagedViewModel for child relations

* Add CreateMultiple

* Update GetRelationController to use factory instead of direct mapping

* Update GetByChildRelationController to use relationViewModelFactory

* Fix up after merge

Co-authored-by: Zeegaan <nge@umbraco.dk>

* New backoffice: Tracked references controller (#12966)

* Add TrackedReferencesControllerBase.cs

* Add GetTrackedReferencesController

* Add relation model

* Add ITrackedReferencesRepository.cs

* Rename relation to RelationModel

* Add RelationMapDefinition.cs

* Add TrackedReferencesSkipTakeRepository

* Rename RelationModelMapDefinition

* Add new services to DI

* Rename RelationModel to RelationItemModel

* Implement TrackedReferencesSkipTakeService

* Formatting

* Add RelationItemViewModel.cs

* Add mapdefition

* Update TrackedReferencesController to use new PagedViewModel

* Add service to DI

* imlement proper routing

* Add async to GetTrackedReferencesController.cs

* Rename action to get

* Add DescendantTrackedReferencesController

* make filterMustBeIsDependency nullable

* Use count instead of capacity

* Rename controller

* Add MultipleTrackedReferencesController.cs

* Refactor TrackedReferencesService to not return pagedViewModel

* Remove TODO untill consensus on convention

* Formatting

* Delete old duplicate controller

* Fix up naming

* Fix up naming and fixed todo

* Fix up mapdefinition registrations

* Rename controllers

* Fix naming

* Fix nullable tree

* Fix up relation controller & action names

* Fix naming

* Fix up multiple to not be post

* Apply suggestions from code review

Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>

* Remove [ApiVersion] from each individual controller and added to base instead

* Added missing semi-colon

* Update all "non-async async" endpoints to return Task.FromResult

* Fixed up LanguageViewModels namespace

* Return proper status code

* Update xml documentation

* Rename GetAllCultureController

* Change return type attribute to proper Model

* Change return type attribute to correct model

* Add clarifying comment

* Change return type attribute to proper model

* Rename ById to by key

* Update DictionaryOverviewViewModel to use Key instead of ID

* Implement factory method to create viewModel instead of using services in mapper

* Fix up DictionaryItemViewModel to use Parent GUID instead of ID

* Update return type attribute to correct model

* Update key to actual GUID instead of string

* Update route to not include "delete"

* Remove redundant IActionResult specification

* Update responseType attribute to correct models

* Update OpenApiTag for DictionaryControllerBase

* Update ResponseType attribute to correct models

* update variable name to not be "XD"

* Update ResponseType attribute to correct model

* Update route to not include "update" as it is redundant

* Update produces responsetype attributes to correct models

* Use IJsonSerializer abstraction instead of JsonConvert directly

* Remove unused field

* Change ResponseType attribute to correct model

* change TrackedReferencesSkipTakeService to return pagedModels directly

* Remove duplicate DI

* change to better variable names

* Move RelationItemModel to core

* Remove empty folder

* Remove ITrackedReferencesSkipTakeService and add it to TrackedReferencesService

* remove TrackedReferencesSkipTakeRepository and add to TrackedReferencesRepository.cs

* Fix up TrackedReferencesService to not use SkipTake repository

* Remove AddRepositories from ManagementApiComposer

* Transition to ManagementApiBaseController

* Fixes ResponseType attribute to correct model

* Add loading files to service instead of having logic in controller

* Add todo comment

* Fix up routing for delete language

* Use problem details builder

* Add SystemTextJsonSerializer

* Add SystemTextJsonSerializer

* Remove unused usings

* Remove obsolete MoveDictionaryController

* Remove CreateDate and UpdateDate from DictionaryViewModel

* Change response type to correct model

* Remove PagedViewModelFactory.cs

* Add obsolete message

* Fix installer view models

A setter is required for the modelbinder to be able to do its work

* Update src/Umbraco.Cms.ManagementApi/Controllers/Analytics/SetAnalyticsController.cs

Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>

* Update ResponseType to correct Model

* Update comment

* Add FileUploadService

* Add DictionaryFactory.cs to handle creation of viewmodel

* Remove unused DI

* Rename actions & controllers to reflect eachother

* Update OpenApi.json

* Add dictionary to openapi

* Update in proper alphabetical order

* Add trackedReferences to openapi

* Fix open api doc

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
Co-authored-by: Zeegaan <nge@umbraco.dk>

* Backport project cleanup from #12907

* Remove empty Directory.Build.props

* Fix GenerateAppsettingsSchema target

* Re-add empty Directory.Build.props to prevent inheritance

* Re-add missing JsonPatch.Net dependency

* Fix merge issues (redundant TargetFramework property and appsettings-schema.json generation)

* Fix and improve OpenAPI test assertion

Co-authored-by: Sebastiaan Janssen <sebastiaan@umbraco.com>
Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
Co-authored-by: Zeegaan <nge@umbraco.dk>
Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
Co-authored-by: Mads Rasmussen <madsr@hey.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Co-authored-by: Bjarne Fyrstenborg <bjarne_fyrstenborg@hotmail.com>
Co-authored-by: Erik-Jan Westendorp <erikjanwestendorp@outlook.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Sean <29239704+Bakersbakebread@users.noreply.github.com>
Co-authored-by: Lucas Bach Bisgaard <rammi@rammi.dk>
Co-authored-by: Lucas Bach Bisgaard <lom@novicell.dk>
Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>
Co-authored-by: Busra Sengul <aysebusrasengul@gmail.com>
Co-authored-by: Justin Neville <justin@nevitech.co.uk>
Co-authored-by: Jeavon Leopold <jeavon@crumpled-dog.com>
Co-authored-by: Austin Gilbert <AGilbert@rbaconsulting.com>
Co-authored-by: patrickdemooij9 <patrickdemooij98@hotmail.com>
Co-authored-by: bakersbakebread <hello@seanthorne.co.uk>
Co-authored-by: Karl Butler <kbutler@carbonsix.digital>
2022-10-05 12:14:43 +02:00

1559 lines
56 KiB
C#

// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Strings;
namespace Umbraco.Extensions;
/// <summary>
/// String extension methods
/// </summary>
public static class StringExtensions
{
internal static readonly Lazy<Regex> Whitespace = new(() => new Regex(@"\s+", RegexOptions.Compiled));
private const char DefaultEscapedStringEscapeChar = '\\';
private static readonly char[] ToCSharpHexDigitLower = "0123456789abcdef".ToCharArray();
private static readonly char[] ToCSharpEscapeChars;
internal static readonly string[] JsonEmpties = { "[]", "{}" };
/// <summary>
/// The namespace for URLs (from RFC 4122, Appendix C).
/// See <a href="http://www.ietf.org/rfc/rfc4122.txt">RFC 4122</a>
/// </summary>
internal static readonly Guid UrlNamespace = new("6ba7b811-9dad-11d1-80b4-00c04fd430c8");
private static readonly char[] CleanForXssChars = "*?(){}[];:%<>/\\|&'\"".ToCharArray();
// From: http://stackoverflow.com/a/961504/5018
// filters control characters but allows only properly-formed surrogate sequences
private static readonly Lazy<Regex> InvalidXmlChars = new(() =>
new Regex(
@"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]",
RegexOptions.Compiled));
static StringExtensions()
{
var escapes = new[] { "\aa", "\bb", "\ff", "\nn", "\rr", "\tt", "\vv", "\"\"", "\\\\", "??", "\00" };
ToCSharpEscapeChars = new char[escapes.Max(e => e[0]) + 1];
foreach (var escape in escapes)
{
ToCSharpEscapeChars[escape[0]] = escape[1];
}
}
/// <summary>
/// Convert a path to node ids in the order from right to left (deepest to shallowest)
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static int[] GetIdsFromPathReversed(this string path)
{
var nodeIds = path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries)
.Select(x =>
int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var output)
? Attempt<int>.Succeed(output)
: Attempt<int>.Fail())
.Where(x => x.Success)
.Select(x => x.Result)
.Reverse()
.ToArray();
return nodeIds;
}
/// <summary>
/// Removes new lines and tabs
/// </summary>
/// <param name="txt"></param>
/// <returns></returns>
public static string StripWhitespace(this string txt) => Regex.Replace(txt, @"\s", string.Empty);
public static string StripFileExtension(this string fileName)
{
// filenames cannot contain line breaks
if (fileName.Contains(Environment.NewLine) || fileName.Contains("\r") || fileName.Contains("\n"))
{
return fileName;
}
var lastIndex = fileName.LastIndexOf('.');
if (lastIndex > 0)
{
var ext = fileName.Substring(lastIndex);
// file extensions cannot contain whitespace
if (ext.Contains(" "))
{
return fileName;
}
return string.Format("{0}", fileName.Substring(0, fileName.IndexOf(ext, StringComparison.Ordinal)));
}
return fileName;
}
/// <summary>
/// Determines the extension of the path or URL
/// </summary>
/// <param name="file"></param>
/// <returns>Extension of the file</returns>
public static string GetFileExtension(this string file)
{
// Find any characters between the last . and the start of a query string or the end of the string
const string pattern = @"(?<extension>\.[^\.\?]+)(\?.*|$)";
Match match = Regex.Match(file, pattern);
return match.Success
? match.Groups["extension"].Value
: string.Empty;
}
/// <summary>
/// This tries to detect a json string, this is not a fail safe way but it is quicker than doing
/// a try/catch when deserializing when it is not json.
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static bool DetectIsJson(this string input)
{
if (input.IsNullOrWhiteSpace())
{
return false;
}
input = input.Trim();
return (input[0] is '[' && input[^1] is ']') || (input[0] is '{' && input[^1] is '}');
}
public static bool DetectIsEmptyJson(this string input) =>
JsonEmpties.Contains(Whitespace.Value.Replace(input, string.Empty));
public static string ReplaceNonAlphanumericChars(this string input, string replacement)
{
// any character that is not alphanumeric, convert to a hyphen
var mName = input;
foreach (var c in mName.ToCharArray().Where(c => !char.IsLetterOrDigit(c)))
{
mName = mName.Replace(c.ToString(CultureInfo.InvariantCulture), replacement);
}
return mName;
}
public static string ReplaceNonAlphanumericChars(this string input, char replacement)
{
var inputArray = input.ToCharArray();
var outputArray = new char[input.Length];
for (var i = 0; i < inputArray.Length; i++)
{
outputArray[i] = char.IsLetterOrDigit(inputArray[i]) ? inputArray[i] : replacement;
}
return new string(outputArray);
}
/// <summary>
/// Cleans string to aid in preventing xss attacks.
/// </summary>
/// <param name="input"></param>
/// <param name="ignoreFromClean"></param>
/// <returns></returns>
public static string CleanForXss(this string input, params char[] ignoreFromClean)
{
// remove any HTML
input = input.StripHtml();
// strip out any potential chars involved with XSS
return input.ExceptChars(new HashSet<char>(CleanForXssChars.Except(ignoreFromClean)));
}
public static string ExceptChars(this string str, HashSet<char> toExclude)
{
var sb = new StringBuilder(str.Length);
foreach (var c in str.Where(c => toExclude.Contains(c) == false))
{
sb.Append(c);
}
return sb.ToString();
}
/// <summary>
/// This will append the query string to the URL
/// </summary>
/// <param name="url"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
/// <remarks>
/// This methods ensures that the resulting URL is structured correctly, that there's only one '?' and that things are
/// delimited properly with '&'
/// </remarks>
public static string AppendQueryStringToUrl(this string url, params string[] queryStrings)
{
// remove any prefixed '&' or '?'
for (var i = 0; i < queryStrings.Length; i++)
{
queryStrings[i] = queryStrings[i].TrimStart(Constants.CharArrays.QuestionMarkAmpersand)
.TrimEnd(Constants.CharArrays.Ampersand);
}
var nonEmpty = queryStrings.Where(x => !x.IsNullOrWhiteSpace()).ToArray();
if (url.Contains("?"))
{
return url + string.Join("&", nonEmpty).EnsureStartsWith('&');
}
return url + string.Join("&", nonEmpty).EnsureStartsWith('?');
}
/// <summary>
/// Returns a stream from a string
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
internal static Stream GenerateStreamFromString(this string s)
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.Write(s);
writer.Flush();
stream.Position = 0;
return stream;
}
// this is from SqlMetal and just makes it a bit of fun to allow pluralization
public static string MakePluralName(this string name)
{
if (name.EndsWith("x", StringComparison.OrdinalIgnoreCase) ||
name.EndsWith("ch", StringComparison.OrdinalIgnoreCase) ||
name.EndsWith("s", StringComparison.OrdinalIgnoreCase) ||
name.EndsWith("sh", StringComparison.OrdinalIgnoreCase))
{
name += "es";
return name;
}
if (name.EndsWith("y", StringComparison.OrdinalIgnoreCase) && name.Length > 1 &&
!IsVowel(name[^2]))
{
name = name.Remove(name.Length - 1, 1);
name += "ies";
return name;
}
if (!name.EndsWith("s", StringComparison.OrdinalIgnoreCase))
{
name += "s";
}
return name;
}
public static bool IsVowel(this char c)
{
switch (c)
{
case 'O':
case 'U':
case 'Y':
case 'A':
case 'E':
case 'I':
case 'o':
case 'u':
case 'y':
case 'a':
case 'e':
case 'i':
return true;
}
return false;
}
/// <summary>
/// Trims the specified value from a string; accepts a string input whereas the in-built implementation only accepts
/// char or char[].
/// </summary>
/// <param name="value">The value.</param>
/// <param name="forRemoving">For removing.</param>
/// <returns></returns>
public static string Trim(this string value, string forRemoving)
{
if (string.IsNullOrEmpty(value))
{
return value;
}
return value.TrimEnd(forRemoving).TrimStart(forRemoving);
}
public static string EncodeJsString(this string s)
{
var sb = new StringBuilder();
foreach (var c in s)
{
switch (c)
{
case '\"':
sb.Append("\\\"");
break;
case '\\':
sb.Append("\\\\");
break;
case '\b':
sb.Append("\\b");
break;
case '\f':
sb.Append("\\f");
break;
case '\n':
sb.Append("\\n");
break;
case '\r':
sb.Append("\\r");
break;
case '\t':
sb.Append("\\t");
break;
default:
int i = c;
if (i < 32 || i > 127)
{
sb.AppendFormat("\\u{0:X04}", i);
}
else
{
sb.Append(c);
}
break;
}
}
return sb.ToString();
}
public static string TrimEnd(this string value, string forRemoving)
{
if (string.IsNullOrEmpty(value))
{
return value;
}
if (string.IsNullOrEmpty(forRemoving))
{
return value;
}
while (value.EndsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase))
{
value = value.Remove(value.LastIndexOf(forRemoving, StringComparison.InvariantCultureIgnoreCase));
}
return value;
}
public static string TrimStart(this string value, string forRemoving)
{
if (string.IsNullOrEmpty(value))
{
return value;
}
if (string.IsNullOrEmpty(forRemoving))
{
return value;
}
while (value.StartsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase))
{
value = value.Substring(forRemoving.Length);
}
return value;
}
public static string EnsureStartsWith(this string input, string toStartWith)
{
if (input.StartsWith(toStartWith))
{
return input;
}
return toStartWith + input.TrimStart(toStartWith);
}
public static string EnsureStartsWith(this string input, char value) =>
input.StartsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : value + input;
public static string EnsureEndsWith(this string input, char value) =>
input.EndsWith(value.ToString(CultureInfo.InvariantCulture)) ? input : input + value;
public static string EnsureEndsWith(this string input, string toEndWith) =>
input.EndsWith(toEndWith.ToString(CultureInfo.InvariantCulture)) ? input : input + toEndWith;
public static bool IsLowerCase(this char ch) => ch.ToString(CultureInfo.InvariantCulture) ==
ch.ToString(CultureInfo.InvariantCulture).ToLowerInvariant();
public static bool IsUpperCase(this char ch) => ch.ToString(CultureInfo.InvariantCulture) ==
ch.ToString(CultureInfo.InvariantCulture).ToUpperInvariant();
/// <summary>
/// Indicates whether a specified string is null, empty, or
/// consists only of white-space characters.
/// </summary>
/// <param name="value">The value to check.</param>
/// <returns>
/// Returns <see langword="true" /> if the value is null,
/// empty, or consists only of white-space characters, otherwise
/// returns <see langword="false" />.
/// </returns>
public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? value) => string.IsNullOrWhiteSpace(value);
[return: NotNullIfNotNull("defaultValue")]
public static string? IfNullOrWhiteSpace(this string? str, string? defaultValue) =>
str.IsNullOrWhiteSpace() ? defaultValue : str;
/// <summary>The to delimited list.</summary>
/// <param name="list">The list.</param>
/// <param name="delimiter">The delimiter.</param>
/// <returns>the list</returns>
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "By design")]
public static IList<string> ToDelimitedList(this string list, string delimiter = ",")
{
var delimiters = new[] { delimiter };
return !list.IsNullOrWhiteSpace()
? list.Split(delimiters, StringSplitOptions.RemoveEmptyEntries)
.Select(i => i.Trim())
.ToList()
: new List<string>();
}
/// <summary>enum try parse.</summary>
/// <param name="strType">The str type.</param>
/// <param name="ignoreCase">The ignore case.</param>
/// <param name="result">The result.</param>
/// <typeparam name="T">The type</typeparam>
/// <returns>The enum try parse.</returns>
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "By Design")]
[SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By Design")]
public static bool EnumTryParse<T>(this string strType, bool ignoreCase, out T? result)
{
try
{
result = (T)Enum.Parse(typeof(T), strType, ignoreCase);
return true;
}
catch
{
result = default;
return false;
}
}
/// <summary>
/// Parse string to Enum
/// </summary>
/// <typeparam name="T">The enum type</typeparam>
/// <param name="strType">The string to parse</param>
/// <param name="ignoreCase">The ignore case</param>
/// <returns>The parsed enum</returns>
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "By Design")]
[SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By Design")]
public static T EnumParse<T>(this string strType, bool ignoreCase) => (T)Enum.Parse(typeof(T), strType, ignoreCase);
/// <summary>
/// Strips all HTML from a string.
/// </summary>
/// <param name="text">The text.</param>
/// <returns>Returns the string without any HTML tags.</returns>
public static string StripHtml(this string text)
{
const string pattern = @"<(.|\n)*?>";
return Regex.Replace(text, pattern, string.Empty, RegexOptions.Compiled);
}
/// <summary>
/// Encodes as GUID.
/// </summary>
/// <param name="input">The input.</param>
/// <returns></returns>
public static Guid EncodeAsGuid(this string input)
{
if (string.IsNullOrWhiteSpace(input))
{
throw new ArgumentNullException("input");
}
var convertToHex = input.ConvertToHex();
var hexLength = convertToHex.Length < 32 ? convertToHex.Length : 32;
var hex = convertToHex.Substring(0, hexLength).PadLeft(32, '0');
Guid output = Guid.Empty;
return Guid.TryParse(hex, out output) ? output : Guid.Empty;
}
/// <summary>
/// Converts to hex.
/// </summary>
/// <param name="input">The input.</param>
/// <returns></returns>
public static string ConvertToHex(this string input)
{
if (string.IsNullOrEmpty(input))
{
return string.Empty;
}
var sb = new StringBuilder(input.Length);
foreach (var c in input)
{
sb.AppendFormat("{0:x2}", Convert.ToUInt32(c));
}
return sb.ToString();
}
public static string DecodeFromHex(this string hexValue)
{
var strValue = string.Empty;
while (hexValue.Length > 0)
{
strValue += Convert.ToChar(Convert.ToUInt32(hexValue.Substring(0, 2), 16)).ToString();
hexValue = hexValue.Substring(2, hexValue.Length - 2);
}
return strValue;
}
/// <summary>
/// Encodes a string to a safe URL base64 string
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static string ToUrlBase64(this string input)
{
if (input == null)
{
throw new ArgumentNullException(nameof(input));
}
if (string.IsNullOrEmpty(input))
{
return string.Empty;
}
// return Convert.ToBase64String(bytes).Replace(".", "-").Replace("/", "_").Replace("=", ",");
var bytes = Encoding.UTF8.GetBytes(input);
return UrlTokenEncode(bytes);
}
/// <summary>
/// Decodes a URL safe base64 string back
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static string? FromUrlBase64(this string input)
{
if (input == null)
{
throw new ArgumentNullException(nameof(input));
}
// if (input.IsInvalidBase64()) return null;
try
{
// var decodedBytes = Convert.FromBase64String(input.Replace("-", ".").Replace("_", "/").Replace(",", "="));
var decodedBytes = UrlTokenDecode(input);
return decodedBytes != null ? Encoding.UTF8.GetString(decodedBytes) : null;
}
catch (FormatException)
{
return null;
}
}
/// <summary>
/// formats the string with invariant culture
/// </summary>
/// <param name="format">The format.</param>
/// <param name="args">The args.</param>
/// <returns></returns>
public static string InvariantFormat(this string? format, params object?[] args) =>
string.Format(CultureInfo.InvariantCulture, format ?? string.Empty, args);
/// <summary>
/// Converts an integer to an invariant formatted string
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static string ToInvariantString(this int str) => str.ToString(CultureInfo.InvariantCulture);
public static string ToInvariantString(this long str) => str.ToString(CultureInfo.InvariantCulture);
/// <summary>
/// Compares 2 strings with invariant culture and case ignored
/// </summary>
/// <param name="compare">The compare.</param>
/// <param name="compareTo">The compare to.</param>
/// <returns></returns>
public static bool InvariantEquals(this string? compare, string? compareTo) =>
string.Equals(compare, compareTo, StringComparison.InvariantCultureIgnoreCase);
public static bool InvariantStartsWith(this string compare, string compareTo) =>
compare.StartsWith(compareTo, StringComparison.InvariantCultureIgnoreCase);
public static bool InvariantEndsWith(this string compare, string compareTo) =>
compare.EndsWith(compareTo, StringComparison.InvariantCultureIgnoreCase);
public static bool InvariantContains(this string compare, string compareTo) =>
compare.IndexOf(compareTo, StringComparison.OrdinalIgnoreCase) >= 0;
public static bool InvariantContains(this IEnumerable<string> compare, string compareTo) =>
compare.Contains(compareTo, StringComparer.InvariantCultureIgnoreCase);
public static int InvariantIndexOf(this string s, string value) =>
s.IndexOf(value, StringComparison.OrdinalIgnoreCase);
public static int InvariantLastIndexOf(this string s, string value) =>
s.LastIndexOf(value, StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Tries to parse a string into the supplied type by finding and using the Type's "Parse" method
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="val"></param>
/// <returns></returns>
public static T? ParseInto<T>(this string val) => (T?)val.ParseInto(typeof(T));
/// <summary>
/// Tries to parse a string into the supplied type by finding and using the Type's "Parse" method
/// </summary>
/// <param name="val"></param>
/// <param name="type"></param>
/// <returns></returns>
public static object? ParseInto(this string val, Type type)
{
if (string.IsNullOrEmpty(val) == false)
{
TypeConverter tc = TypeDescriptor.GetConverter(type);
return tc.ConvertFrom(val);
}
return val;
}
/// <summary>
/// Generates a hash of a string based on the FIPS compliance setting.
/// </summary>
/// <param name="str">Refers to itself</param>
/// <returns>The hashed string</returns>
public static string GenerateHash(this string str) => str.ToSHA1();
/// <summary>
/// Generate a hash of a string based on the specified hash algorithm.
/// </summary>
/// <typeparam name="T">The hash algorithm implementation to use.</typeparam>
/// <param name="str">The <see cref="string" /> to hash.</param>
/// <returns>
/// The hashed string.
/// </returns>
public static string GenerateHash<T>(this string str)
where T : HashAlgorithm => str.GenerateHash(typeof(T).FullName);
/// <summary>
/// Converts the string to SHA1
/// </summary>
/// <param name="stringToConvert">refers to itself</param>
/// <returns>The SHA1 hashed string</returns>
public static string ToSHA1(this string stringToConvert) => stringToConvert.GenerateHash("SHA1");
/// <summary>
/// Decodes a string that was encoded with UrlTokenEncode
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static byte[] UrlTokenDecode(this string input)
{
if (input == null)
{
throw new ArgumentNullException(nameof(input));
}
if (input.Length == 0)
{
return Array.Empty<byte>();
}
// calc array size - must be groups of 4
var arrayLength = input.Length;
var remain = arrayLength % 4;
if (remain != 0)
{
arrayLength += 4 - remain;
}
var inArray = new char[arrayLength];
for (var i = 0; i < input.Length; i++)
{
var ch = input[i];
switch (ch)
{
case '-': // restore '-' as '+'
inArray[i] = '+';
break;
case '_': // restore '_' as '/'
inArray[i] = '/';
break;
default: // keep char unchanged
inArray[i] = ch;
break;
}
}
// pad with '='
for (var j = input.Length; j < inArray.Length; j++)
{
inArray[j] = '=';
}
return Convert.FromBase64CharArray(inArray, 0, inArray.Length);
}
/// <summary>
/// Generate a hash of a string based on the hashType passed in
/// </summary>
/// <param name="str">Refers to itself</param>
/// <param name="hashType">
/// String with the hash type. See remarks section of the CryptoConfig Class in MSDN docs for a
/// list of possible values.
/// </param>
/// <returns>The hashed string</returns>
private static string GenerateHash(this string str, string? hashType)
{
HashAlgorithm? hasher = null;
// create an instance of the correct hashing provider based on the type passed in
if (hashType is not null)
{
hasher = HashAlgorithm.Create(hashType);
}
if (hasher == null)
{
throw new InvalidOperationException("No hashing type found by name " + hashType);
}
using (hasher)
{
// convert our string into byte array
var byteArray = Encoding.UTF8.GetBytes(str);
// get the hashed values created by our selected provider
var hashedByteArray = hasher.ComputeHash(byteArray);
// create a StringBuilder object
var stringBuilder = new StringBuilder();
// loop to each byte
foreach (var b in hashedByteArray)
{
// append it to our StringBuilder
stringBuilder.Append(b.ToString("x2"));
}
// return the hashed value
return stringBuilder.ToString();
}
}
/// <summary>
/// Encodes a string so that it is 'safe' for URLs, files, etc..
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static string UrlTokenEncode(this byte[] input)
{
if (input == null)
{
throw new ArgumentNullException(nameof(input));
}
if (input.Length == 0)
{
return string.Empty;
}
// base-64 digits are A-Z, a-z, 0-9, + and /
// the = char is used for trailing padding
var str = Convert.ToBase64String(input);
var pos = str.IndexOf('=');
if (pos < 0)
{
pos = str.Length;
}
// replace chars that would cause problems in URLs
var chArray = new char[pos];
for (var i = 0; i < pos; i++)
{
var ch = str[i];
switch (ch)
{
case '+': // replace '+' with '-'
chArray[i] = '-';
break;
case '/': // replace '/' with '_'
chArray[i] = '_';
break;
default: // keep char unchanged
chArray[i] = ch;
break;
}
}
return new string(chArray);
}
/// <summary>
/// Ensures that the folder path ends with a DirectorySeparatorChar
/// </summary>
/// <param name="currentFolder"></param>
/// <returns></returns>
public static string NormaliseDirectoryPath(this string currentFolder)
{
currentFolder = currentFolder
.IfNull(x => string.Empty)
.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
return currentFolder;
}
/// <summary>
/// Truncates the specified text string.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="maxLength">Length of the max.</param>
/// <param name="suffix">The suffix.</param>
/// <returns></returns>
public static string Truncate(this string text, int maxLength, string suffix = "...")
{
// replaces the truncated string to a ...
var truncatedString = text;
if (maxLength <= 0)
{
return truncatedString;
}
var strLength = maxLength - suffix.Length;
if (strLength <= 0)
{
return truncatedString;
}
if (text == null || text.Length <= maxLength)
{
return truncatedString;
}
truncatedString = text.Substring(0, strLength);
truncatedString = truncatedString.TrimEnd();
truncatedString += suffix;
return truncatedString;
}
/// <summary>
/// Strips carrage returns and line feeds from the specified text.
/// </summary>
/// <param name="input">The input.</param>
/// <returns></returns>
public static string StripNewLines(this string input) => input.Replace("\r", string.Empty).Replace("\n", string.Empty);
/// <summary>
/// Converts to single line by replacing line breaks with spaces.
/// </summary>
public static string ToSingleLine(this string text)
{
if (string.IsNullOrEmpty(text))
{
return text;
}
text = text.Replace("\r\n", " "); // remove CRLF
text = text.Replace("\r", " "); // remove CR
text = text.Replace("\n", " "); // remove LF
return text;
}
public static string OrIfNullOrWhiteSpace(this string input, string alternative) =>
!string.IsNullOrWhiteSpace(input)
? input
: alternative;
/// <summary>
/// Returns a copy of the string with the first character converted to uppercase.
/// </summary>
/// <param name="input">The string.</param>
/// <returns>The converted string.</returns>
public static string ToFirstUpper(this string input) =>
string.IsNullOrWhiteSpace(input)
? input
: input.Substring(0, 1).ToUpper() + input.Substring(1);
/// <summary>
/// Returns a copy of the string with the first character converted to lowercase.
/// </summary>
/// <param name="input">The string.</param>
/// <returns>The converted string.</returns>
public static string ToFirstLower(this string input) =>
string.IsNullOrWhiteSpace(input)
? input
: input.Substring(0, 1).ToLower() + input.Substring(1);
/// <summary>
/// Returns a copy of the string with the first character converted to uppercase using the casing rules of the
/// specified culture.
/// </summary>
/// <param name="input">The string.</param>
/// <param name="culture">The culture.</param>
/// <returns>The converted string.</returns>
public static string ToFirstUpper(this string input, CultureInfo culture) =>
string.IsNullOrWhiteSpace(input)
? input
: input.Substring(0, 1).ToUpper(culture) + input.Substring(1);
/// <summary>
/// Returns a copy of the string with the first character converted to lowercase using the casing rules of the
/// specified culture.
/// </summary>
/// <param name="input">The string.</param>
/// <param name="culture">The culture.</param>
/// <returns>The converted string.</returns>
public static string ToFirstLower(this string input, CultureInfo culture) =>
string.IsNullOrWhiteSpace(input)
? input
: input.Substring(0, 1).ToLower(culture) + input.Substring(1);
/// <summary>
/// Returns a copy of the string with the first character converted to uppercase using the casing rules of the
/// invariant culture.
/// </summary>
/// <param name="input">The string.</param>
/// <returns>The converted string.</returns>
public static string ToFirstUpperInvariant(this string input) =>
string.IsNullOrWhiteSpace(input)
? input
: input.Substring(0, 1).ToUpperInvariant() + input.Substring(1);
/// <summary>
/// Returns a copy of the string with the first character converted to lowercase using the casing rules of the
/// invariant culture.
/// </summary>
/// <param name="input">The string.</param>
/// <returns>The converted string.</returns>
public static string ToFirstLowerInvariant(this string input) =>
string.IsNullOrWhiteSpace(input)
? input
: input.Substring(0, 1).ToLowerInvariant() + input.Substring(1);
/// <summary>
/// Returns a new string in which all occurrences of specified strings are replaced by other specified strings.
/// </summary>
/// <param name="text">The string to filter.</param>
/// <param name="replacements">The replacements definition.</param>
/// <returns>The filtered string.</returns>
public static string ReplaceMany(this string text, IDictionary<string, string> replacements)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
if (replacements == null)
{
throw new ArgumentNullException(nameof(replacements));
}
foreach (KeyValuePair<string, string> item in replacements)
{
text = text.Replace(item.Key, item.Value);
}
return text;
}
/// <summary>
/// Returns a new string in which all occurrences of specified characters are replaced by a specified character.
/// </summary>
/// <param name="text">The string to filter.</param>
/// <param name="chars">The characters to replace.</param>
/// <param name="replacement">The replacement character.</param>
/// <returns>The filtered string.</returns>
public static string ReplaceMany(this string text, char[] chars, char replacement)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
if (chars == null)
{
throw new ArgumentNullException(nameof(chars));
}
for (var i = 0; i < chars.Length; i++)
{
text = text.Replace(chars[i], replacement);
}
return text;
}
/// <summary>
/// Returns a new string in which only the first occurrence of a specified string is replaced by a specified
/// replacement string.
/// </summary>
/// <param name="text">The string to filter.</param>
/// <param name="search">The string to replace.</param>
/// <param name="replace">The replacement string.</param>
/// <returns>The filtered string.</returns>
public static string ReplaceFirst(this string text, string search, string replace)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
var pos = text.IndexOf(search, StringComparison.InvariantCulture);
if (pos < 0)
{
return text;
}
return text.Substring(0, pos) + replace + text.Substring(pos + search.Length);
}
/// <summary>
/// An extension method that returns a new string in which all occurrences of a
/// specified string in the current instance are replaced with another specified string.
/// StringComparison specifies the type of search to use for the specified string.
/// </summary>
/// <param name="source">Current instance of the string</param>
/// <param name="oldString">Specified string to replace</param>
/// <param name="newString">Specified string to inject</param>
/// <param name="stringComparison">String Comparison object to specify search type</param>
/// <returns>Updated string</returns>
public static string Replace(this string source, string oldString, string newString, StringComparison stringComparison)
{
// This initialization ensures the first check starts at index zero of the source. On successive checks for
// a match, the source is skipped to immediately after the last replaced occurrence for efficiency
// and to avoid infinite loops when oldString and newString compare equal.
var index = -1 * newString.Length;
// Determine if there are any matches left in source, starting from just after the result of replacing the last match.
while ((index = source.IndexOf(oldString, index + newString.Length, stringComparison)) >= 0)
{
// Remove the old text.
source = source.Remove(index, oldString.Length);
// Add the replacement text.
source = source.Insert(index, newString);
}
return source;
}
/// <summary>
/// Converts a literal string into a C# expression.
/// </summary>
/// <param name="s">Current instance of the string.</param>
/// <returns>The string in a C# format.</returns>
public static string ToCSharpString(this string s)
{
if (s == null)
{
return "<null>";
}
// http://stackoverflow.com/questions/323640/can-i-convert-a-c-sharp-string-value-to-an-escaped-string-literal
var sb = new StringBuilder(s.Length + 2);
for (var rp = 0; rp < s.Length; rp++)
{
var c = s[rp];
if (c < ToCSharpEscapeChars.Length && ToCSharpEscapeChars[c] != '\0')
{
sb.Append('\\').Append(ToCSharpEscapeChars[c]);
}
else if (c <= '~' && c >= ' ')
{
sb.Append(c);
}
else
{
sb.Append(@"\x")
.Append(ToCSharpHexDigitLower[(c >> 12) & 0x0F])
.Append(ToCSharpHexDigitLower[(c >> 8) & 0x0F])
.Append(ToCSharpHexDigitLower[(c >> 4) & 0x0F])
.Append(ToCSharpHexDigitLower[c & 0x0F]);
}
}
return sb.ToString();
// requires full trust
/*
using (var writer = new StringWriter())
using (var provider = CodeDomProvider.CreateProvider("CSharp"))
{
provider.GenerateCodeFromExpression(new CodePrimitiveExpression(s), writer, null);
return writer.ToString().Replace(string.Format("\" +{0}\t\"", Environment.NewLine), "");
}
*/
}
public static string EscapeRegexSpecialCharacters(this string text)
{
var regexSpecialCharacters = new Dictionary<string, string>
{
{ ".", @"\." },
{ "(", @"\(" },
{ ")", @"\)" },
{ "]", @"\]" },
{ "[", @"\[" },
{ "{", @"\{" },
{ "}", @"\}" },
{ "?", @"\?" },
{ "!", @"\!" },
{ "$", @"\$" },
{ "^", @"\^" },
{ "+", @"\+" },
{ "*", @"\*" },
{ "|", @"\|" },
{ "<", @"\<" },
{ ">", @"\>" },
};
return ReplaceMany(text, regexSpecialCharacters);
}
/// <summary>
/// Checks whether a string "haystack" contains within it any of the strings in the "needles" collection and returns
/// true if it does or false if it doesn't
/// </summary>
/// <param name="haystack">The string to check</param>
/// <param name="needles">The collection of strings to check are contained within the first string</param>
/// <param name="comparison">
/// The type of comparison to perform - defaults to <see cref="StringComparison.CurrentCulture" />
/// </param>
/// <returns>True if any of the needles are contained with haystack; otherwise returns false</returns>
/// Added fix to ensure the comparison is used - see http://issues.umbraco.org/issue/U4-11313
public static bool ContainsAny(this string haystack, IEnumerable<string> needles, StringComparison comparison = StringComparison.CurrentCulture)
{
if (haystack == null)
{
throw new ArgumentNullException("haystack");
}
if (string.IsNullOrEmpty(haystack) || needles == null || !needles.Any())
{
return false;
}
return needles.Any(value => haystack.IndexOf(value, comparison) >= 0);
}
public static bool CsvContains(this string csv, string value)
{
if (string.IsNullOrEmpty(csv))
{
return false;
}
var idCheckList = csv.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries);
return idCheckList.Contains(value);
}
/// <summary>
/// Converts a file name to a friendly name for a content item
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
public static string ToFriendlyName(this string fileName)
{
// strip the file extension
fileName = fileName.StripFileExtension();
// underscores and dashes to spaces
fileName = fileName.ReplaceMany(Constants.CharArrays.UnderscoreDash, ' ');
// any other conversions ?
// Pascalcase (to be done last)
fileName = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(fileName);
// Replace multiple consecutive spaces with a single space
fileName = string.Join(" ", fileName.Split(Constants.CharArrays.Space, StringSplitOptions.RemoveEmptyEntries));
return fileName;
}
/// <summary>
/// An extension method that returns a new string in which all occurrences of an
/// unicode characters that are invalid in XML files are replaced with an empty string.
/// </summary>
/// <param name="text">Current instance of the string</param>
/// <returns>Updated string</returns>
/// <summary>
/// removes any unusual unicode characters that can't be encoded into XML
/// </summary>
public static string ToValidXmlString(this string text) =>
string.IsNullOrEmpty(text) ? text : InvalidXmlChars.Value.Replace(text, string.Empty);
/// <summary>
/// Converts a string to a Guid - WARNING, depending on the string, this may not be unique
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static Guid ToGuid(this string text) =>
CreateGuidFromHash(
UrlNamespace,
text,
CryptoConfig.AllowOnlyFipsAlgorithms ? 5 // SHA1
: 3); // MD5
/// <summary>
/// Turns an null-or-whitespace string into a null string.
/// </summary>
public static string? NullOrWhiteSpaceAsNull(this string text)
=> string.IsNullOrWhiteSpace(text) ? null : text;
/// <summary>
/// Creates a name-based UUID using the algorithm from RFC 4122 §4.3.
/// See
/// <a href="https://github.com/LogosBible/Logos.Utility/blob/master/src/Logos.Utility/GuidUtility.cs#L34">GuidUtility.cs</a>
/// for original implementation.
/// </summary>
/// <param name="namespaceId">The ID of the namespace.</param>
/// <param name="name">The name (within that namespace).</param>
/// <param name="version">
/// The version number of the UUID to create; this value must be either
/// 3 (for MD5 hashing) or 5 (for SHA-1 hashing).
/// </param>
/// <returns>A UUID derived from the namespace and name.</returns>
/// <remarks>
/// See
/// <a href="http://code.logos.com/blog/2011/04/generating_a_deterministic_guid.html">Generating a deterministic GUID</a>
/// .
/// </remarks>
internal static Guid CreateGuidFromHash(Guid namespaceId, string name, int version)
{
if (name == null)
{
throw new ArgumentNullException("name");
}
if (version != 3 && version != 5)
{
throw new ArgumentOutOfRangeException("version", "version must be either 3 or 5.");
}
// convert the name to a sequence of octets (as defined by the standard or conventions of its namespace) (step 3)
// ASSUME: UTF-8 encoding is always appropriate
var nameBytes = Encoding.UTF8.GetBytes(name);
// convert the namespace UUID to network order (step 3)
var namespaceBytes = namespaceId.ToByteArray();
SwapByteOrder(namespaceBytes);
// comput the hash of the name space ID concatenated with the name (step 4)
byte[] hash;
using (HashAlgorithm algorithm = version == 3 ? MD5.Create() : SHA1.Create())
{
algorithm.TransformBlock(namespaceBytes, 0, namespaceBytes.Length, null, 0);
algorithm.TransformFinalBlock(nameBytes, 0, nameBytes.Length);
hash = algorithm.Hash!;
}
// most bytes from the hash are copied straight to the bytes of the new GUID (steps 5-7, 9, 11-12)
var newGuid = new byte[16];
Array.Copy(hash, 0, newGuid, 0, 16);
// set the four most significant bits (bits 12 through 15) of the time_hi_and_version field to the appropriate 4-bit version number from Section 4.1.3 (step 8)
newGuid[6] = (byte)((newGuid[6] & 0x0F) | (version << 4));
// set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively (step 10)
newGuid[8] = (byte)((newGuid[8] & 0x3F) | 0x80);
// convert the resulting UUID to local byte order (step 13)
SwapByteOrder(newGuid);
return new Guid(newGuid);
}
// Converts a GUID (expressed as a byte array) to/from network order (MSB-first).
internal static void SwapByteOrder(byte[] guid)
{
SwapBytes(guid, 0, 3);
SwapBytes(guid, 1, 2);
SwapBytes(guid, 4, 5);
SwapBytes(guid, 6, 7);
}
private static void SwapBytes(byte[] guid, int left, int right)
{
var temp = guid[left];
guid[left] = guid[right];
guid[right] = temp;
}
/// <summary>
/// Checks if a given path is a full path including drive letter
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static bool IsFullPath(this string path) => Path.IsPathFullyQualified(path);
// FORMAT STRINGS
/// <summary>
/// Cleans a string to produce a string that can safely be used in an alias.
/// </summary>
/// <param name="alias">The text to filter.</param>
/// <param name="shortStringHelper">The short string helper.</param>
/// <returns>The safe alias.</returns>
public static string ToSafeAlias(this string alias, IShortStringHelper? shortStringHelper) =>
shortStringHelper?.CleanStringForSafeAlias(alias) ?? string.Empty;
/// <summary>
/// Cleans a string to produce a string that can safely be used in an alias.
/// </summary>
/// <param name="alias">The text to filter.</param>
/// <param name="camel">A value indicating that we want to camel-case the alias.</param>
/// <param name="shortStringHelper">The short string helper.</param>
/// <returns>The safe alias.</returns>
public static string ToSafeAlias(this string alias, IShortStringHelper shortStringHelper, bool camel)
{
var a = shortStringHelper.CleanStringForSafeAlias(alias);
if (string.IsNullOrWhiteSpace(a) || camel == false)
{
return a;
}
return char.ToLowerInvariant(a[0]) + a.Substring(1);
}
/// <summary>
/// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an alias.
/// </summary>
/// <param name="alias">The text to filter.</param>
/// <param name="culture">The culture.</param>
/// <param name="shortStringHelper">The short string helper.</param>
/// <returns>The safe alias.</returns>
public static string ToSafeAlias(this string alias, IShortStringHelper shortStringHelper, string culture) =>
shortStringHelper.CleanStringForSafeAlias(alias, culture);
// the new methods to get a url segment
/// <summary>
/// Cleans a string to produce a string that can safely be used in an url segment.
/// </summary>
/// <param name="text">The text to filter.</param>
/// <param name="shortStringHelper">The short string helper.</param>
/// <returns>The safe url segment.</returns>
public static string ToUrlSegment(this string text, IShortStringHelper shortStringHelper)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
if (string.IsNullOrWhiteSpace(text))
{
throw new ArgumentException(
"Value can't be empty or consist only of white-space characters.",
nameof(text));
}
return shortStringHelper.CleanStringForUrlSegment(text);
}
/// <summary>
/// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an url
/// segment.
/// </summary>
/// <param name="text">The text to filter.</param>
/// <param name="shortStringHelper">The short string helper.</param>
/// <param name="culture">The culture.</param>
/// <returns>The safe url segment.</returns>
public static string ToUrlSegment(this string text, IShortStringHelper shortStringHelper, string? culture)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
if (string.IsNullOrWhiteSpace(text))
{
throw new ArgumentException(
"Value can't be empty or consist only of white-space characters.",
nameof(text));
}
return shortStringHelper.CleanStringForUrlSegment(text, culture);
}
/// <summary>
/// Cleans a string.
/// </summary>
/// <param name="text">The text to clean.</param>
/// <param name="shortStringHelper">The short string helper.</param>
/// <param name="stringType">
/// A flag indicating the target casing and encoding of the string. By default,
/// strings are cleaned up to camelCase and Ascii.
/// </param>
/// <returns>The clean string.</returns>
/// <remarks>The string is cleaned in the context of the ICurrent.ShortStringHelper default culture.</remarks>
public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType) => shortStringHelper.CleanString(text, stringType);
/// <summary>
/// Cleans a string, using a specified separator.
/// </summary>
/// <param name="text">The text to clean.</param>
/// <param name="shortStringHelper">The short string helper.</param>
/// <param name="stringType">
/// A flag indicating the target casing and encoding of the string. By default,
/// strings are cleaned up to camelCase and Ascii.
/// </param>
/// <param name="separator">The separator.</param>
/// <returns>The clean string.</returns>
/// <remarks>The string is cleaned in the context of the ICurrent.ShortStringHelper default culture.</remarks>
public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType, char separator) => shortStringHelper.CleanString(text, stringType, separator);
/// <summary>
/// Cleans a string in the context of a specified culture.
/// </summary>
/// <param name="text">The text to clean.</param>
/// <param name="shortStringHelper">The short string helper.</param>
/// <param name="stringType">
/// A flag indicating the target casing and encoding of the string. By default,
/// strings are cleaned up to camelCase and Ascii.
/// </param>
/// <param name="culture">The culture.</param>
/// <returns>The clean string.</returns>
public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType, string culture) => shortStringHelper.CleanString(text, stringType, culture);
/// <summary>
/// Cleans a string in the context of a specified culture, using a specified separator.
/// </summary>
/// <param name="text">The text to clean.</param>
/// <param name="shortStringHelper">The short string helper.</param>
/// <param name="stringType">
/// A flag indicating the target casing and encoding of the string. By default,
/// strings are cleaned up to camelCase and Ascii.
/// </param>
/// <param name="separator">The separator.</param>
/// <param name="culture">The culture.</param>
/// <returns>The clean string.</returns>
public static string ToCleanString(this string text, IShortStringHelper shortStringHelper, CleanStringType stringType, char separator, string culture) =>
shortStringHelper.CleanString(text, stringType, separator, culture);
// note: LegacyCurrent.ShortStringHelper will produce 100% backward-compatible output for SplitPascalCasing.
// other helpers may not. DefaultCurrent.ShortStringHelper produces better, but non-compatible, results.
/// <summary>
/// Splits a Pascal cased string into a phrase separated by spaces.
/// </summary>
/// <param name="phrase">The text to split.</param>
/// <param name="shortStringHelper"></param>
/// <returns>The split text.</returns>
public static string SplitPascalCasing(this string phrase, IShortStringHelper shortStringHelper) =>
shortStringHelper.SplitPascalCasing(phrase, ' ');
/// <summary>
/// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a
/// filename,
/// both internally (on disk) and externally (as a url).
/// </summary>
/// <param name="text">The text to filter.</param>
/// <param name="shortStringHelper"></param>
/// <returns>The safe filename.</returns>
public static string ToSafeFileName(this string text, IShortStringHelper shortStringHelper) =>
shortStringHelper.CleanStringForSafeFileName(text);
// NOTE: Not sure what this actually does but is used a few places, need to figure it out and then move to StringExtensions and obsolete.
// it basically is yet another version of SplitPascalCasing
// plugging string extensions here to be 99% compatible
// the only diff. is with numbers, Number6Is was "Number6 Is", and the new string helper does it too,
// but the legacy one does "Number6Is"... assuming it is not a big deal.
internal static string SpaceCamelCasing(this string phrase, IShortStringHelper shortStringHelper) =>
phrase.Length < 2 ? phrase : phrase.SplitPascalCasing(shortStringHelper).ToFirstUpperInvariant();
/// <summary>
/// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a
/// filename,
/// both internally (on disk) and externally (as a url).
/// </summary>
/// <param name="text">The text to filter.</param>
/// <param name="shortStringHelper"></param>
/// <param name="culture">The culture.</param>
/// <returns>The safe filename.</returns>
public static string ToSafeFileName(this string text, IShortStringHelper shortStringHelper, string culture) =>
shortStringHelper.CleanStringForSafeFileName(text, culture);
/// <summary>
/// Splits a string with an escape character that allows for the split character to exist in a string
/// </summary>
/// <param name="value">The string to split</param>
/// <param name="splitChar">The character to split on</param>
/// <param name="escapeChar">The character which can be used to escape the character to split on</param>
/// <returns>The string split into substrings delimited by the split character</returns>
public static IEnumerable<string> EscapedSplit(this string value, char splitChar, char escapeChar = DefaultEscapedStringEscapeChar)
{
if (value == null)
{
yield break;
}
var sb = new StringBuilder(value.Length);
var escaped = false;
foreach (var chr in value.ToCharArray())
{
if (escaped)
{
escaped = false;
sb.Append(chr);
}
else if (chr == splitChar)
{
yield return sb.ToString();
sb.Clear();
}
else if (chr == escapeChar)
{
escaped = true;
}
else
{
sb.Append(chr);
}
}
yield return sb.ToString();
}
}