From: <de...@de...> - 2017-07-21 09:20:46
|
Author: MahiroAndo Date: 2017-07-21 08:59:17 +0000 (Fri, 21 Jul 2017) New Revision: 30358 Trac url: http://develop.twiki.org/trac/changeset/30358 Added: twiki/trunk/EJSPlugin/.gitignore twiki/trunk/EJSPlugin/data/ twiki/trunk/EJSPlugin/data/TWiki/ twiki/trunk/EJSPlugin/data/TWiki/EJSPluginAPI.txt twiki/trunk/EJSPlugin/data/TWiki/EJSPluginTableAPI.txt twiki/trunk/EJSPlugin/data/TWiki/VarEJS_INCLUDE.txt twiki/trunk/EJSPlugin/lib/ twiki/trunk/EJSPlugin/lib/TWiki/ twiki/trunk/EJSPlugin/lib/TWiki/Plugins/ twiki/trunk/EJSPlugin/lib/TWiki/Plugins/EJSPlugin.pm twiki/trunk/EJSPlugin/lib/TWiki/Plugins/EJSPlugin/ twiki/trunk/EJSPlugin/lib/TWiki/Plugins/EJSPlugin/Config.spec twiki/trunk/EJSPlugin/lib/TWiki/Plugins/EJSPlugin/DEPENDENCIES twiki/trunk/EJSPlugin/lib/TWiki/Plugins/EJSPlugin/Func.pm twiki/trunk/EJSPlugin/lib/TWiki/Plugins/EJSPlugin/MANIFEST twiki/trunk/EJSPlugin/lib/TWiki/Plugins/EJSPlugin/build.pl twiki/trunk/EJSPlugin/t/ twiki/trunk/EJSPlugin/t/01-basics.t twiki/trunk/EJSPlugin/t/02-parseParam.t twiki/trunk/EJSPlugin/t/03-findTopics.t twiki/trunk/EJSPlugin/t/testenv.cfg Log: Item7817: Import initial code Added: twiki/trunk/EJSPlugin/.gitignore =================================================================== --- twiki/trunk/EJSPlugin/.gitignore (rev 0) +++ twiki/trunk/EJSPlugin/.gitignore 2017-07-21 08:59:17 UTC (rev 30358) @@ -0,0 +1,2 @@ +EJSPlugin.* +EJSPlugin_* Added: twiki/trunk/EJSPlugin/data/TWiki/EJSPluginAPI.txt =================================================================== --- twiki/trunk/EJSPlugin/data/TWiki/EJSPluginAPI.txt (rev 0) +++ twiki/trunk/EJSPlugin/data/TWiki/EJSPluginAPI.txt 2017-07-21 08:59:17 UTC (rev 30358) @@ -0,0 +1,710 @@ +%META:TOPICINFO{author="TWikiContributor" date="1495793747" format="1.1" version="$Rev$"}% +---+!! !EJSPlugin API + +This page lists all the API functions for [[EJSPlugin]]. See also [[EJSPluginTableAPI][EJS Table API List]]. + +<!-- + * Set EJS = off + * Set SAVE_ACTION_POLICIES = See also [[EJSPlugin#Save_Action_Policies][Save Action Policies]]. + * Set DATE_TIME_VALUES = See also [[EJSPlugin#Date_Time_Values][Date/Time Values]]. +--> + +%TOC% + +---++ print +<verbatim> +print("text"[, "text" ...]); +</verbatim> + +Appends output texts. +Multiple arguments are appended altogether. +If non-string objects are given, [[#JSON][JSON.stringify()]] is automatically used to serialize the data. + +---++ println +<verbatim> +println("text"[, "text" ...]); +</verbatim> + +Appends output texts in the same way as [[#print][print()]], followed by a line break at the end. + +---++ requireTopic +<verbatim> +requireTopic("[Web.]Topic"); +requireTopic({web: "Web", topic: "Topic"}); +</verbatim> + +Executes the topic content as EJS. A topic is loaded only once even when used multiple times. +It should usually be used to load !JavaScript libraries. + +An exception is thrown if the topic does not exist or if the VIEW permission is denied. + +---++ findWebs +<verbatim> +var webNames = findWebs("Web*"); +var webNames = findWebs({web: "Web*"}); +var formattedNames = findWebs("Web*", function (webName, loop) {}); +var formattedNames = findWebs({web: "Web*", callback: function (webName, loop) {}}); +</verbatim> + +Returns an array of web names that are matched with a pattern with wildcards. + +---++ findTopics +<verbatim> +var topicNames = findTopics("[Web*.]Topic*"); +var topicNames = findTopics({web: "Web*", topic: "Topic*"}); +var formattedNames = findTopics("[Web*.]Topic*", function (topicName, loop) {}); +var formattedNames = findTopics({web: "Web*", topic: "Topic*", callback: function (topicName, loop) {}}); +</verbatim> + +Returns an array of topic names that are matched with a pattern with wildcards. The web name can also include wildcards. + +If a web name is specified, the returned topic names will contain the web name prefix. + +For example: + +<verbatim> +findTopics('TestTopic*'); // returns ['TestTopic1', 'TestTopic2'] +findTopics('WebA.TestTopic*'); // returns ['WebA.TestTopic1', 'WebA.TestTopic2'] +findTopics('Web*.TestTopic*'); // returns ['WebA.TestTopic1', 'WebA.TestTopic2', 'WebB.TestTopic1', 'WebB.TestTopic2'] +</verbatim> + +---++ webExists +<verbatim> +var exists = webExists("Web"); +var exists = webExists({web: "Web"}); +</verbatim> + +Determines if the web exists. + +---++ topicExists +<verbatim> +var exists = topicExists("[Web.]Topic"); +var exists = topicExists({web: "Web", topic: "Topic"}); +</verbatim> + +Determines if the topic exists. + +---++ createWeb +<verbatim> +createWeb("%WEB%/SubWeb"); +createWeb("%WEB%/SubWeb", {baseWeb: "TemplateWeb"}); +createWeb({web: "%WEB%/SubWeb", baseWeb: "TemplateWeb"}); +</verbatim> + +Creates a new web or subweb. Because of the default same-web policy, this API is usually used to create a subweb rather than a top-level web. + +If the =baseWeb= parameter is given, the new web is copied from the baseWeb. +Otherwise, the baseWeb defaults to the configured value (=$TWiki::cfg{EJSPlugin}{DefaultBaseWeb}=). + +%SAVE_ACTION_POLICIES% + +---++ moveWeb +<verbatim> +moveWeb("%WEB%/SubWeb1", "%WEB%/SubWeb2"); +moveWeb({web: "%WEB%/SubWeb1", toWeb: "%WEB%/SubWeb2"}); +</verbatim> + +Renames a web or subweb, or moves it to another web or subweb. + +Note: Links are *not* automatically updated. + +%SAVE_ACTION_POLICIES% + +---++ removeWeb +<verbatim> +removeWeb("%WEB%/SubWeb"); +removeWeb("%WEB%/SubWeb", {toWeb: "%TRASHWEB%/SubWeb"}); +removeWeb({web: "%WEB%/SubWeb", toWeb: "%TRASHWEB%/SubWeb"}); +</verbatim> + +Removes a web or subweb. The target web will be moved to the trash web, rather than permanently deleted. + +%SAVE_ACTION_POLICIES% + +---++ readTopic +<verbatim> +var topicText = readTopic("[Web.]Topic"); +var topicText = readTopic("[Web.]Topic", {rev: revisionNumber}); +var topicText = readTopic({web: "Web", topic: "Topic", rev: revisionNumber}); +</verbatim> + +Returns the topic content. + +An exception is thrown if the topic does not exist or if the VIEW permission is denied. + +---++ saveTopic +<verbatim> +saveTopic("[Web.]Topic", "TopicText"); +saveTopic("[Web.]Topic", "TopicText", {forceNewRevision: true, minor: true, breakLock: true, parentTopic: "ParenTopic"}); +saveTopic({web: "Web", topic: "Topic", text: "TopicText", forceNewRevision: true, minor: true, breakLock: true, parentTopic: "ParentTopic"}); +</verbatim> + +Saves the topic content. If the topic does not exist, a new topic is created. + +An exception is thrown if the specified web does not exist or if the CHANGE permission is denied. + +Additional parameter(s) can be specified as below: + +| *Parameter* | *Value* | +| =forceNewRevision= | Force the revision to be incremented, even when the revision number should otherwise stay the same. | +| =minor= | Suppress notification (similarly to =checkpoint= or =quietsave=). | +| =breakLock= | Ignore the edit lock acquired by another user. | +| =parentTopic= | Set the parent topic name. If the value is either =undefined=, =null=, =""=, or ="none"=, then the parent topic will be unset. | +| =formName= | Set the form name (same as [[#setFormName][setFormName()]]) | +| _FormFieldName_ | Form field name/value pairs can be specified to update the values (same as [[#setFormField][setFormField()]]) | + +%SAVE_ACTION_POLICIES% + +---++ moveTopic +<verbatim> +moveTopic("[FromWeb.]FromTopic", "[ToWeb.]ToTopic"); +moveTopic("[FromWeb.]FromTopic", "[ToWeb.]ToTopic", {breakLock: true}); +moveTopic({web: "FromWeb", topic: "FromTopic", toWeb: "ToWeb", toTopic: "ToTopic", breakLock: true}); +</verbatim> + +Renames the topic or moves it to another web. + +An exception is thrown if the topic does not exist or if the CHANGE permission is denied. + +Additional parameter(s) can be specified as below: + +| *Parameter* | *Value* | +| =breakLock= | Ignore the edit lock acquired by another user. | + +%SAVE_ACTION_POLICIES% + +---++ removeTopic +<verbatim> +removeTopic("[Web.]Topic"); +removeTopic("[Web.]Topic", {toTopic: "ToTopic"}); +removeTopic({web: "Web", topic: "Topic", toWeb: "%TRASHWEB%", toTopic: "ToTopic"}); +</verbatim> + +Removes a topic. The target topic will be moved to the trash web, rather than permanently deleted. + +%SAVE_ACTION_POLICIES% + +---++ getFormName +<verbatim> +var formName = getFormName("[Web.]Topic"); +var formName = getFormName("[Web.]Topic", {rev: revisionNumber}); +var formName = getFormName({web: "Web", topic: "Topic", rev: revisionNumber}); +</verbatim> + +Retrieves the form name of the topic. + +An exception is thrown if the topic does not exist or if the VIEW permission is denied. + +---++ setFormName +<verbatim> +setFormName("[Web.]Topic", "ExampleForm"); +setFormName({web: "Web", topic: "Topic", name: "ExampleForm"}); +</verbatim> + +Saves the topic with the new form name. + +An exception is thrown if the topic does not exist or if the CHANGE permission is denied. + +The call of this function will eventually invoke the same logic as [[#saveTopic][saveTopic()]] so the same parameters are accepted. + +%SAVE_ACTION_POLICIES% + +---++ getFormField +<verbatim> +var fieldValue = getFormField("[Web.]Topic", "FieldName"); +var fieldValue = getFormField("[Web.]Topic", "FieldName", {rev: revisionNumber}); +var fieldValue = getFormField({web: "Web", topic: "Topic", name: "FieldName", rev: revisionNumber}); +</verbatim> + +Retrieves the form field value of the topic. + +An exception is thrown if the topic does not exist or if the VIEW permission is denied. + +---++ setFormField +<verbatim> +setFormField(["Web.]Topic", "FieldName", "FieldValue"); +setFormField({web: "Web", topic: "Topic", name: "FieldName", value: "FieldValue"}); +</verbatim> + +Saves the topic with the new form field name/value pair(s). + +An exception is thrown if the topic does not exist or if the CHANGE permission is denied. + +The call of this function will eventually invoke the same logic as [[#saveTopic][saveTopic()]] so the same parameters are accepted. +If multiple fields are to be set, it is more efficient to use [[#saveTopic][saveTopic()]] to set all the fields at the same time. + +%SAVE_ACTION_POLICIES% + +---++ getFormFields +<verbatim> +var fields = getFormFields("[Web.]Topic"); +var fields = getFormFields("[Web.]Topic", {rev: revisionNumber}); +var fields = getFormFields({web: "Web", topic: "Topic", rev: revisionNumber}); +</verbatim> + +Retrieves all the form fields of the topic. The return value is an object of field/value pairs. + +An exception is thrown if the topic does not exist or if the VIEW permission is denied. + +---++ checkPermission +<verbatim> +var allowed = checkPermission("[Web.]Topic", "VIEW"); // or "CHANGE" +var allowed = checkPermission({web: "Web", topic: "Topic", type: "VIEW"}); // or "CHANGE" +</verbatim> + +Determines if the specified permission type is allowed for the topic. + +An exception is thrown if the topic does not exist. + +---++ getRevisionInfo +<verbatim> +var revisionInfo = getRevisionInfo("[Web.]Topic"); +var revisionInfo = getRevisionInfo("[Web.]Topic", {rev: revisionNumber}); +var revisionInfo = getRevisionInfo({web: "Web", topic: "Topic", rev: revisionNumber}); +</verbatim> + +Retrieves the revision information. +An exception is thrown if the topic does not exist or if the VIEW permission is denied. + +| *Return Value* | *Description* | +| revisionInfo.rev | Revision number, such as 1, 2, 3, ... | +| revisionInfo.time | Unix timestamp | +| revisionInfo.user | User name stored in the revision management system | +| revisionInfo.userName | Converted user name, retrieved by [[#getUserName][getUserName()]] | + +%DATE_TIME_VALUES% + +---++ getRevisionAtTime +<verbatim> +var revisionNumber = getRevisionAtTime("[Web.]Topic", unixTimestamp); +var revisionNumber = getRevisionAtTime({web: "Web", topic: "Topic", time: unixTimestamp}); +</verbatim> + +Retrieves the revision number at the specified Unix timestamp. + +%DATE_TIME_VALUES% + +---++ getEditLock +<verbatim> +var editLock = getEditLock("[Web.]Topic"); +var editLock = getEditLock({web: "Web", topic: "Topic"}); +</verbatim> + +Retrieves the edit lock (a.k.a. lease file) of the topic. +Returns =undefined= if there are no lease files. + +An exception is thrown if the topic does not exist or if the VIEW permission is denied. + +| *Return Value* | *Description* | +| editLock.user | The user name stored in the lease file | +| editLock.userName | Converted user name, retrieved by [[#getUserName][getUserName()]] | +| editLock.conflict | A string indicating a lock conflict ("lease_active" or "lease_old"); or an empty string if no conflicts | +| editLock.taken | Unix timestamp at which the lock was taken | +| editLock.expires | Unix timestamp at which the lock is to expire | + +%DATE_TIME_VALUES% + +---++ acquireEditLock +<verbatim> +acquireEditLock("[Web.]Topic"); +acquireEditLock({web: "Web", topic: "Topic"}); +</verbatim> + +Acquires the edit lock (a.k.a. lease file) to prevent other users from editing the same topic. + +An exception is thrown if there is a lease conflict with another user, if the topic does not exist or if the CHANGE permission is denied. + +---++ releaseEditLock +<verbatim> +releaseEditLock("[Web.]Topic"); +releaseEditLock({web: "Web", topic: "Topic"}); +</verbatim> + +Releases the edit lock acquired by the current user if any. If there is an edit lock acquired by another user, the lease file is left unchanged. + +An exception is thrown if the topic does not exist. + +---++ getAttachmentList +<verbatim> +var fileNames = getAttachmentList("[Web.]Topic"); +var fileNames = getAttachmentList({web: "Web", topic: "Topic"}); +</verbatim> + +Returns an array of attachment names of the topic. + +An exception is thrown if the topic does not exist or if the VIEW permission is denied. + +---++ attachmentExists +<verbatim> +var exists = attachmentExists("[Web.]Topic", "FileName.txt"); +var exists = attachmentExists({web: "Web", topic: "Topic", file: "FileName.txt"}); +</verbatim> + +Determines if the attachment exists. + +---++ readAttachment +<verbatim> +var contentText = readAttachment("[Web.]Topic", "FileName.txt"); +var contentText = readAttachment("[Web.]Topic", "FileName.txt", {rev: fileRevisionNumber}); +var contentText = readAttachment({web: "Web", topic: "Topic", file: "FileName.txt", rev: fileRevisionNumber}); +</verbatim> + +Retrieves the attachment file content. + +An exception is thrown if the attachment does not exist or if the VIEW permission is denied. + +---++ saveAttachment +<verbatim> +saveAttachment("[Web.]Topic", "FileName.txt", "CataText"); +saveAttachment({web: "Web", topic: "Topic", file: "FileName.txt", data: "CataText"}); +</verbatim> + +Saves the attachment file content. If the attachment does not exist, a new attachment is created. + +An exceptiton is thrown if the topic does not exist or if the CHANGE permission is denied. + +%SAVE_ACTION_POLICIES% + +---++ moveAttachment +<verbatim> +moveAttachment("[FromWeb.]FromTopic", "FromFileName.txt", "[ToWeb.]ToTopic", "ToFileName.txt"); +moveAttachment({web: "FromWeb", topic: "FromTopic", file: "FromFileName.txt", toWeb: "ToWeb", toTopic: "ToTopic", toFile: "ToFileName.txt"}); +</verbatim> + +Renames the attachment or moves it to another topic. + +An exception is thrown if the attachment does not exist or if the CHANGE permission is denied. + +%SAVE_ACTION_POLICIES% + +---++ removeAttachment +<verbatim> +removeTopic("[Web.]Topic", "FileName.txt"); +removeTopic({web: "Web", topic: "Topic", file: "FileName.txt", toWeb: "%TRASHWEB%", toTopic: "ToTopic", toFile: "ToFileName.txt"}); +</verbatim> + +Removes an attachment. The target attachment will be moved to the =TrashAttachment= topic in the trash web, rather than permanently deleted. + +%SAVE_ACTION_POLICIES% + +---++ getMethod +<verbatim> +var method = getMethod(); // "GET" or "POST" +</verbatim> + +Retrieves the current HTTP request method. + +---++ isPost +<verbatim> +var condition = isPost(); +</verbatim> + +Determines if the current HTTP request method is POST. + +---++ getParam +<verbatim> +var paramValue = getParam("ParamName"); +var paramValue = getParam("ParamName", "DefaultValue"); +var paramValue = getParam({name: "ParamName", default: "DefaultValue"}); +</verbatim> + +Retrieves the URL parameter value (GET) or form-data value (POST). + +---++ setParam +<verbatim> +setParam("ParamName", "ParamValue"); +setParam({name: "ParamName", value: "ParamValue"}); +</verbatim> + +Sets the URL parameter value (GET) or form-data value (POST) for later use in the session. +It affects the actual usage of =%<nop>URLPARAM{...}%= in the TWiki markup after EJS preprocessing is done. + +---++ getUserName +<verbatim> +var userName = getUserName(); +var userName = getUserName("someUserName"); +var userName = getUserName({user: "someUserName"}); +</verbatim> + +Retrieves the current user name if there are no arguments. + +If the argument is given, it is converted to the canonical or login user name. + +---++ getWikiName +<verbatim> +var wikiName = getWikiName(); +var wikiName = getWikiName("someUserName"); +var wikiName = getWikiName({user: "someUserName"}); +</verbatim> + +Retrieves the current user's wiki name (such as =UserFoobar=) if there are no arguments. + +If the argument is given, it is converted to the user's wiki name. + +---++ getWikiUserName +<verbatim> +var wikiUserName = getWikiUserName(); +var wikiUserName = getWikiUserName("someUserName"); +var wikiUserName = getWikiUserName({user: "someUserName"}); +</verbatim> + +Retrieves the current user's wiki user name (such as =%USERSWEB%.UserFoobar=) if there are no arguments. + +If the argument is given, it is converted to the user's wiki user name. + +---++ getVariable +<verbatim> +var variableValue = getVariable("VARIABLE_NAME"[, {param: value, ...}]); +</verbatim> + +Retrieves the value of the TWiki variable (=%<nop>VARIABLE%=). + +Optionally, parameters can be specified as key/value pairs (=%<nop>VARIABLE{param="value"}%=). +The default parameter is passed as =_DEFAULT= key (=%<nop>VARIABLE{"The default value"}%=). + +---++ setVariable +<verbatim> +setVariable("VARIABLE_NAME", "VariableValue"); +</verbatim> + +Sets the value of the TWiki variable (=%<nop>VARIABLE%=) for later use. +It affects the actual usage of =%<nop>VARIABLE%= in the TWiki markup after EJS preprocessing is done. + +---++ expandVariables +<verbatim> +var expandedText = expandVariables("%FOO% %BAR% %BAZ{...}%"); +</verbatim> + +Expands all the occurrences of the TWiki variables (=%<nop>VARIABLE%=). + +---++ expandAutoInc +<verbatim> +var newTopicName = expandAutoInc("TopicNameAUTOINC001"); +</verbatim> + +Replaces the =AUTOINC= pattern in a topic name with the next number to make a new (non-existing) topic name. + +---++ formatText +<verbatim> +var formatedText = formatText("$foo $bar $baz", {foo: "Foo", bar: "Bar", baz: "Baz"}); +</verbatim> + +Converts the variable names in the =$variable= format, where the values are replaced by the given key/value pairs. +The standard escape variables (such as =$percnt= and =$dollar=) are also expanded. +Non-existing variable names are ignored, and the =$variable= will appear in the result string. + +---++ isTrue +<verbatim> +var condition = isTrue("on/off"); +var condition = isTrue("on/off", "default"); +</verbatim> + +Determines if the given text is a true value. +If it is either "off", "false" or "no", then the value is regarded as false. +If is it =undefined= or =null=, the default value is used instead. + +---++ normalizeWebTopicName +<verbatim> +var nameInfo = normalizeWebTopicName('[Web.]Topic'); +var nameInfo = normalizeWebTopicName('Web', 'Topic'); +var nameInfo = normalizeWebTopicName({web: 'Web', topic: 'Topic'}); +</verbatim> + +Normalizes the web name and topic name. + +| *Return Value* | *Description* | +| =nameInfo.web= | Normalized web name | +| =nameInfo.topic= | Normalized topic name | + +---++ escapeHTML +<verbatim> +var escapedText = escapeHTML("< unescaped >"); +</verbatim> + +Converts special HTML characters (such as =<= and =>=) into HTML entity strings (such as =&lt;= and =&gt;=). + +---++ unescapeHTML +<verbatim> +var unescapedText = unescapeHTML("< escaped >"); +</verbatim> + +Converts HTML entity strings (such as =&lt;= and =&gt;=) into the original HTML characters (such as =<= and =>=). + +---++ escapeQuote +<verbatim> +var escapedText = escapeQuote("\"unescaped\" text\n"); +</verbatim> + +Escapes special characters so that the text can be embedded in the JSON or !JavaScript string notation. + +---++ unescapeQuote +<verbatim> +var unescapedText = unescapeQuote("\\\"escaped\\\" text\\n"); +</verbatim> + +Unescapes special characters that are escaped by [[#escapeQuote][escapeQuote()]]. + +---++ escapeTable +<verbatim> +var escapedText = escapeTable("unescaped || text"); +</verbatim> + +Escapes special characters so that the text can be embedded in the !TWiki table notation. +Special HTML characters are also converted to HTML entity strings. + +---++ unescapeTable +<verbatim> +var unescapedText = unescapeTable("escaped %VBAR%%VBAR% text"); +</verbatim> + +Unescapes special characters that are escaped by [[#escapeTable][escapeTable()]]. +HTML entity strings are also converted to the original HTML characters. + +---++ parseTable +<verbatim> +var table = parseTable("| *A* | *B* |\n" + "| 1 | 2 |\n" + "| 3 | 4 |\n"); +var table = parseTable("| 1 | 2 |\n" + "| 3 | 4 |\n", {header: false}); +var table = parseTable("| 1 | 2 |\n" + "| 3 | 4 |\n", {header: false, fields: ['Field1', 'Field2']}); +var table = parseTable(readTopic("ManyTables"), {target: 2}); +var table = parseTable({text: "| *A* | *B* |\n" + "| 1 | 2 |\n" + "| 3 | 4 |\n"}); +var table = parseTable(..., function (row, loop) {}); +var table = parseTable({text: "...", header: false, fields: [...], target: 2, callback: function (row, loop) {}}); +</verbatim> + +Parses a TWiki table notation. + +| *Argument Parameter* | *Default Value* | *Description* | +| =header= | =true= | Indicates the parsed table has a header row. | +| =fields= | (none) | Specifies an array of field names. If there is already a header row in the parsed table, this parameter will override the field names used in the return value. | +| =target= | =1= | Select a table by number (starting from =1=) out of multiple tables in the input text. | +| =callback= | (none) | Callback function per row. Each =row= argument can be an object of field/value pairs or a simple array, depending on the context. | + +The return value is an object containing =fields= and =rows= if the =fields= are detected from the input or specified as an argument. +If there are no available =fields=, the return value is simply an array of arrays. + +| *Return Value* | *Description* | +| =table.fields= | Array of field names | +| =table.rows= | Array of rows, each of which is an object of field/value pairs | + +By default, the first row is assumed to be a header, and the values are detected as =fields=. +If the =header= parameter is set to =false=, the first row is not parsed as a header. + +If the =fields= parameter is given, the fields used as the keys of each row object are overridden regardless of the first row. + +If a =callback= function is given, each =row= can be altered by returning an updated row object, or excluded by returning nothing (=undefined= or =null=). + +When the input text contains multiple tables, the first table is parsed by default. If the =target= parameter is given, the table at the specified number is detected (starting from =1=). + +All the values are converted by [[#unescapeTable][unescapeTable()]]. + +See also [[EJSPluginTableAPI#parseTable][Table API]] for example code. + +---++ formatTable +<verbatim> +var formatedText = formatTable(table); +var formatedText = formatTable(table, {header: false}); +var formatedText = formatTable(table, function (row, loop) {}); +var formatedText = formatTable({fields: [...], rows: [...], header: false, callback: function (row, loop) {}}); +</verbatim> + +Builds a TWiki table notation from an object representing a table. + +| *Argument Parameter* | *Default Value* | *Description* | +| =fields= | (none) | Array of field names | +| =rows= | (none) | Array of rows, each of which is an object of field/value pairs | +| =header= | =true= | Indicates the header row is prepended to the result table. | +| =callback= | (none) | Callback function per row. Each =row= argument can be an object of field/value pairs or a simple array, depending on the context. | + +The structure of the =table= parameter (first argument) roughly corresponds to the return value of [[#parseTable][parseTable()]], either an object with =fields= and =rows= or an array of arrays. + +By default, the first row is prepended as a header based on the =fields= values. +If the =header= parameter is set to =false= or if there are no =fields=, the header row is not prepended. + +If a =callback= function is given, each =row= can be altered by returning an updated row object, or excluded by returning nothing (=undefined= or =null=). + +All the values are converted by [[#escapeTable][escapeTable()]]. + +Because of how this API works, it can also be used to generate a single row of a table: + +<verbatim> +formatTable([['Foo', 'Bar', 'Baz']]); +// returns "| Foo | Bar | Baz |" + +formatTable({fields: ['Foo', 'Bar', 'Baz']}); +// returns "| *Foo* | *Bar* | *Baz* |" +</verbatim> + +See also [[EJSPluginTableAPI#formatTable][Table API]] for example code. + +---++ parseCSV +<verbatim> +var table = parseCSV("A,B,C\n1,2,3\n4,5,6"); +var table = parseCSV("1,2,3\n4,5,6", {header: false}); +var table = parseCSV("1,2,3\n4,5,6", {header: false, fields: ['Field1', 'Field2', 'Field3']}); +var table = parseCSV({text: "A,B,C\n1,2,3\n4,5,6"}); +var table = parseCSV(..., function (row, loop) {}); +var table = parseCSV({text: "...", header: false, fields: [...], callback: function (row, loop) {}}); +</verbatim> + +Parses a CSV format. The input parameters and the return value are similar to [[#parseTable][parseTable()]]. + +See also [[EJSPluginTableAPI#parseTable][Table API]] for example code. + +---++ formatCSV +<verbatim> +var formatedText = formatCSV(table); +var formatedText = formatCSV(table, {header: false}); +var formatedText = formatCSV(table, function (row, loop) {}); +var formatedText = formatCSV({fields: [...], rows: [...], header: false, callback: function (row, loop) {}}); +</verbatim> + +Builds a CSV text from an object representing a table. The input object and the parameters are similar to [[#formatTable][formatTable()]]. + +See also [[EJSPluginTableAPI#parseTable][Table API]] for example code. + +---++ getJavaScriptEngine +<verbatim> +var engineName = getJavaScriptEngine(); +</verbatim> + +Retrieves the underlying !JavaScript engine name, such as =JE= and =JavaScript::V8=. + +---++ getEJSConfig +<verbatim> +var config = getEJSConfig(); +</verbatim> + +Retrieves the [[EJSPlugin]] configuration under the current execution context. + +| *Return Value* | *Description* | +| =config.namespace= | Namespace for EJSPlugin API functions. Default: =undefined= | +| =config.postMethodPolicy= | POST method policy | +| =config.saveWebPolicy= | Same-web policy | +| =config.timeout= | Timeout in seconds | + +The context may be affected by the system configurations (=$TWiki::cfg=), the preference variables (such as =EJS_NAMESPACE=), and the parameters passed to =%<nop>EJS_INCLUDE{...}%=. + +---++ console +<verbatim> +console.log("message"); +console.debug("message"); +console.info("message"); +console.warn("message"); +console.error("message"); +</verbatim> + +These methods are equivalent to invoking the following code, where the outputs are inserted into the browser-side console. + +<verbatim> +<script>console.log(...);</script> +</verbatim> + +The argument is serialized by =JSON.stringify()=, so non-string objects can also be examined in the console. + +---++ JSON +<verbatim> +var object = JSON.parse("{\"key\": \"value\"}"); +var jsonText = JSON.stringify({key: "value"}); +</verbatim> + +Converts a JSON string into a !JavaScript value, and vice versa. Added: twiki/trunk/EJSPlugin/data/TWiki/EJSPluginTableAPI.txt =================================================================== --- twiki/trunk/EJSPlugin/data/TWiki/EJSPluginTableAPI.txt (rev 0) +++ twiki/trunk/EJSPlugin/data/TWiki/EJSPluginTableAPI.txt 2017-07-21 08:59:17 UTC (rev 30358) @@ -0,0 +1,190 @@ +%META:TOPICINFO{author="TWikiContributor" date="1495793747" format="1.1" version="$Rev$"}% +---+!! !EJSPlugin Table API + +This page describes the usage of EJS Table API of [[EJSPlugin]]. See also [[EJSPluginAPI][EJS API List]]. + +<!-- + * Set EJS = off +--> + +%TOC% + +---++ parseTable + +<verbatim> +parseTable( + "| *A* | *B* |\n" + + "| 1 | 2 |\n" + + "| 3 | 4 |\n" +); +/* +Returns: { + fields: ['A', 'B'], + rows: [ + {A: 1, B: 2}, + {A: 3, B: 4} + ], +} +*/ +</verbatim> + +<verbatim> +parseTable( + "| 1 | 2 |\n" + + "| 3 | 4 |\n", + {header: false} +); +/* +Returns: [ + [1, 2], + [3, 4] +] +*/ +</verbatim> + +<verbatim> +parseTable( + "| *A* | *B* |\n" + + "| 1 | 2 |\n" + + "| 3 | 4 |\n", + {header: true, fields: ['Foo', 'Bar']} +); +/* +Returns: { + fields: ['Foo', 'Bar'], + rows: [ + {Foo: 1, Bar: 2}, + {Foo: 3, Bar: 4} + ], +} +*/ +</verbatim> + +<verbatim> +parseTable( + "| 1 | 2 |\n" + + "| 3 | 4 |\n", + {header: false, fields: ['Foo', 'Bar']} +); +/* +Returns: { + fields: ['Foo', 'Bar'], + rows: [ + {Foo: 1, Bar: 2}, + {Foo: 3, Bar: 4} + ], +} +*/ +</verbatim> + +<verbatim> +parseTable( + "| *A* | *B* |\n" + + "| 1 | 2 |\n" + + "| 3 | 4 |\n", + {fields: ['Foo', 'Bar']} + function (row) { + println(formatText("---++ Foo: $Foo", row)); + println(formatText("Bar: $Bar", row)); + } +); +/* +Prints: +---++ Foo: 1 +Bar: 2 +---++ Foo: 3 +Bar: 4 +*/ +</verbatim> + +---++ formatTable + +<verbatim> +formatTable({ + fields: ['A', 'B'], + rows: [ + {A: 1, B: 2}, + {A: 3, B: 4} + ] +}); +/* +Returns: +| *A* | *B* | +| 1 | 2 | +| 3 | 4 | +*/ +</verbatim> + +<verbatim> +formatTable({ + fields: ['A', 'B'], + rows: [ + {A: 1, B: 2}, + {A: 3, B: 4} + ] +}, {header: false}); +/* +Returns: +| 1 | 2 | +| 3 | 4 | +*/ +</verbatim> + +<verbatim> +formatTable([ + [1, 2], + [3, 4] +]); +/* +Returns: +| 1 | 2 | +| 3 | 4 | +*/ +</verbatim> + +<verbatim> +formatTable({ + fields: ['Foo', 'Bar'], + rows: [ + {Foo: 1, Bar: 2}, + {Foo: 3, Bar: 4} + ] +}, function (row) { + row.Bar = '%RED%' + row.Bar + '%ENDCOLOR%'; + return row; +}); +/* +Returns: +| *Foo* | *Bar* | +| 1 | %RED%2%ENDCOLOR% | +| 3 | %RED%4%ENDCOLOR% | +*/ +</verbatim> + +---++ Special Characters + +The EJSPlugin Table API treats the following character sequences in the special ways: + +| *Sequence* | *Descrpition* | +| %<nop>VBAR% | The vertical bar (<code>%VBAR%</code>) | +| \0 | Null byte | +| \n | Line feed | +| \r | Carriage return | +| \t | Tab | +| \b | Backspace | +| \f | Form feed | +| \v | Vertical tab | +| \\ | Backslash literal (<code>\</code>) | + +When formatting and parsing a table, it escapes and unescapes these sequences in addition to HTML entities. (=escapeTable()= and =unescapeTable()=) + +<verbatim> +// Topic name: TestTable +| <%VBAR%gt;\n | +<!-- Note: \n is encoded as a backslash followed by n --> + +// EJS Code +var table = parseTable(readTopic("TestTable"), {header: false}); +table[0][0] == "<|>\n"; // Note: \n is the actual line break +formatTable(table); // returns the same text as TestTable +</verbatim> Added: twiki/trunk/EJSPlugin/data/TWiki/VarEJS_INCLUDE.txt =================================================================== --- twiki/trunk/EJSPlugin/data/TWiki/VarEJS_INCLUDE.txt (rev 0) +++ twiki/trunk/EJSPlugin/data/TWiki/VarEJS_INCLUDE.txt 2017-07-21 08:59:17 UTC (rev 30358) @@ -0,0 +1,9 @@ +%META:TOPICINFO{author="TWikiContributor" date="1495793747" format="1.1" version="$Rev$"}% +%META:TOPICPARENT{name="TWikiVariables"}% +#VarEJS_INCLUDE +---+++ EJS_INCLUDE -- execute and include topic containing EJS tags + * See [[%SYSTEMWEB%.EJSPlugin]] for information about EJS (Embedded !JavaScript) template. + * This tag is used to embed an EJS-based component topic. The !JavaScript execution context (namespace) within this %<nop>EJS_INCLUDE{...}% tag is completely separate from the outside, even if the "including" topic is also EJS-enabled. + * Syntax: =%<nop>EJS_INCLUDE{"ComponentTopicName" namespace="NamespaceRequiredByComponent" timeout="TimeoutInSeconds"}%= + * Category: ApplicationsAndComponentsVariables, DevelopmentVariables, ImportVariables + * Related: [[%IF{"'%INCLUDINGTOPIC%'='TWikiVariables'" then="#"}%VarINCLUDE][INCLUDE]] Added: twiki/trunk/EJSPlugin/lib/TWiki/Plugins/EJSPlugin/Config.spec =================================================================== --- twiki/trunk/EJSPlugin/lib/TWiki/Plugins/EJSPlugin/Config.spec (rev 0) +++ twiki/trunk/EJSPlugin/lib/TWiki/Plugins/EJSPlugin/Config.spec 2017-07-21 08:59:17 UTC (rev 30358) @@ -0,0 +1,30 @@ +# ---+ Extensions +# ---++ EJSPlugin +# **BOOLEAN** +# Set this option to 1 to execute EJS code within <%...%> by default, without requiring the =EJS= preference setting +$TWiki::cfg{Plugins}{EJSPlugin}{DefaultExecute} = 0; +# **BOOLEAN** +# Set this option to 1 to enable EJS Dynamic Template by default +$TWiki::cfg{Plugins}{EJSPlugin}{DefaultDynamicTemplate} = 0; +# **STRING** +# Optionally set a default !JavaScript namespace for API functions +$TWiki::cfg{Plugins}{EJSPlugin}{DefaultNamespace} = ''; +# **BOOLEAN** +# Set this option to 0 to disable POST Method Policy by default +$TWiki::cfg{Plugins}{EJSPlugin}{DefaultPostMethodPolicy} = 1; +# **BOOLEAN** +# Set this option to 0 to disable Same-Web Policy by default +$TWiki::cfg{Plugins}{EJSPlugin}{DefaultSameWebPolicy} = 1; +# **STRING** +# Optionally set a Perl module name of !JavaScript engine +$TWiki::cfg{Plugins}{EJSPlugin}{JavaScriptEngine} = ''; +# **NUMBER** +# Set a default timeout in seconds for !JavaScript execution +$TWiki::cfg{Plugins}{EJSPlugin}{DefaultTimeout} = 5; +# **NUMBER** +# Set a maximum timeout in seconds that can be overridden by preference (Set 0 to indicate unlimited) +$TWiki::cfg{Plugins}{EJSPlugin}{MaxTimeout} = 180; +# **STRING** +# Set a default template web name used for [[EJSPluginAPI#createWeb][createWeb()]] API +$TWiki::cfg{Plugins}{EJSPlugin}{DefaultBaseWeb} = '_default'; +1; Added: twiki/trunk/EJSPlugin/lib/TWiki/Plugins/EJSPlugin/DEPENDENCIES =================================================================== --- twiki/trunk/EJSPlugin/lib/TWiki/Plugins/EJSPlugin/DEPENDENCIES (rev 0) +++ twiki/trunk/EJSPlugin/lib/TWiki/Plugins/EJSPlugin/DEPENDENCIES 2017-07-21 08:59:17 UTC (rev 30358) @@ -0,0 +1,9 @@ +TWiki::Plugins,>=1.1,perl,TWiki Dakar release. +EJS::Template,>=0.08,cpan,Required for EJS parsing and execution +JE,>=0.066,cpan,Either CPAN:JE or CPAN:JavaScript::V8 is required +JavaScript::V8,>=0.07,cpan,Either CPAN:JE or CPAN:JavaScript::V8 is required +HTML::Entities,>=3.72,cpan,Required +IO::String,>=1.08,cpan,Required +JSON,>=2.94,cpan,Required +Scalar::Util,>=1.48,cpan,Required +Text::CSV,>=1.95,cpan,Required Added: twiki/trunk/EJSPlugin/lib/TWiki/Plugins/EJSPlugin/Func.pm =================================================================== --- twiki/trunk/EJSPlugin/lib/TWiki/Plugins/EJSPlugin/Func.pm (rev 0) +++ twiki/trunk/EJSPlugin/lib/TWiki/Plugins/EJSPlugin/Func.pm 2017-07-21 08:59:17 UTC (rev 30358) @@ -0,0 +1,2047 @@ +use strict; +use warnings; + +package TWiki::Plugins::EJSPlugin::Func; +use base 'Exporter'; + +require TWiki::Func; + +use EJS::Template; +use EJS::Template::Util; +use File::Basename; +use HTML::Entities; +use IO::String; +use JSON; +use Scalar::Util qw(looks_like_number refaddr); +use Text::CSV; + +our @EJS_ROOT_FUNCTIONS = qw( + print + println +); + +our @EJS_FUNCTIONS = qw( + requireTopic + findWebs + findTopics + webExists + topicExists + createWeb + moveWeb + removeWeb + readTopic + saveTopic + moveTopic + removeTopic + getFormName + setFormName + getFormField + setFormField + getFormFields + checkPermission + getRevisionInfo + getRevisionAtTime + getEditLock + acquireEditLock + releaseEditLock + getAttachmentList + attachmentExists + readAttachment + saveAttachment + moveAttachment + removeAttachment + getMethod + isPost + getParam + setParam + getUserName + getWikiName + getWikiUserName + getVariable + setVariable + expandVariables + expandAutoInc + formatText + isTrue + normalizeWebTopicName + escapeHTML + unescapeHTML + escapeQuote + unescapeQuote + escapeTable + unescapeTable + parseTable + formatTable + parseCSV + formatCSV + getJavaScriptEngine + getEJSConfig +); + +our @EXPORT_OK = @EJS_FUNCTIONS; +our %EXPORT_TAGS = (all => \@EXPORT_OK); + +our $CONTEXT = {}; +my $alreadyRequiredTopics; +our $currentlyRequiredWeb; +our $currentlyRequiredTopic; +our $overrideDefaultWeb; +our $overrideDefaultTopic; + +my $alarmAlreadySet = 0; + +sub _executeEJS { + my ($textRef, $web, $topic, $meta, $ejsConfig) = @_; + + # If there are no EJS tags at all, do nothing. + if ($$textRef !~ /<%/) { + return; + } + + eval { + my $ejs = EJS::Template->new(engine => $ejsConfig->{jsEngine}); + my $adapter = $ejs->executor->adapter; + my $engine = $adapter->engine; + + local $CONTEXT = { + adapter => $adapter, + engine => $engine, + config => $ejsConfig, + web => $web, + topic => $topic, + meta => $meta, + requiredTopics => {}, + }; + + my $variables = _initializeVariables(); + + my $timeout = $ejsConfig->{timeout}; + local $SIG{ALRM} = sub {die "EJS took too long (timeout: $timeout sec.)\n"}; + + my $newAlarm = 0; + + if (!$alarmAlreadySet) { + $alarmAlreadySet = 1; + $newAlarm = 1; + alarm $timeout; + } + + EJS::Template->process($textRef, $variables, $textRef); + + if ($newAlarm) { + alarm 0; + $alarmAlreadySet = 0; + } + }; + + if ($@) { + $$textRef .= "\n\n".'%RED%'."EJS: $@".'%ENDCOLOR%'; + } +} + +sub _initializeVariables { + my ($class) = @_; + my $isJE = $CONTEXT->{engine}->isa('JE'); + + my $variables = { + JSON => { + stringify => _wrapFunction(\&TWiki::Plugins::EJSPlugin::Func::JSON_stringify, $isJE), + parse => _wrapFunction(\&TWiki::Plugins::EJSPlugin::Func::JSON_parse, $isJE), + }, + console => { + log => _wrapFunction(\&TWiki::Plugins::EJSPlugin::Func::console_log, $isJE), + debug => _wrapFunction(\&TWiki::Plugins::EJSPlugin::Func::console_debug, $isJE), + info => _wrapFunction(\&TWiki::Plugins::EJSPlugin::Func::console_info, $isJE), + warn => _wrapFunction(\&TWiki::Plugins::EJSPlugin::Func::console_warn, $isJE), + error => _wrapFunction(\&TWiki::Plugins::EJSPlugin::Func::console_error, $isJE), + }, + }; + + my $namespace = _makeNamespace($variables, $CONTEXT->{config}{namespace}); + + no strict 'refs'; + for my $name (@EJS_ROOT_FUNCTIONS) { + $variables->{$name} = _wrapFunction(\&{'TWiki::Plugins::EJSPlugin::Func::'.$name}, $isJE); + } + for my $name (@EJS_FUNCTIONS) { + $namespace->{$name} = _wrapFunction(\&{'TWiki::Plugins::EJSPlugin::Func::'.$name}, $isJE); + } + use strict 'refs'; + + $alreadyRequiredTopics = {}; + + return $variables; +} + +sub _makeNamespace { + my ($root, $spec) = @_; + return $root unless defined $spec; + + my $namespace = $root; + $spec =~ s/^\s+|\s+$//g; + + for my $name (split /\s*\.\s*/, $spec) { + if ($name =~ /^[A-Za-z_\$][A-Za-z_\$0-9]*$/) { + $namespace = ($namespace->{$name} ||= {}); + + if (ref($namespace) ne 'HASH') { + die "Invalid namespace (non-object): $spec\n"; + } + } else { + die "Invalid namespace (unknown characters): $spec\n"; + } + } + + return $namespace; +} + +sub _wrapFunction { + my ($func, $isJE) = @_; + + if ($isJE) { + return sub { + my $ret = eval {$func->(map {_je2perl($_)} @_)}; + die JE::Object::Error->new($CONTEXT->{engine}->global(), _perl2je($@)) if $@; + return _perl2je($ret); + }; + } else { + return sub { + my $ret = $func->(@_); + return _sanitizePerl($ret); + }; + } +} + +sub _sanitizePerl { + my ($value, $seen) = @_; + my $ref = ref $value; + my $addr = refaddr $value; + $seen ||= {}; + + if (!defined $value) { + return undef; + } elsif ($ref) { + if ($ref eq 'HASH') { + return $seen->{$addr} ||= {map {$_ => _sanitizePerl($value->{$_}, $seen)} keys %$value}; + } elsif ($ref eq 'ARRAY') { + return $seen->{$addr} ||= [map {_sanitizePerl($_, $seen)} @$value]; + } elsif ($ref eq 'CODE') { + return $seen->{$addr} ||= sub { + my $ret = $value->(@_); + return _sanitizePerl($ret); + }; + } else { + return undef; + } + } else { + my $class = ref $CONTEXT->{adapter}; + + no strict 'refs'; + my $encode_utf8 = ${$class.'::ENCODE_UTF8'}; + my $sanitize_utf8 = ${$class.'::SANITIZE_UTF8'}; + my $force_untaint = ${$class.'::FORCE_UNTAINT'}; + use strict 'refs'; + + my $newRef = EJS::Template::Util::clean_text_ref(\$value, + $encode_utf8, $sanitize_utf8, $force_untaint); + + return $$newRef; + } +} + +sub _perl2je { + my ($value, $seen) = @_; + my $engine = $CONTEXT->{engine}; + my $ref = ref $value; + my $addr = refaddr $value; + $seen ||= {}; + + if (!defined $value) { + return $engine->undefined(); + } elsif ($ref) { + if ($ref eq 'HASH') { + return $seen->{$addr} ||= $engine->upgrade({map {$_ => _perl2je($value->{$_}, $seen)} keys %$value}); + } elsif ($ref eq 'ARRAY') { + return $seen->{$addr} ||= $engine->upgrade([map {_perl2je($_, $seen)} @$value]); + } elsif ($ref eq 'CODE') { + return $seen->{$addr} ||= $engine->upgrade(sub { + my $ret = $value->(map {_je2perl($_)} @_); + return _perl2je($ret); + }); + } else { + # TODO: Throw an exception? + return $engine->undefined(); + } + } elsif (looks_like_number($value)) { + return JE::Number->new($engine, $value); + } else { + return $engine->upgrade($value); + } +} + +sub _je2perl { + my ($jeObj, $seen) = @_; + my $ref = ref $jeObj; + my $addr = refaddr $jeObj; + $seen ||= {}; + + if ($ref =~ /^JE::/) { + if ($ref =~ /^JE::(?:Object::)?(?:Null|Undefined)$/) { + return undef; + } elsif ($ref =~ /^JE::(?:Object::)?(?:String|Number|Boolean|Date|RegExp)$/) { + return $jeObj->value; + } elsif ($ref =~ /^JE::Object::Function(?:$|::)/) { + return $seen->{$addr} ||= sub { + my $jeRet = $jeObj->(map {_perl2je($_)} @_); + return _je2perl($jeRet); + }; + } elsif ($ref =~ /^JE::Object::Array(?:$|::)/) { + return $seen->{$addr} ||= [map {_je2perl($_, $seen)} @$jeObj]; + } elsif ($ref =~ /^JE::Object(?:$|::)/) { + return $seen->{$addr} ||= {map {$_ => _je2perl($jeObj->{$_}, $seen)} keys %$jeObj}; + } else { + return eval {$jeObj->value}; + } + } + + return $jeObj; +} + +sub _applyCallback { + my ($callback, $values, $param) = @_; + my $loop = {}; + my $results = []; + + for my $index (0..$#$values) { + my $value = $values->[$index]; + + $loop->{first} = ($index == 0 ? 1 : 0); + $loop->{last} = ($index == $#$values ? 1 : 0); + $loop->{index} = $index; + $loop->{value} = $value; + + my $result = $callback->($value, $loop); + push @$results, $result if defined $result; + } + + return $results; +} + +sub _flattenParam { + my ($args) = @_; + + my $param = {}; + my $positional = []; + my $traverse; + + $traverse = sub { + my ($args) = @_; + + for my $arg (@$args) { + my $ref = ref $arg; + + if (!$ref) { + push @$positional, $arg; + } elsif ($ref eq 'HASH') { + for my $key (keys %$arg) { + $param->{$key} = $arg->{$key}; + } + } elsif ($ref eq 'ARRAY') { + # Treat an array as a positional parameter + push @$positional, $arg; + } elsif ($ref eq 'CODE') { + $param->{callback} = $arg; + } + } + }; + + $traverse->($args); + return ($param, $positional); +} + +sub _parseParam { + my ($args, $fields) = @_; + my ($param, $positional) = _flattenParam($args); + + # Scan specification ($fields) + my $missingInParam = []; + my $optionalCount = 0; + my $isSpecified = {}; + my $isOptional = {}; + my $isWildcard = {}; + + for my $rawField (@$fields) { + my $field = $rawField; + my $optional = ($field =~ s/\?$//); + my $wildcard = ($field =~ s/\*$//); + $optional = 1 if $wildcard; + + if (!exists $param->{$field}) { + push @$missingInParam, [\$field, $optional]; + $optionalCount++ if $optional; + } + + $isSpecified->{$field} = 1; + $isOptional->{$field} = 1 if $optional; + $isWildcard->{$field} = 1 if $wildcard; + } + + # Assign scalar params + my $min = @$missingInParam - $optionalCount; + my $max = @$missingInParam; + + if (@$positional < $min) { + (my $func = (caller 1)[3]) =~ s/.*://; + die "Insufficient parameters: $func(".join(', ', @$fields).")\n"; + } elsif (@$positional > $max) { + (my $func = (caller 1)[3]) =~ s/.*://; + die "Too many parameters: $func(".join(', ', @$fields).")\n"; + } + + my $optionalToIgnore = @$missingInParam - @$positional; + my $mappedFields = []; + + for my $pair (@$missingInParam) { + if ($optionalToIgnore > 0 && $pair->[1]) { + $optionalToIgnore--; + } else { + push @$mappedFields, $pair->[0]; + } + } + + for my $i (0..$#$positional) { + my $fieldRef = $mappedFields->[$i]; + $param->{$$fieldRef} = $positional->[$i]; + } + + # Expand variables + for my $key (qw(web topic toWeb toTopic baseWeb user)) { + my $value = $param->{$key}; + + if (defined $value && $value =~ /%/) { + $TWiki::Plugins::SESSION->expandAllTags(\$param->{$key}, + $CONTEXT->{topic}, $CONTEXT->{web}, $CONTEXT->{meta}); + } + } + + # Normalize Web.Topic + for my $keys (['web', 'topic'], ['toWeb', 'toTopic']) { + my ($webKey, $topicKey) = @$keys; + + if ($isSpecified->{$webKey} || $isSpecified->{$topicKey} || + defined $param->{$webKey} || defined $param->{$topicKey}) { + my ($web, $topic) = ($param->{$webKey}, $param->{$topicKey}); + my ($origWeb, $origTopic) = ($web, $topic); + my ($webEmpty, $topicEmpty); + + if (!defined $web || $web eq '') { + $web = $CONTEXT->{web} || $TWiki::Plugins::SESSION->{webName}; + $webEmpty = 1; + } + + if (!defined $topic || $topic eq '') { + $topic = $CONTEXT->{topic} || $TWiki::Plugins::SESSION->{topicName}; + $topicEmpty = 1; + } else { + if ($topic =~ m{[./]}) { + $webEmpty = 0; + } + } + + ($web, $topic) = TWiki::Func::normalizeWebTopicName($web, $topic); + + if ($isWildcard->{$webKey} || $isWildcard->{$topicKey} || + (!$isSpecified->{$webKey} && defined $param->{$webKey}) || + (!$isSpecified->{$topicKey} && defined $param->{$topicKey})) { + $topic = $origTopic if $topicEmpty; + $web = $origWeb if $webEmpty; + } + + ($param->{$webKey}, $param->{$topicKey}) = ($web, $topic); + } + } + + return $param; +} + +sub _attrValue { + my ($text) = @_; + $text =~ s/"/\\"/g; + return '"'.$text.'"'; +} + +sub _wildcard2regex { + my ($wildcard) = @_; + my $regex = quotemeta $wildcard; + $regex =~ s/(?<!\\\\)\\\*/.*/g; + return qr/^$regex$/; +} + +sub _verifyChangePolicy { + my ($action, $webs) = @_; + my $session = $TWiki::Plugins::SESSION; + my $query = $session->{request}; + + # Check for POST policy + if ($CONTEXT->{config}{postMethodPolicy}) { + if ($query->request_method() ne 'POST') { + die "POST method is required for action '$action'\n"; + } + } + + # Check for Same-Web policy + if ($CONTEXT->{config}{sameWebPolicy}) { + my $baseWeb = $session->{SESSION_TAGS}{BASEWEB}; + my $quoted = quotemeta $baseWeb; + my $regex = qr{^$quoted(?:$|[/\.])}; + + for my $web (@{$webs || []}) { + if ($web !~ $regex) { + die "'$web' is outside the current web and cannot be modified\n"; + } + } + } + + # Check for CryptToken + if ($TWiki::cfg{CryptToken}{Enable} && $CONTEXT->{config}{secureActions}{lc $action}) { + eval {TWiki::UI::verifyCryptToken($session, $query->param('crypttoken'))}; + die "Invalid crypttoken\n" if $@; + } +} + +sub _getTopic { + my ($web, $topic, $rev, $permType) = @_; + + # If the topic does not exist, TWiki::Func::readTopic() returns an empty meta + my ($meta) = TWiki::Func::readTopic($web, $topic, $rev); + + my $allowed = TWiki::Func::checkAccessPermission($permType, + TWiki::Func::getWikiName(), $meta->{_text}, $topic, $web, $meta); + + if (!$allowed) { + die "$permType access denied: $web.$topic\n"; + } + + return $meta; +} + +sub _parentWeb { + my ($web) = @_; + return defined $web && $web =~ s/[\/\.][^\/\.]+$// ? $web : undef; +} + +sub requireTopic { + my $param = _parseParam(\@_, ['topic']); + my $web = $param->{web}; + my $topic = $param->{topic}; + + if (exists $CONTEXT->{requiredTopics}{"$web.$topic"}) { + return 0; + } else { + $CONTEXT->{requiredTopics}{"$web.$topic"} = 1; + } + + if (!TWiki::Func::topicExists($web, $topic)) { + die "Topic '$web.$topic' does not exist\n"; + } + + my $meta = _getTopic($web, $topic, $param->{rev}, 'VIEW'); + + local $CONTEXT->{web} = $web; + local $CONTEXT->{topic} = $topic; + local $CONTEXT->{meta} = $meta; + + # Workaround for EJS bug where Executor will unexpectedly overwrite print() and EJS object. + my $variables = {}; + my $isJE = $CONTEXT->{engine}->isa('JE'); + + no strict 'refs'; + for my $name (@EJS_ROOT_FUNCTIONS) { + $variables->{$name} = _wrapFunction(\&{'TWiki::Plugins::EJSPlugin::Func::'.$name}, $isJE); + } + use strict 'refs'; + + EJS::Template->context->process(\$meta->{_text}, $variables); + + return 1; +} + +sub _print { + EJS::Template->context->print(map {ref($_) ? JSON_stringify($_) : $_} @_); +} + +sub print { + _print(@_); + return undef; +} + +sub println { + _print(@_, "\n"); + return undef; +} + +sub findWebs { + my $param = _parseParam(\@_, ['web*']); + my $web = $param->{web}; + + my $filter = $param->{filter}; + my @webs; + + if (!defined $web || $web eq '' || $web eq '*') { + @webs = $TWiki::Plugins::SESSION->{store}->getListOfWebs($filter, undef, 1); + } else { + for my $chunk (split m{/}, $web) { + my @subwebs; + + if ($chunk =~ /\*/) { + my $regex = _wildcard2regex($chunk); + + if (@webs) { + @subwebs = map {$TWiki::Plugins::SESSION->{store}->getListOfWebs($filter, $_, 1)} @webs; + } else { + @subwebs = $TWiki::Plugins::SESSION->{store}->getListOfWebs($filter, undef, 1); + } + + @subwebs = grep {basename($_) =~ $regex} @subwebs; + } else { + if (@webs) { + @subwebs = grep {TWiki::Func::webExists($_)} map {"$_/$chunk"} @webs; + } else { + @subwebs = ($chunk) if TWiki::Func::webExists($chunk); + } + } + + @webs = @subwebs; + last unless @webs; + } + } + + if (my $callback = $param->{callback}) { + return _applyCallback($callback, \@webs, $param); + } else { + return \@webs; + } +} + +sub findTopics { + my $param = _parseParam(\@_, ['topic*']); + my $topic = $param->{topic}; + + my @webs; + my $webSpecified = defined $param->{web}; + + if ($webSpecified) { + if ($param->{web} =~ /\*/) { + @webs = @{findWebs($param->{web}, {filter => $param->{filter}})}; + } elsif (TWiki::Func::webExists($param->{web})) { + @webs = ($param->{web}); + } + } else { + @webs = ($TWiki::Plugins::SESSION->{webName}); + } + + my @topics; + + for my $web (@webs) { + my @foundTopics; + + if (!defined $topic || $topic eq '' || $topic eq '*') { + @foundTopics = TWiki::Func::getTopicList($web); + } elsif ($topic =~ /\*/) { + my $regex = _wildcard2regex($topic); + my @found = TWiki::Func::getTopicList($web); + @foundTopics = grep {$_ =~ $regex} @found; + } else { + if (TWiki::Func::topicExists($web, $topic)) { + @foundTopics = ($topic); + } + } + + if ($webSpecified) { + @foundTopics = map {"$web.$_"} @foundTopics; + } + + push @topics, @foundTopics; + } + + if (my $callback = $param->{callback}) { + my $ret = _applyCallback($callback, \@topics, $param); + return $ret; + } else { + return \@topics; + } +} + +sub webExists { + my $param = _parseParam(\@_, ['web']); + my $web = $param->{web}; + return TWiki::Func::webExists($web) ? 1 : 0; +} + +sub topicExists { + my $param = _parseParam(\@_, ['topic']); + my $web = $param->{web}; + my $topic = $param->{topic}; + return TWiki::Func::topicExists($web, $topic) ? 1 : 0; +} + +sub createWeb { + my $param = _parseParam(\@_, ['web']); + _verifyChangePolicy('createweb', [$param->{web}]); + + my $web = $param->{web}; + my $baseWeb = $param->{baseWeb}; + + unless (defined $baseWeb) { + $baseWeb = $CONTEXT->{config}{defaultBaseWeb}; + } + + my $session = $TWiki::Plugins::SESSION; + my $cUID = $session->{user}; + my $parentWeb = _parentWeb($web); + + # Check metadata + if ($TWiki::cfg{Mdrepo}{WebRecordRequired} && $session->{mdrepo} && + !$session->{mdrepo}->getRec('webs', TWiki::topLevelWeb($web))) { + die "No metadata exists for web '$web'\n"; + } + + # Check existence + if (TWiki::Func::webExists($web)) { + die "Web '$web' already exists\n"; + } elsif (TWiki::Func::topicExists(undef, $web)) { + die "Topic '$web' already exists\n"; + } elsif ($parentWeb && !TWiki::Func::webExists($parentWeb)) { + die "Web '$parentWeb' does not exist\n"; + } elsif (!TWiki::Func::webExists($baseWeb)) { + die "Base web '$baseWeb' does not exist\n"; + } + + # Check permission + if ($parentWeb) { + if (!$session->{users}->canCreateWeb($parentWeb)) { + if (!TWiki::Func::checkAccessPermission('CHANGE', + TWiki::Func::getWikiName(), undef, undef, $parentWeb, undef)) { + die "CREATEWEB access denied: $parentWeb\n"; + } + } + } + + # Execute changes + $session->{store}->createWeb($cUID, $web, $baseWeb, undef); + + return 1; +} + +sub moveWeb { + my $param = _parseParam(\@_, ['web', 'toWeb']); + _verifyChangePolicy('renameweb', [$param->{web}, $param->{toWeb}]); + + my $fromWeb = $param->{web}; + my $toWeb = $param->{toWeb}; + + my $session = $TWiki::Plugins::SESSION; + my $cUID = $session->{user}; + my $parentWeb = _parentWeb($toWeb); + + # Check metadata + if ($TWiki::cfg{Mdrepo}{WebRecordRequired} && $session->{mdrepo} && + !$session->{mdrepo}->getRec('webs', TWiki::topLevelWeb($toWeb))) { + die "No metadata exists for web '$toWeb'\n"; + } + + # Check existence + if (!TWiki::Func::webExists($fromWeb)) { + die "Web '$fromWeb' does not exist\n"; + } + + if (TWiki::Func::webExists($toWeb)) { + die "Web '$toWeb' already exists\n"; + } elsif (TWiki::Func::topicExists(undef, $toWeb)) { + die "Topic '$toWeb' already exists\n"; + } elsif ($parentWeb && !TWiki::Func::webExists($parentWeb)) { + die "Web '$parentWeb' does not exist\n"; + } + + # Check permission + if (!$session->{users}->canRenameWeb($fromWeb, $toWeb)) { + if (!TWiki::Func::checkAccessPermission('CHANGE', + TWiki::Func::getWikiName(), undef, undef, $fromWeb, undef)) { + die "RENAMEWEB access denied: $fromWeb, $toWeb\n"; + } + + if ($parentWeb) { + if (!TWiki::Func::checkAccessPermission('CHANGE', + TWiki::Func::getWikiName(), undef, undef, $parentWeb, undef)) { + die "CHANGE access denied: $parentWeb\n"; + } + } + } + + # TODO: Run something similar to TWiki::UI::Manage::_updateWebReferringTopics? + + # Execute changes + $session->{store}->moveWeb($fromWeb, $toWeb, $cUID); + + return 1; +} + +sub removeWeb { + my $param = _parseParam(\@_, ['web']); + _verifyChangePolicy('renameweb', [$param->{web}]); + + my $fromWeb = $param->{web}; + my $toWeb = $param->{toWeb}; + + my $session = $TWiki::Plugins::SESSION; + my $cUID = $session->{user}; + my $trashWeb = $session->trashWebName(web => $fromWeb); + my $parentWeb = _parentWeb($toWeb); + + if (defined $toWeb) { + if (substr($toWeb, 0, length $trashWeb + 1) ne "$trashWeb/") { + die "Web '$toWeb' must be under '$trashWeb'\n"; + } + } else { + (my $name = $fromWeb) =~ s/[\/\.]//g; + $toWeb .= "$trashWeb/$name"; + + my $base = $toWeb; + my $next = 1; + + while (TWiki::Func::webExists($toWeb) || TWiki::Func::topicExists(undef, $toWeb)) { + $toWeb = $base.$next; + $next++; + } + } + + # Check existence + if (!TWiki::Func::webExists($fromWeb)) { + die "Web '$fromWeb' does not exist\n"; + } + + if (TWiki::Func::webExists($toWeb)) { + die "Web '$toWeb' already exists\n"; + } elsif (TWiki::Func::topicExists(undef, $toWeb)) { + die "Topic '$toWeb' already exists\n"; + } elsif ($parentWeb && !TWiki::Func::webExists($parentWeb)) { + die "Web '$parentWeb' does not exist\n"; + } + + # Check permission + if (!$session->{users}->canRenameWeb($fromWeb, $toWeb)) { + if (!TWiki::Func::checkAccessPermission('CHANGE', + TWiki::Func::getWikiName(), undef, undef, $fromWeb, undef)) { + die "RENAMEWEB access denied: $fromWeb, $toWeb\n"; + } + + if ($parentWeb) { + if (!TWiki::Func::checkAccessPermission('CHANGE', + TWiki::Func::getWikiName(), undef, undef, $parentWeb, undef)) { + die "CHANGE access denied: $parentWeb\n"; + } + } + } + + # Execute changes + $session->{store}->moveWeb($fromWeb, $toWeb, $cUID); + + return 1; +} + +sub readTopic { + my $param = _parseParam(\@_, ['topic']); + my $web = $param->{web}; + my $topi... [truncated message content] |