= Built-In Expansions

In addition to storing attribute references, the server has a number
of built-in expansions.  These expansions act largely as functions
which operate on inputs, and produce an output.

== Attribute Manipulation

=== %length( ... )

The `length` expansion returns the size of the input as an integer.
When the input is a string, then the output is identical to the
`strlen` expansion.

When the input is an attribute reference, the output is the size of
the attributes data as encoded "on the wire".

.Return: _size_

.Determining the length of fixed and variable length attributes
====
[source,unlang]
----
&Tmp-String-0 := "Caipirinha"
&Framed-IP-Address := 192.0.2.1

&reply += {
	&Reply-Message = "The length of %{control.Tmp-String-0} is %length(&control.Tmp-String-0)"
	&Reply-Message = "The length of %{control.Framed-IP-Address} is %length(&control.Framed-IP-Address)"
}
----

.Output
....
The length of Caipirinha is 10
The length of 192.168.0.2 is 4
....
====

=== %rand(<number>)

Generate random number from `0` to `<number>-1`.

.Return: _uint64_

.Generating a random number between 0 and 511
====
[source,unlang]
----
&reply.Reply-Message := "The random number is %rand(512}"
----

.Output

```
The random number is 347
```
====

== Server Configuration

=== %config(<key>)

Refers to a variable in the configuration file. See the documentation
on configuration file references.

.Return: _string_

.Example

[source,unlang]
----
"Server installed in %config('prefix')"
"Module rlm_exec.shell_escape = %config('modules.exec.shell_escape')"
----

.Output

```
Server installed in /opt/freeradius
Module rlm_exec.shell_escape = yes
```

== Miscellaneous Expansions

=== %{0}+..+%{32}

`%{0}` expands to the portion of the subject that matched the last regular
expression evaluated. `%{1}`..`%{32}` expand to the contents of any capture
groups in the pattern.

Every time a regular expression is evaluated, whether it matches or not,
the numbered capture group values will be cleared.

=== +%regex(<named capture group>}+

Return named subcapture value from the last regular expression evaluated.

Results of named capture groups are available using the `%regex(<named capture
group>}` expansion. They will also be accessible using the numbered expansions
described xref:xlat/builtin.adoc#_0_32[above].

Every time a regular expression is evaluated, whether it matches or not,
the named capture group values will be cleared.

[NOTE]
====
This expansion is only available if the server is built with libpcre or libpcre2.
Use the output of `radiusd -Xxv` to determine which regular expression library in use.

....
...
Debug :   regex-pcre               : no
Debug :   regex-pcre2              : yes
Debug :   regex-posix              : no
Debug :   regex-posix-extended     : no
Debug :   regex-binsafe            : yes
...
Debug :   pcre2                    : 10.33 (2019-04-16) - retrieved at build time
....
====

=== +%eval(<string>)+

Evaluates the string as an expansion, and returns the result.  The main difference between using this expansion and just using `%{...}` is that the string being evaluated can be dynamically changed.

.Return: _data_

.Example

[source,unlang]
----
if (&User-Name == "bob") {
    &request.Tmp-String-0 := "&User-Name"
} else {
    &request.Tmp-String-0 := "not bob!"
}

&reply.Reply-Message := "%eval(&request.Tmp-String-0}"
----

.Output when `&User-Name == bob`

```
bob
```

.Output when `&User-Name == not bob`

```
not bob!
```

=== %nexttime(<time>)

Calculate number of seconds until next n hour(`s`), day(`s`), week(`s`), year(`s`).

.Return: _string_

.Example

With the current time at 16:18, `%nexttime(1h)` will expand to `2520`.

[source,unlang]
----
&reply.Reply-Message := "You should wait for %nexttime(1h)s"
----

.Output

```
You should wait for 2520s
```

### %sub(<subject>, /<regex>/[flags], <replace>)

Substitute text just as easily as it can match it, even using regex patterns.

.Return: _string_.

.Example

