Tags:
create new tag
, view all tags

EmbeddedJSPlugin

EJS (Embedded JavaScript) plugin

IDEA! 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.

  1. Preference variables (* Set NAME = VALUE) are extracted.
  2. EJS tags (<% ... %>) are processed with a JavaScript engine.
  3. TWiki variables (%VARIABLE%) are expanded, where %EJS_INCLUDE{...}% tags are also expanded.
  4. 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

Plugin Author(s): TWiki:Main.MahiroAndo
Copyright: © 2017 TWiki:Main.MahiroAndo
© 2017 TWiki:TWiki.TWikiContributor
License: GPL (Gnu General Public License)
Plugin Version: 2017-08-24
Change History:  
2017-08-24: TWikibug:Item7817: Rename plugin to WikiName - TWiki:Main.MahiroAndo
2017-08-05: TWikibug:Item7817: Add some API functions - TWiki:Main.MahiroAndo
2017-07-21: TWikibug:Item7817: Initial release - TWiki:Main.MahiroAndo
Dependencies: CPAN:EJS::Template, JavaScript engine (either CPAN:JE or CPAN:JavaScript::V8), CPAN:HTML::Entities, CPAN:IO::String, CPAN:JSON, CPAN:Scalar::Util, CPAN:Text::CSV
Plugin Home: http://twiki.org/cgi-bin/view/Plugins/EmbeddedJSPlugin
Feedback: http://twiki.org/cgi-bin/view/Plugins/EmbeddedJSPluginDev
Appraisal: http://twiki.org/cgi-bin/view/Plugins/EmbeddedJSPluginAppraisal

Related Topics: TWikiPlugins, DeveloperDocumentationCategory, AdminDocumentationCategory, TWikiPreferences

Topic attachments
I Attachment History Action Size Date Who Comment
Unknown file formatmd5 EmbeddedJSPlugin.md5 r1 manage 0.2 K 2017-08-24 - 10:44 MahiroAndo  
Compressed Zip archivetgz EmbeddedJSPlugin.tgz r1 manage 36.9 K 2017-08-24 - 10:44 MahiroAndo  
Compressed Zip archivezip EmbeddedJSPlugin.zip r1 manage 41.1 K 2017-08-24 - 10:44 MahiroAndo  
Unknown file formatEXT EmbeddedJSPlugin_installer r1 manage 4.1 K 2017-08-24 - 10:44 MahiroAndo  
Topic revision: r1 - 2017-08-24 - MahiroAndo
 
  • Learn about TWiki  
  • Download TWiki
This site is powered by the TWiki collaboration platform Powered by Perl Hosted by OICcam.com Ideas, requests, problems regarding TWiki? Send feedback. Ask community in the support forum.
Copyright © 1999-2017 by the contributing authors. All material on this collaboration platform is the property of the contributing authors.