The URL class, upgraded.
Provides a collection of classes to extend or complete the built-in URL class.
- 🛠️ Extended
URLclasses with enhanced setters and representation helpers :- 🔒
XUrl: to create secure restrictedURLinstances with custom whitelisted hosts and protocols - 👉
RelativeUrl: to create relative http / httpsURLinstances, without any url base
- 🔒
- 📷
UrlRepr: helper class providing representation methods for aURLinstance - 🧮 Types
Both classes extend the native URL class with :
➤ An as attribute :
Returns a UrlRepr instance linked to the current instance, providing multiple representation helpers for the url (see Representation helper class : UrlRepr)
import { XUrl, RelativeUrl } from 'url-toolbox'
const url = new XUrl('http://my-domain.com/path?query2=value2&query1=value1')
const relativeUrl = new RelativeUrl('/somewhere?query2=value2&query1=value1')
url.as.normalised() // -> http://my-domain.com/path/?query1=value1&query2=value2
relativeUrl.as.normalised() // -> /somewhere/?query1=value1&query2=value2
url.as.filtered({
search: false
}) // -> http://my-domain.com/path
relativeUrl.as.filtered({
search: false
}) // -> /somewhere➤ A Type-Restricted Constructor :
Constructor arguments are type-checked at execution time and will throw a TypeError if invalid, preventing common silent failures.
| Class | Constructor Args & Allowed Types |
|---|---|
XUrl |
url : string or URL base : string, URL or undefined restrictions : key-value object or undefined |
RelativeUrl |
url : string, URL or undefined |
import { XUrl, RelativeUrl } from 'url-toolbox'
const url = new XUrl(
undefined, // invalid type
'http://test.com'
) // -> throws TypeError
XUrl.canParse(
10, // invalid type
'http://test.com'
) // -> false
XUrl.canParse(
'http://test.com/test/',
undefined,
[1,2,3] // invalid restrictions
) // -> false
const relativeUrl = new RelativeUrl(
0 // invalid url
) // -> throws TypeError
RelativeUrl.canParse(
[1, 2, 3] // invalid url
) // -> false
RelativeUrl.canParse(
undefined // valid for relative url (will default to '/')
) // -> true ➤ Extended Attribute Setters :
Attribute setters perform type-checks at execution time, and can throw a TypeError for invalid argument types, preventing common silent failures.
Additionally, some attributes are extended with alternative setting options (port, pathname and search).
| Attribute | Allowed Setter Values and their Behaviour |
|---|---|
protocol |
✱ This restriction is only applied for XUrl instances as this attribute is ignored by RelativeUrl instances➤ string : calls default URL setter |
username |
✱ This restriction is only applied for XUrl instances as this attribute is ignored by RelativeUrl instances➤ string : calls default URL setter |
password |
✱ This restriction is only applied for XUrl instances as this attribute is ignored by RelativeUrl instances➤ string : calls default URL setter |
host |
✱ This restriction is only applied for XUrl instances as this attribute is ignored by RelativeUrl instances➤ string : calls default URL setter |
hostname |
✱ This restriction is only applied for XUrl instances as this attribute is ignored by RelativeUrl instances➤ string : calls default URL setter |
port |
✱ This restriction is only applied for XUrl instances as this attribute is ignored by RelativeUrl instances➤ number or string (representing a positive number) : checks that the given value can be converted to a valid integer before calling the default URL setter |
pathname |
➤ string : calls default URL pathname setter➤ string array : assembles a path from the given segments (useful if building paths from dynamic values) |
search |
➤ string : calls default URL setter➤ URLSearchParams or key-value object with string values : converted to a search string before calling the default URL search setter |
hash |
➤ string : calls default URL setter |
href |
➤ string : calls default URL setter for XUrl instances, or manually sets attribute values for RelativeUrl instances |
Here are some examples to illustrate the additional type restrictions on attribute setters :
import { XUrl, RelativeUrl } from 'url-toolbox'
const url = new XUrl('http://my-domain.com/path?query2=value2&query1=value1')
const relativeUrl = new RelativeUrl('/somewhere?query2=value2&query1=value1')
// Attempting to set a non-expected value to an attribute setter
// will throw a TypeError
url.host = undefined // (expected string)
url.port = '-10' // (expected positive number)
url.pathname = [1,2,3] // (expected string or string array)
relativeUrl.pathname = [1,2,3] // (expected string or string array)
// Exception :
// url base attributes are ignored by RelativeUrl instances,
// so even if the value has an invalid type,
// no error is thrown, and the instance is not modified :
relativeUrl.host = undefined // (ignored)
relativeUrl.protocol = undefined // (ignored)Here are some examples on how to use the alternative setter options
import { XUrl, RelativeUrl } from 'url-toolbox'
const url = new XUrl('http://my-domain.com/path?query2=value2&query1=value1')
const relativeUrl = new RelativeUrl('/somewhere?query2=value2&query1=value1')
// port can be directly set with a positive number
// (only available in XUrl instances)
url.port = 8000
url.port // -> '8000'
// pathname can be set from a string array
// each path segment is encoded before being assembled
url.pathname = ['single/path/segment', 'other']
relativeUrl.pathname = ['this', 'is', 'my path']
url.pathname // /single%2Fpath%2Fsegment/other/
relativeUrl.pathname // /this/is/my%20path/
// search can be set from a URLSearchParams instance
// or from a key-value pair object containing only string values
url.search = new URLSearchParams({
newQuery: 'rabbits',
filter: 'white'
})
relativeUrl.search = {
newQuery: 'rabbits',
filter: 'white'
}
url.search // ?newQuery=rabbits&filter=white
relativeUrl.search // ?newQuery=rabbits&filter=whiteThis class allows to define restrictions on an instance :
-
To define the whitelisted protocols (protocols are always restricted, even if no allowed protocols are defined)
-
To define the whitelisted hosts
-
To ignore credentials (username & password) for the instance
Type of error thrown if a protocol or host restriction is about to be broken in a XUrl instance.
import {BrokenUrlRestrictionError} from 'url-toolbox'Restrictions are declared as a key-value object.
They are defined at instance creation, if no value is given for a key, the default value is used.
| Key | Description | Default |
|---|---|---|
allowedProtocols |
String array (optional) Defines whitelisted protocols. (it's not possible to allow all protocols, if no allowed protocols are listed the default list is used) Any attempt to use a non-allowed protocol will throw a BrokenUrlRestrictionError |
http:, https:, ftp:, sftp:, ws:, wss:, blob:, about:, mailto:, tel:, sms: |
allowedHosts |
String array (optional) Defines whitelisted hosts. (should include eventual subdomain and eventual port number, if applicable) If defined and non empty, any attempt to use a non-allowed host will throw a BrokenUrlRestrictionError |
Not restricted |
ignoreCredentials |
Boolean (optional) If true, username and password values are erased and cannot be modified. Any attempt to set credentials anyway will be ignored but will not throw any error. |
Allow credentials |
Restrictions are set at instance creation (available both with new or XUrl.canParse)
import { XUrl } from 'url-toolbox'
const restrictions = {
allowedProtocols: [ 'http:', 'https:' ],
allowedHosts: [ 'domain.com' ]
}
// Creating XUrl instances
const urlA = new XUrl(
'/test/path/',
'https://domain.com',
restrictions
)
const urlB = XUrl.parse(
'/test/path/',
'https://domain.com',
restrictions
)
// These operations will throw an error
urlA.href = 'http://wrong-domain.com/path' // (wrong host)
urlA.protocol = 'ftp:' // (wrong protocol)If the given url doesn't respect protocol or host restrictions at instance creation, operation will also fail with BrokenUrlRestrictionError
import { XUrl } from 'url-toolbox'
const restrictions = {
allowedProtocols: [ 'http:', 'https:' ],
allowedHosts: [ 'domain.com' ]
}
// Those attempts will fail because the initial value
// breaks host or protocol restrictions
const urlA = new XUrl(
'ftp://wrong-domain.com/test/path/',
undefined,
restrictions
) // (wrong protocol & host)
const urlB = new XUrl('javascript:alert("XSS")')
// -> (javascript: protocol is not in the default list of allowed protocols and '' is not an allowed host)If credentials are ignored, any attempt to set username or password will be ignored, but no error will be thrown.
import { XUrl } from 'url-toolbox'
// None of these methods will succeed at setting the credentials
const url = XUrl.parse(
'https://user:password123@domain.com//test/path/',
undefined,
{ ignoreCredentials: true }
)
url.username = 'user'
url.password = 'password123'
url.href = 'https://user:password123@domain.com//test/path/'
url.toString() // -> 'https://domain.com//test/path/'XUrl.canParse will also check the url string against provided (or default) restrictions
import { XUrl } from 'url-toolbox'
const restrictions = {
allowedProtocols: [ 'http', 'https' ],
allowedHosts: [ 'domain.com', ]
}
XUrl.canParse('https://domain.com/test/path/', undefined, restrictions) // -> true
XUrl.canParse('/test/path/', 'https://wrong-domain.com', restrictions) // -> false (wrong host)
XUrl.canParse('javascript:alert("XSS")') // -> false (javascript: is not in the default list of allowed protocols and '' is not an allowed host) This class is built around the idea of ignoring the url base (protocol, username, password, host, hostname, or port).
To maintain coherence with the built-in URL API, the base url attribute setters and getters are still accessible but will be ignored.
Instances can be created with new or RelativeUrl.parse, with a relative or an absolute url string.
If using an absolute url string, the protocol should be http:, or https:, the use of any other protocol will fail with a TypeError.
import { RelativeUrl } from 'url-toolbox'
const urlA = new RelativeUrl('/my/path/?query=banana')
const urlB = RelativeUrl.parse('/my/path/?query=banana')
const urlC = RelativeUrl.parse('https://domain.com/my/path/?query=banana')
urlA.toString() // -> /my/path/?query=banana
urlB.toString() // -> /my/path/?query=banana
urlC.toString() // -> /my/path/?query=banana
const urlD = new RelativeUrl('mailto:me@box.house') // -> throws error (only supports http: or https: based urls)In a RelativeUrl instance, attributes describing the url base are deactivated (protocol, username, password, host, hostname, or port).
They will always return an empty string, and any attempt to modify their value will be ignored.
// all of those attempts at setting url base related attributes are ignored
const url = RelativeUrl.parse('https://user:password@domain.com:8080/my/path/?query=banana')
url.protocol = 'https:'
url.username = 'user'
url.password = 'password'
url.host = 'domain.com:8080'
url.hostname = 'domain.com'
url.port = '8080'
url.protocol // -> ""
url.username // -> ""
url.password // -> ""
url.host // -> ""
url.hostname // -> ""
url.port // -> ""The href attribute accepts relative or absolute url strings, but will throw a TypeError if given an invalid url or an absolute url with a non-http protocol.
import { RelativeUrl } from 'url-toolbox'
const url = new RelativeUrl('/my/path/?query=banana')
url.href = 'https://domain.com/new-page/'
url.href // -> /new-page/
url.href = '/other/page/?query=fish'
url.href // -> /other/page/?query=fish
url.href = 'ftp://domain.com/path/' // -> throws error (only supports http: or https: based urls)canParse static method is modified to allow relative url strings
import { RelativeUrl } from 'url-toolbox'
RelativeUrl.canParse('/my/path/?query=banana') // -> true
RelativeUrl.canParse('https://domain.com/my/path/?query=banana') // -> true
RelativeUrl.canParse('ftp://domain.com/my/path/?query=banana') // -> false (only supports http: or https: based urls)Class defining representation utils for a URL instance
ℹ️ For XUrl and RelativeUrl instances, an instance of UrlRepr is accessible under the as attribute.
A UrlRepr instance needs to be linked with a URL instance
import { UrlRepr } from 'url-toolbox'
const url = new URL('http://my-domain.com/path?query1=value1&query2=value2')
const urlRepr = new UrlRepr(url)The normalised method allows to get a url representation that is easy to compare with another url.
The path and search params order is normalised to ensure that most equivalent urls can be efficiently compared.
import { UrlRepr } from 'url-toolbox'
// defining equivalent urls having non matching hrefs
const urlA = new URL('http://my-domain.com/path?query1=value1&query2=value2')
// -> the second URL, has search params in a different order
// and a slash at the end of the path
const urlB = new URL('http://my-domain.com/path/?query2=value2&query1=value1')
const reprA = new UrlRepr(urlA)
const reprB = new UrlRepr(urlB)
urlA.href === urlB.href // false
reprA.normalised() === reprB.normalised() // trueThere are also shortcut attributes allowing to get only the normalised path or normalised search :
normalisedPathname and normalisedSearch
import { UrlRepr } from 'url-toolbox'
const url = new URL('http://my-domain.com/path?query2=value2&query1=value1')
const repr = new UrlRepr(url)
repr.normalisedPathname // -> /path/ (added slash)
repr.normalisedSearch // -> ?query1=value1&query2=value2 (ordered keys)It's possible to filter the url parts in the representation by using the filtered method, or by providing filtering options to the normalised method.
The filtering options argument is a key-value object allowing to filter the url parts included in a url representation (all keys are optional with default values).
| Key | Description | Default |
|---|---|---|
baseMode |
Literal string (optional) Modifies what's included in the url base :
|
"NO_CREDENTIALS" |
pathname |
Boolean (optional) Indicates whether or not we should include the pathname in the result |
included |
search |
Boolean (optional) Indicates whether or not we should include the search in the result |
included |
hash |
Boolean (optional) Indicates whether or not we should include the hash in the result |
included |
import { UrlRepr } from 'url-toolbox'
const url = new URL('http://my-domain.com/path?query2=value2&query1=value1#somewhere')
const repr = new UrlRepr(url)
repr.filtered({
baseMode: 'HOST_ONLY',
search: false
}) // -> my-domain.com/path#somewhere
repr.normalised({
baseMode: 'NO_BASE',
hash: false
}) // -> /path/?query1=value1&query2=value2ℹ️ Remark : for "host-less" protocols like mailto:, or tel:, the only available part of the url base is the protocol.
import { UrlRepr } from 'url-toolbox'
const url = new URL('mailto:me@you.us')
const repr = new UrlRepr(url)
repr.normalised({
baseMode: 'NO_CREDENTIALS'
}) // -> mailto:me@you.us
repr.normalised({
baseMode: 'NO_PROTOCOL'
}) // -> me@you.us
repr.filtered({
baseMode: 'HOST_ONLY',
}) // -> me@you.us
repr.filtered({
baseMode: 'HOST_ONLY',
pathname: false
}) // -> '' (because "me@you.us" is the pathname)ExtendedUrl_T : instance of XUrl or RelativeUrl