[source,unlang]
----
&control.Tmp-String-0 := "Caipirinha is a light and refreshing drink!"
&reply.Reply-Message := "%sub(%{control.Tmp-String-0}, / /, ',')"
----

.Output

```
Caipirinha,is,a,light,and,refreshing,drink!
```

### %time()

Return the current time.

.Return: _date_.

If no argument is passed, it returns the current time.  Otherwise if the argument is:

* `dst` - returns a `bool` indicating whether or not the system is running in daylight savings time.
* `mday_offset` - returns the `time_delta` offset since the start of the month.  Use `%d` to get an integer representing the day of the month.
* `now` - returns the current time
* `offset` - returns a `time_delta` of the current time zone offset.  This value may be negative.
* `request` - returns the time at which the request packet was received (always less than `now`!)
* `wday_offset` - returns the `time_delta` offset since the start of the week.
* any other string is parsed as type `date`, using the normal date parsing routines.

.Example

[source,unlang]
----
&Acct-Start-Time := %time(now)
----

The current time can also be compared to a known date:

.Example
[source,unlang]
----
if (%time() < (date) 'Aug 1 2023 01:02:03 UTC') {
	...
}
----

The format of the date strings should be the same format as the server
prints out.  The parse will try to accept other date formats (raw
integer, etc.), but not all formats are guaranteed to work.  There are
hundreds of different date formats used across the world, and the
server cannot support them all.

[NOTE]
====
This expansion should be used in preference to the xref:xlat/character.adoc[single letter expansions] `%l`.  That expansion returns integer seconds, and is not suitable for millisecond or microsecond resolution.
====

Due to limitations in the underlying time functions (both system and
FreeRADIUS), previous versions of FreeRADIUS did not always handle
dates correctly.  When print dates, the time zone information would
sometimes not be printed, or the time zone would sometimes be ignored
when parsed a date from a string.

Even if the time zone was used, the nature of time zones means that
there may be duplicate time zone names!  For example, the time zone
`CST` has three separate (and different) definitions.

The server now tracks all times internally as UTC, and by default
prints times as UTC, or prints the time zone as a decimal offset from
UTC, instead of printing an ambiguous name.

This handling of time zones has some minor side effects.  When
calculating values like "tomorrow", the default is to return the UTC
version of "tomorrow".  This value may not be what you want.

In order to correctly calculate the local value of "tomorrow", it is
necessary to add the local time zone offset to the UTC time.

Note that the server will automatically determine (and use) any
daylight savings time differences.  So the value of `%time(offset)`
may change while the server is running!

The following example calculates the correct value of "tomorrow" in
UTC by using the following steps:

* taking the current time of the request
* calculating how long it has been since the start of the day as a `time_delta`
* subtracting that `time_delta` from the current time

.Example Calculating the UTC value of "tomorrow"
[source,unlang]
----
group {
    date now
    date tomorrow
    time_delta time_of_day

    &now := %time('request')

    #  We are this many seconds into one day
    &time_of_day := &now % (time_delta) 1d

    # calculate the start of today, and then add one day to that
    &tomorrow := &now - &time_of_day + (time_delta) 1d
}
----

The following example calculates the correct value of "tomorrow" in
local time by using the preceding example, but then adding the local
time zone offset to the final value.

.Example Calculating the local value of "tomorrow"
[source,unlang]
----
group {
    date now
    date tomorrow
    time_delta time_of_day

    &now := %time('request')

    #  We are this many seconds into one day
    &time_of_day := &now % (time_delta) 1d

    # calculate the start of today, and then add one day to that
    &tomorrow := &now - &time_of_day + (time_delta) 1d

    #  add in the time zone offset
    &tomorrow += %time('offset')
}
----

This kind of math works well for "tomorrow", but it is less useful for
"next week Monday", or "start of next month".  The `%nexttime(...)`
expansion above should be used for those time operations.

// Copyright (C) 2023 Network RADIUS SAS.  Licenced under CC-by-NC 4.0.
// This documentation was developed by Network RADIUS SAS.
