EmbeddedJSPlugin
EJS (Embedded JavaScript) plugin
See EmbeddedJSPluginAPI for the full list of available APIs.
Introduction
This plugin enables
EJS (Embedded JavaScript) template
to embed JavaScript code as part of TWiki topic contents.
JavaScript is executed on the server side to interact with various TWiki contents (Webs, Topics, Attachments, etc.) and generate TWiki markups, naturally using loops, if-statements, and function calls.
The example below finds and lists all the topics whose names start with "IssueItem".
<%
var issues = findTopics('IssueItem*');
for (var i = 0; i < issues.length; i++) {
println('---+++ ' + issues[i]);
});
%>
In this code,
findTopics() and
println() are
API functions that are made available by this plugin.
The same thing can be achieved with TWiki variables such as
%SEARCH{...}%, but JavaScript allows you to express the logic in a more flexible way.
While JavaScript is a full-fledged programming language, it is also safe because arbitrary file system access and command executions are not allowed, while only certain safe interfaces are provided (such as reading/writing TWiki topics with permission restrictions in place).
Once the
EJS preference variable is set to
on, the content of a TWiki topic will be processed by the
EJS::Template
Perl module.
* Set EJS = on
In other words, EJS is
not automatically turned on.
However, the administrator can choose to configure
$TWiki::cfg{Plugins}{EmbeddedJSPlugin}{DefaultExecute} to
on, if EJS should be executed by default in all the topics.
It is also possible to use
%EJS_INCLUDE{...}% to process EJS, where the code is executed regardless of the
EJS preference variable.
%EJS_INCLUDE{"ComponentTopicWithEJS"}%
It is useful when a library or application component is implemented in EJS, so that it can be used anywhere without having to set the
EJS preference variable.
Synopsis
Within the EJS tag
<% ... %>, the code is executed as JavaScript.
The
print() function appends the text to the output content.
The short-hand notation
<%= ... %> can also be used to print the value of the expression.
Any texts outside of these tags are simply appended to the output, just like the print function.
<%
function hello() {
return 'Hello, World!';
}
%>
<%= hello() %>
<% print(hello()) %>
The above two lines are equivalent.
Texts outside of the EJS tags are simply printed.
Execution Model
When a topic is viewed (via the TWiki
view script) where the
EJS preference variable is set to
on, EJS is executed as a
preprocessor, prior to expanding the TWiki variables (e.g.
%TOPIC%) and finally rendering HTML.
An exception is the
%EJS_INCLUDE{...}% tag, which is processed as part of all the TWiki variables.
Roughly speaking, the rendering phases will take place in the following order.
- Preference variables (
* Set NAME = VALUE) are extracted.
- EJS tags (
<% ... %>) are processed with a JavaScript engine.
- TWiki variables (
%VARIABLE%) are expanded, where %EJS_INCLUDE{...}% tags are also expanded.
- TWiki markups (
---++ Heading) are converted to HTML.
This means TWiki variables that appear within JavaScript code will look as they are, rather than the expanded texts.
var topic = '%TOPIC%';
// During JavaScript execution, the value is literally '%TOPIC%' rather than something like 'WebHome'.
On the other hand, some API functions
do expand TWiki variables that are used as arguments.
var topics = findTopics('%SYSTEMWEB%.Web*');
// The findTopics() will expand the argument, so it is treated as something like 'TWiki.Web*'.
If necessary, you can call
expandVariables() or
getVariable() to expand TWiki variables.
var topic = expandVariables('%TOPIC%');
// or
var topic = getVariable('TOPIC');
It is possible to modify preference variables by calling
setVariable().
<!--
* Set FOO = Value 1
-->
<%
setVariable('FOO', 'Value 2');
%>
%FOO% <!-- "Value 2" is printed here -->
For the
%EJS_INCLUDE{...}% tag, a completely new JavaScript environment is created with an initial namespace each time
%EJS_INCLUDE{...}% is expanded.
Thus, functions and variables defined in JavaScript code cannot be shared between the including and included topics.
The three examples below demonstrate the namespace separation:
<!-- Topic name: TopicA -->
<%
function funcA() {...} // Define funcA()
%>
<!-- Topic name: TopicB -->
%EJS_INCLUDE{"TopicA"}%
<%
// funcA() is not defined here
%>
<%
function funcB() {...} // Define funcB()
%>
%EJS_INCLUDE{"TopicC"}%
<!-- Topic name: TopicC -->
<%
// funcB() is not defined here
%>
Developing Libraries
Re-usable libraries can be developed in a TWiki topic, which can be referenced via the
requireTopic().
<!-- Topic name: ExampleLibrary -->
<%
function exampleFunction1() {
...
}
function exampleFunction2() {
...
}
%>
<!-- Topic name: UsingLibrary -->
<%
requireTopic('ExampleLibrary'); // Load library
exampleFunction1();
exampleFunction2();
%>
The
requireTopic() will load the specified topic
only once. The expected usage of
requireTopic() is to write JavaScript functions (or class-like implementation), which can be utilized in other topics. It should not be used like
%INCLUDUE{...}% to print texts just by calling
requireTopic().
It is a good practice to put a collection of library code in a designated Web, so that the library components can be utilized across many Webs.
If the
requireTopic() is used within a library to load another library (often sub-components), the topic name is referenced relative to where the
requireTopic() is invoked.
<%
requireTopic('LibraryWeb.ExampleLibrary');
%>
<!-- LibraryWeb.ExampleLibrary -->
<%
requireTopic('SubComponent1');
requireTopic('SubComponent2');
// These sub-components are assumed to be in the same "LibraryWeb"
%>
Function Arguments
Most EJS API functions parse the given arguments in two ways:
positional and
keyed.
For example,
findTopics() can accept the arguments in either of the following ways:
findTopics('Web.Topic*');
findTopics({topic: 'Web.Topic*'});
findTopics({web: 'Web', topic: 'Topic*'});
Keyed parameters can span across multiple arguments:
findTopics({web: 'Web'}, {topic: 'Topic*'});
If the same key appears multiple times, the last one has the precedence.
A callback function can be given as a positional function object or a
callback key:
findTopics('Web.Topic*', function () {...});
findTopics('Web.Topic*', {callback: function () {...}});
TWiki variables (
%VARIABLE%) included in the arguments are expanded if the arguments are of the following types:
-
web, toWeb, baseWeb
-
topic, toTopic
-
file, toFile
-
user
-
url
In other cases, TWiki variables are
not expanded, but used literally.
For example,
saveTopic() takes two arguments
topic and
text, where only
topic argument will expand TWiki variables.
saveTopic('%TOPIC%_Data', 'Content contains %VARIABLE%');
In the above example, the first argument is expanded (since it is the
topic argument), while the second argument is
not expanded (since it is the
text argument).
Callback
Some EJS API functions accept a callback function, where the callback invocation is usually a loop iteration.
findTopics() is an example:
<%
findTopics('IssueItem*', function (topic, loop) {
// "topic" is a string like "IssueItem1234"
// "loop" is an object - See below
});
%>
The first argument is each value in the loop, and the second argument is a loop object that has information about the current loop iteration.
| Property |
Value |
Description |
loop.first |
0 or 1 |
The value is 1 if this is the first iteration in the loop; 0 otherwise |
loop.last |
0 or 1 |
The value is 1 if this is the last iteration in the loop; 0 otherwise |
loop.index |
0-based index |
The index starts at 0 for the first iteration, and increments as the loop goes on |
loop.value |
Current value |
The value of the current loop while iterating through multiple values |
The return value of the callback function will affect the result array as the final return value of the API function.
If the callback returns nothing (
undefined or
null), the value in the iteration will be excluded from the result. All other return values from the callback will be the values of the final array.
<%
var topics = findTopics('IssueItem*', function (topic, loop) {
if (!loop.first) {
return '<b>' + topic + '</b>';
}
});
/*
Returns something like:
['<b>IssueItem2</b>', '<b>IssueItem3</b>', '<b>IssueItem4</b>', ...]
*/
%>
Save Action Policies
When TWiki contents are modified via EJS (e.g.
saveTopic() and
moveTopic()), there are some restrictions in order to prevent unintended data changes.
For example, when the EJS script page is displayed right after saving the script, the modification would take place immediately even if you just wanted to save the script temporarily.
In addition, a search engine crawler might access your page while you are developing something, where
saveTopic() call might corrupt some data if it were not for any protection.
POST Method Policy
By default, the HTTP POST method is required to invoke save actions. In order to run EJS with any save actions, the
POST method should be sent to the TWiki
view script (rather than the
save script etc.).
<form method="POST">
<input type="submit" value="Save">
</form>
<%
if (isPost()) {
var topic = "...";
var text = "...";
saveTopic(topic, text);
println("Saved!");
}
%>
If you are very confident, set
EJS_POST_METHOD_POLICY preference variable to
off to disable this policy. See also
#Configurations.
Same-Web Policy
By default, the save actions can modify only the contents within the same Web or its SubWebs (and all the way downwards recursively). This is to prevent the EJS script to modify contents of unintended Webs outside the current scope. Note the save actions can be invoked from within libraries (loaded by
requireTopic()) to modify contents within the
currently visited Web, rather than where the library topic resides.
If you are very confident, set
EJS_SAME_WEB_POLICY preference variable to
off to disable this policy. See also
#Configurations.
Crypt Token Policy
If
$TWiki::cfg{CryptToken}{Enable} is turned on, any save actions listed by
$TWiki::cfg{CryptToken}{SecureActions} will require the CRYPTTOKEN as the POST parameter.
<form method="POST">
<input type="hidden" name="crypttoken" value="%CRYPTTOKEN%">
<input type="submit" value="Save">
</form>
<%
if (isPost()) {
...
}
%>
Namespace
All the built-in API functions are defined in the global scope, which may not be ideal in some cases.
If the
EJS_NAMESPACE preference variable is set, the API functions will be defined in the specified object name.
<!--
* Set EJS_NAMESPACE = TWiki
-->
<%
TWiki.findTopics(...);
%>
The namespace can be specified as chained object path by the "." notation (e.g. "Foo.Bar.Baz") but it does not allow arbitrary JavaScript expression.
The following functions are always defined in the global scope (that is, they cannot be in the specified namespace):
The namespace for
%EJS_INCLUDE{...}% also inherits this setting, but it is also possible to specify the namespace in the tag, if the included component is implemented with an assumed namespace.
%EJS_INCLUDE{"ComponentTopic" namespace="SomeNamespace"}%
Data Types
TWiki source code is written in Perl, and the EJS API functions attempt to convert values between JavaScript and Perl as much as possible.
However, due to the difference between the two languages, there are some data types that cannot be converted straightforwardly.
Boolean Values
Although JavaScript has the notion of Boolean values (
true and
false), some EJS API functions return
1 or
0 instead, due to the limitation with Perl.
While the value can be used as a conditional expression (such as
if statement), it should not be assumed that the values are integer or boolean for potential future compatibility.
Null Values
EJS API functions will not distintuish
undefined and
null if they are passed as arguments. EJS API will always return
undefined when there are no values to return.
Somewhat confusingly, if values are serialized by
JSON.stringify(), then the corresponding value is encoded as
null.
Date/Time Values
Some EJS API functions return a Unix timestamp (such as
getEditLock()), which is an integer value of seconds that have elapsed since 1 January 1970.
The integer value can be converted to a JavaScript
Date object like this:
var expires = getEditLock('SomeTopic').expires; // Unix timestamp in seconds
var dateObject = new Date(expires * 1000); // Convert timestamp to milliseconds and pass it as the argument
Similarly, some EJS API functions accept a Unix timestamp as an argument (
getRevisionAtTime()), which can be converted like this:
var dateObject = new Date();
dateObject.setMonth(dateObject.getMonth() - 3); // Three months ago
getRevisionAtTime('SomeTopic', dateObject.getTime() / 1000); // Convert milliseconds to timestamp
Custom Object Types
If a custom object (instanciated by the
new operator) is passed as an argument to an EJS API function, it is converted to a plain object.
function CustomParam(web, topic) {
this.web = web;
this.topic = topic;
}
var param = new CustomParam('WebName', 'TopicName');
readTopic(param); // same as readTopic({web: 'WebName', topic: 'TopicName'})
Exceptions
Some API functions throw an exception that your JavaScript code can
catch.
Due to the limitation with conversion between Perl and JavaScript, API functions can only throw a standard
Error object.
The
message property of the object contains the string error message.
try {
readTopic('NonExistingTopic');
} catch (e) {
println(e.message);
}
Raw View
If EJS is turned on, it is executed when the raw view is accessed with
?raw=expandvariables.
Dynamic Template
If EJS dynamic template is enabled, EJS is executed when a new topic is created with a template topic.
A template topic is copied into the edit page's text box for topic creation, where the new content is dynamically generated by executing EJS in the template topic.
For example, consider a topic named
ExampleTemplate contains some EJS script:
---+ %TOPICTITLE%
Copyright (c) <%=new Date().getFullYear()%>
When a new topic named
NewTopic is being created where
ExampleTemplate is specified as the topic template, the edit page will start with the generated content:
---+ %TOPICTITLE%
Copyright (c) 2017
The
WebTopicEditTemplate topic can also be used as an EJS dynamic template.
This feature can be enabled by either
EJS_DYNAMIC_TEMPLATE preference variable (which should be placed in the
WebPreferences topic) or
$TWiki::cfg{Plugins}{EmbeddedJSPlugin}{DefaultDynamicTemplate} set by the administrator.
Configurations
| Preference Variable |
Configuration |
Default |
Description |
EJS |
$TWiki::cfg{Plugins}{EmbeddedJSPlugin}{DefaultExecute} |
off |
Enable EJS execution (view script). |
EJS_DYNAMIC_TEMPLATE |
$TWiki::cfg{Plugins}{EmbeddedJSPlugin}{DefaultDynamicTemplate} |
off |
Enable EJS dynamic template (edit script). |
EJS_NAMESPACE |
$TWiki::cfg{Plugins}{EmbeddedJSPlugin}{DefaultNamespace} |
|
Specify the namespace for API functions. |
EJS_POST_METHOD_POLICY |
$TWiki::cfg{Plugins}{EmbeddedJSPlugin}{DefaultPostMethodPolicy} |
on |
Apply POST method policy for save actions. |
EJS_SAME_WEB_POLICY |
$TWiki::cfg{Plugins}{EmbeddedJSPlugin}{DefaultSameWebPolicy} |
on |
Apply same-web policy for save actions. |
| |
$TWiki::cfg{Plugins}{EmbeddedJSPlugin}{JavaScriptEngine} |
|
Set JavaScript engine, which is automatically determined by CPAN:EJS::Template by default. |
EJS_TIMEOUT |
$TWiki::cfg{Plugins}{EmbeddedJSPlugin}{DefaultTimeout} |
5 |
Set JavaScript execution timeout in seconds. |
| |
$TWiki::cfg{Plugins}{EmbeddedJSPlugin}{MaxTimeout} |
180 |
Set the maximum limit configurable by the EJS_TIMEOUT preference variable. |
EJS_DEFAULT_BASE_WEB |
$TWiki::cfg{Plugins}{EmbeddedJSPlugin}{DefaultBaseWeb} |
_default |
Set the template web name used for createWeb() API. |
EJS_INCLUDE
Below is a list of parameters that can be specified for the
%EJS_INCLUDE{...}% tag.
| Parameter |
Description |
_DEFAULT |
Topic name of the EJS component |
namespace |
Namespace for EmbeddedJSPlugin API functions, if required by the EJS component |
postMethodPolicy |
True/false to enable/disable the POST method policy |
sameWebPolicy |
True/false to enable/disable the same-web policy |
timeout |
Timeout in seconds |
defaultBaseWeb |
Template web name for createWeb() API |
By default, these parameters are inherited from the current preference variables or TWiki configurations.
Installation Instructions
Note: You do not need to install anything on the browser to use this plugin. The following instructions are for the administrator who installs the plugin on the TWiki server.
- For an automated installation, run the configure script and follow "Find More Extensions" in the in the Extensions section.
- Or, follow these manual installation steps:
- Download the ZIP file from the Plugins home (see below).
- Unzip
EmbeddedJSPlugin.zip in your twiki installation directory. Content: | File: | Description: |
data/TWiki/EmbeddedJSPlugin*.txt | Plugin topics |
lib/TWiki/Plugins/EmbeddedJSPlugin.pm | Plugin Perl module |
lib/TWiki/Plugins/EmbeddedJSPlugin/*.pm | Component modules |
- Set the ownership of the extracted directories and files to the webserver user.
- Install the dependencies.
- Plugin configuration and testing:
- Run the configure script and enable the plugin in the Plugins section.
- Configure additional plugin settings in the Extensions section if needed.
- Test if the installation was successful using the example above.
Plugin Info
Many thanks to the following sponsors for supporting this work:
- Acknowledge any sponsors here
Related Topics: TWikiPlugins,
DeveloperDocumentationCategory,
AdminDocumentationCategory,
TWikiPreferences