Scripting (OrchardCore.Scripting)¶
Purpose¶
The scripting module provides an API allowing you to evaluate custom scripts in different languages.
Usage¶
Executing some script¶
The main interface is IScriptingManager
To evaluate an expression using a scripting engine, you must know which ones are available in the system.
For instance, a JavaScript one is available by default and its prefix is js.
To return the current date and time as an object we could do something like this:
var scriptingManager = serviceProvider.GetService<IScriptingManager>();
// Find the javascript engine by its prefix
var engine = scriptingManager.GetScriptingEngine("js");
// Find all global methods in the system. Here you could add more methods to the scope as needed
var globalMethods = _scriptingManager.GlobalMethodProviders.SelectMany(x => x.GetMethods());
// Create scope for the engine
var scope = engine.CreateScope(globalMethods, serviceProvider, null, null);
// Evaluate the given script. The engine is already the JavaScript one, so the
// script is passed without the `js:` prefix, which only a directive carries.
var date = engine.Evaluate(scope, "new Date().toISOString()");
The js: prefix is used to describe in which language the code is written. Any module can provide
a new scripting engine by implementing the IScriptingEngine interface.
Customizing the scripting environment¶
Any module can provide custom methods for scripts independently of the chosen language.
For instance the Contents module provides a uuid() helper method that computes a unique content item identifier.
To create a global method, implement the IGlobalMethodProvider. Then, add it to the current IScriptingManager instance by registering it as a singleton in your Module's Startup
services.AddSingleton<IGlobalMethodProvider, MyGlobalMethodProvider>();
File¶
The File scripting engine provides methods to read file contents.
| Name | Example | Description |
|---|---|---|
text |
file:text('../wwwroot/template.html') |
Returns the content of a text file. |
base64 |
file:base64('../wwwroot/image.jpg') |
Returns the base64 encoded content of a file. |
JavaScript OrchardCore.Scripting.JavaScript¶
The JavaScript scripting module implements an IScriptingEngine that uses Jint to evaluate scripts.
Configuring the JavaScript engine¶
The engine is configured through Jint's own options type, which is registered as IOptions<Jint.Options>:
services.Configure<Jint.Options>(options =>
{
options.MaxStatements(10_000);
options.TimeoutInterval(TimeSpan.FromSeconds(5));
});
Three things are worth knowing about how that instance is used:
- One
Jint.Optionsinstance is shared by every engine of the tenant. Configure only settings that make sense for the whole tenant, since the same options serve recipe execution, workflow scripts and layer rules alike. Use the constraint helpers shown above (MaxStatements,TimeoutInterval,LimitMemory,CancellationToken): each of them registers a factory, so every engine gets its own counter and its own deadline and the shared instance stays safe for concurrent requests. Registering a constraint instance —options.Constraint(new MyConstraint())— shares that instance, and with it its per-execution state, across every concurrently running engine of the tenant. Derive fromJint.Constraintand register it with the factory overload,options.Constraint(() => new MyConstraint()), instead. - A limit spelled as a saturated or absent value registers nothing.
MaxStatements(int.MaxValue),LimitMemory(long.MaxValue)andTimeoutInterval(TimeSpan.MaxValue)produce exactly the same engine as never calling the method, and additionally remove any limit of that kind set earlier. The same is true ofMaxStatements()with no argument: its parameter defaults to0, and only a positive budget registers a constraint, so that call reads like it turns a statement limit on while leaving the statement count unlimited. Always pass the budget you mean, and omit the call rather than passing a maximum value. - Do not register an
IObjectConverterthat handlesDelegate. Global methods areDelegatevalues, and converters are consulted before Jint's own delegate wrapping, so such a converter would change the shape of the globals that are created on demand while leaving the eagerly created ones alone. - Do not use
Engine.Advanced.HostDefinedon an engine the scripting engine created. A global that is created on demand is built long after its engine was, so the services it belongs to have to travel with the engine, and that slot is where they travel. It is reachable, becauseJavaScriptScope.Engineis public. Writing to it replaces the services and the next global to be created is built from whatever was put there instead. A scope built around an engine of your own is the exception: it claims the slot only if nothing else has, so an engine you are already using it on keeps what you put there, and the registered globals on that engine fail with a message saying so rather than silently taking your value.
No execution budget is configured by default, so a script such as while (true) {} runs until the process is recycled. Sites that let non-administrators author scripts should set at least one of the limits above.
One protection is on by default, because no budget substitutes for it: Constraints.StackOverflowGuard. An unbounded recursion — function f() { return 1 + f(); }, but equally a getter, a constructor or a valueOf that re-enters itself — overflows the native stack, and a native stack overflow is not an exception. It cannot be caught, it is not logged, and the whole process is killed rather than the one request. A budget does not help, because a budget is only reached by a script that survives long enough to spend it, and the overflow arrives in milliseconds: with MaxStatements(10_000) and TimeoutInterval(TimeSpan.FromSeconds(5)) both set, that script still ends the process. With the guard on, it raises an ordinary RangeError that the script itself can catch, only the request that ran it fails, and the engine is still usable.
The guard measures the remaining stack at every entry into a script function, which Jint's own benchmarks put at 1.7–2.3% on deeply recursive code and unmeasurable on ordinary calls. An application that has weighed that against its own scripts can turn it off:
services.Configure<Jint.Options>(options =>
{
options.Constraints.StackOverflowGuard = false;
});
One constraint is registered, and it bounds nothing until asked to. IScriptingManager.EvaluateAsync() and IScriptingEngine.EvaluateAsync() take a CancellationToken, and Jint's own EvaluateAsync observes a token only while awaiting promise settlement — the script is interpreted to completion first. Forwarding the token there therefore bounded an evaluation that awaits something and nothing at all for one that does not, which is the case that matters. AddJavaScriptEngine() registers a Jint.Constraints.OperationDeadlineConstraint factory, and JavaScriptEngine.EvaluateAsync() arms that engine's instance with the token for the duration of the call. Three things follow:
- Cancelling the token stops the script, not only the promise settlement. It surfaces as
OperationCanceledExceptioncarrying the token — the type the rest of a .NET call stack already filters on — rather than as a scripting error. - A token cancelled before the call is observed before the script starts. One cancelled while the script is running is observed on the engine's amortized constraint cadence, so a runaway script stops within a bounded number of statements rather than at the exact statement cancellation arrived. That cadence is why the interpreter keeps its tight-loop fast path, and it is the same cadence the built-in
CancellationToken()andTimeoutInterval()constraints are checked on. - No time budget is introduced. The constraint is armed with
Timeout.InfiniteTimeSpan, its cancellation-only shape. How long a script may run stays a policy for the site to set with the helpers above. A disarmedOperationDeadlineConstraint— every synchronous evaluation, and every asynchronous one whose token cannot be cancelled — reads two fields and takes no timestamp.
The built-in CancellationToken() helper cannot serve this: it takes the token when the options are configured, and one Jint.Options instance serves every engine of the tenant for the tenant's lifetime, while the token belongs to a single evaluation.
Global methods are created on demand¶
The globals contributed by IGlobalMethodProvider implementations are declared on every engine but are not built until a script reads the name. A recipe expression such as [js:uuid()] therefore only pays for uuid, not for every registered global. This is not observable from script — the properties exist, they are non-enumerable, and the value a name resolves to is stable for the whole evaluation — but the Func<IServiceProvider, Delegate> you supply is invoked lazily, and not at all when the script does not use the method. Keep it free of side effects that the surrounding code depends on.
Three kinds of method are created eagerly instead, so a factory backing one of them runs once per engine whether or not the script uses it:
- Methods passed directly to
IScriptingEngine.CreateScope()rather than registered through DI, such as a recipe'svariables()or a workflow'sworkflow(). These also take precedence over a registered global of the same name. - A name contributed by more than one registered provider, because which provider wins depends on the order the methods are set in.
- A method carrying an asynchronous variant whose
<name>Asyncglobal is also claimed by a method literally named<name>Async.
Methods¶
Here is a list of javascript methods provided by Orchard Modules.
Generic functions¶
| Function | Description |
|---|---|
log(level: String, text: String, param: Object): void |
Formats and writes a log message at the specified log level. |
uuid(): String |
Generates a unique identifier for a content item. |
base64(String): String |
Decodes the specified string from Base64 encoding. Use https://www.base64-image.de/ to convert your files to base64. |
html(String): String |
Decodes the specified string from HTML encoding. |
gzip(String): String |
Decodes the specified string from gzip/base64 encoding. Use http://www.txtwizard.net/compression to gzip your strings. |
protect(purpose: String, value: String): String |
Protects the specified value using the ASP.NET Core Data Protection API with the given purpose string. |
encrypt(value: String): String |
Encrypts the specified value using the ASP.NET Core Data Protection API. Returns a Base64-encoded ciphertext. |
decrypt(value: String): String |
Decrypts a Base64-encoded string previously encrypted with the encrypt function. Returns an empty string if decryption fails. |
Warning
The protect function is intended for use during development and testing scenarios only. Storing secrets in recipe files for production environments is not recommended and should be avoided. Use a secure secret management solution (e.g., Azure Key Vault, environment variables) for production deployments.
Example (protect):
{
"steps": [
{
"name": "settings",
"Properties": {
"ApiKey": "[js: protect('MyModule.ApiKey', 'my-secret-value')]"
}
}
]
}
Example (encrypt / decrypt):
var encryptedValue = encrypt('my-secret-value');
To read the value back later using JavaScript:
var plainText = decrypt(encryptedValue);
Content (OrchardCore.Contents)¶
| Function | Description |
|---|---|
newContentItemAsync(contentTypeName: String): Promise<IContent> |
Creates a new instance of a ContentType (does not persist) |
createContentItemAsync(contentTypeName: String, publish: Boolean, properties: Object): Promise<IContent> |
Creates and persists a new ContentItem. Conditionally publishes it. |
updateContentItemAsync(contentItem: IContent, properties: Object): Promise |
Updates an existing content item with the properties |
deleteContentItemAsync(contentItem: IContent): Promise |
Deletes an existing content item |
getUrlPrefix(path: String): String |
Prefixes the path with the Tenant prefix (if specified) |
Layers (OrchardCore.Layers)¶
| Function | Description |
|---|---|
isHomepage(): Boolean |
Returns true if the current request Url is the current homepage |
isAnonymous(): Boolean |
Returns true if there is no authenticated user on the current request |
isAuthenticated(): Boolean |
Returns true if there is an authenticated user on the current request |
url(url: String): Boolean |
Returns true if the current URL matches the provided URL. Add a * to the end of the URL parameter to match any URL that starts with the provided value. |
culture(name: String): Boolean |
Returns true if the current culture name or the current culture's parent name matches the name argument |
Queries (OrchardCore.Queries)¶
| Function | Description |
|---|---|
executeQueryAsync(name: String, parameters: Dictionary<string,object>): Promise<IEnumerable<object>> |
Returns the result of the query. |
HTTP (OrchardCore.Workflows.Http)¶
| Function | Description |
|---|---|
httpContext(): HttpContext |
Returns the HttpContext which encapsulates all HTTP-specific information about an individual HTTP request. |
queryString(name: String): String |
Array| Returns the entire query string (including the leading?`) when invoked with no arguments, or the value(s) of the parameter name passed in as an argument. |
responseWriteAsync(text: String): Promise |
Writes the argument string directly to the HTTP response stream. |
absoluteUrl(relativePath: String): String |
Returns the absolute URL for the relative path argument. |
readBodyAsync(): Promise<String> |
Returns the raw HTTP request body. |
requestForm(name: String): String |
Array` |
deserializeRequestDataAsync(): Promise<Dictionary<string, object>> |
Deserializes the request data as a Dictionary |
Recipes (OrchardCore.Recipes)¶
| Function | Description |
|---|---|
variables(): string |
Declare variables at the root of a recipe. Ex: "variables": { "blogContentItemId": "[js:uuid()]" } Retrieve a variable value like this: "ContentItemId": "[js: variables('blogContentItemId')]" |
parameters(): string |
Retrieves the parameters specified during the setup. Ex: "Owner": "[js: parameters('AdminUserId')]" See the available Setup Recipe parameters |
configuration(key: String, defaultValue: String): string |
Retrieves the specified configuration setting by its key, optionally providing a default. Ex: [js: configuration('OrchardCore_Admin:AdminUrlPrefix', 'Admin')] See IShellConfiguration |
Workflows (OrchardCore.Workflows.Http)¶
The following JavaScript functions are available by default to any workflow activity that supports script expressions:
| Function | Description |
|---|---|
workflow(): WorkflowExecutionContext |
Returns the WorkflowExecutionContext which provides access to all information related to the current workflow execution context. |
workflowId(): String |
Returns the unique workflow ID. |
input(name: String): Any |
Returns the input parameter with the specified name. Input to the workflow is provided when the workflow is executed by the workflow manager. |
output(name: String, value: Any): void |
Sets an output parameter with the specified name. Workflow output can be collected by the invoker of the workflow. |
property(name: String): Any |
Returns the property value with the specified name. Properties are a dictionary that workflow activities can read and write information from and to. |
lastResult(): Any |
Returns the value that the previous activity provided, if any. |
correlationId(): String |
Returns the correlation value of the workflow instance. |
setCorrelationId(id:string): void |
Set the correlation value of the workflow instance. |
signalUrl(signal: String): String |
Returns workflow trigger URL with a protected SAS token into which the specified signal name is encoded. Use this to generate URLs that can be shared with trusted parties to trigger the current workflow if it is blocked on the Signal activity that is configured with the same signal name. |
setOutcome(outcome: String): void |
Adds the provided outcome to the list of outcomes of the current activity |
createWorkflowToken(workflowTypeId: String, activityId: String, expiresInDays: Integer): String |
Generates a workflow SAS token for the specified workflowTypeid, activityId. You can also set the expiration date in number of days. |