diff --git a/core/modules/startup/rootwidget.js b/core/modules/startup/rootwidget.js index a8ad5f8c6..4761fef85 100644 --- a/core/modules/startup/rootwidget.js +++ b/core/modules/startup/rootwidget.js @@ -20,6 +20,11 @@ exports.before = ["story"]; exports.synchronous = true; exports.startup = function() { + // Install the HTTP client event handler + $tw.httpClient = new $tw.utils.HttpClient(); + $tw.rootWidget.addEventListener("tm-http-request",function(event) { + $tw.httpClient.handleHttpRequest(event); + }); // Install the modal message mechanism $tw.modal = new $tw.utils.Modal($tw.wiki); $tw.rootWidget.addEventListener("tm-modal",function(event) { diff --git a/core/modules/utils/dom/http.js b/core/modules/utils/dom/http.js index 6e07b1040..797419b81 100644 --- a/core/modules/utils/dom/http.js +++ b/core/modules/utils/dom/http.js @@ -3,7 +3,7 @@ title: $:/core/modules/utils/dom/http.js type: application/javascript module-type: utils -Browser HTTP support +HTTP support \*/ (function(){ @@ -13,11 +13,127 @@ Browser HTTP support "use strict"; /* -A quick and dirty HTTP function; to be refactored later. Options are: +Manage tm-http-request events. Options are: +wiki - the wiki object to use +*/ +function HttpClient(options) { + options = options || {}; +} + +HttpClient.prototype.handleHttpRequest = function(event) { + console.log("Initiating an HTTP request",event) + var self = this, + wiki = event.widget.wiki, + paramObject = event.paramObject || {}, + url = paramObject.url, + completionActions = paramObject.oncompletion || "", + progressActions = paramObject.onprogress || "", + bindStatus = paramObject["bind-status"], + bindProgress = paramObject["bind-progress"], + method = paramObject.method || "GET", + HEADER_PARAMETER_PREFIX = "header-", + QUERY_PARAMETER_PREFIX = "query-", + PASSWORD_HEADER_PARAMETER_PREFIX = "password-header-", + PASSWORD_QUERY_PARAMETER_PREFIX = "password-query-", + CONTEXT_VARIABLE_PARAMETER_PREFIX = "var-", + requestHeaders = {}, + contextVariables = {}, + setBinding = function(title,text) { + if(title) { + wiki.addTiddler(new $tw.Tiddler({title: title, text: text})); + } + }; + if(url) { + setBinding(bindStatus,"pending"); + setBinding(bindProgress,"0"); + $tw.utils.each(paramObject,function(value,name) { + // Look for query- parameters + if(name.substr(0,QUERY_PARAMETER_PREFIX.length) === QUERY_PARAMETER_PREFIX) { + url = $tw.utils.setQueryStringParameter(url,name.substr(QUERY_PARAMETER_PREFIX.length),value); + } + // Look for header- parameters + if(name.substr(0,HEADER_PARAMETER_PREFIX.length) === HEADER_PARAMETER_PREFIX) { + requestHeaders[name.substr(HEADER_PARAMETER_PREFIX.length)] = value; + } + // Look for password-header- parameters + if(name.substr(0,PASSWORD_QUERY_PARAMETER_PREFIX.length) === PASSWORD_QUERY_PARAMETER_PREFIX) { + url = $tw.utils.setQueryStringParameter(url,name.substr(PASSWORD_QUERY_PARAMETER_PREFIX.length),$tw.utils.getPassword(value) || ""); + } + // Look for password-query- parameters + if(name.substr(0,PASSWORD_HEADER_PARAMETER_PREFIX.length) === PASSWORD_HEADER_PARAMETER_PREFIX) { + requestHeaders[name.substr(PASSWORD_HEADER_PARAMETER_PREFIX.length)] = $tw.utils.getPassword(value) || ""; + } + // Look for var- parameters + if(name.substr(0,CONTEXT_VARIABLE_PARAMETER_PREFIX.length) === CONTEXT_VARIABLE_PARAMETER_PREFIX) { + contextVariables[name.substr(CONTEXT_VARIABLE_PARAMETER_PREFIX.length)] = value; + } + }); + // Set the request tracker tiddler + var requestTrackerTitle = wiki.generateNewTitle("$:/temp/HttpRequest"); + wiki.addTiddler({ + title: requestTrackerTitle, + tags: "$:/tags/HttpRequest", + text: JSON.stringify({ + url: url, + type: method, + status: "inprogress", + headers: requestHeaders, + data: paramObject.body + }) + }); + $tw.utils.httpRequest({ + url: url, + type: method, + headers: requestHeaders, + data: paramObject.body, + callback: function(err,data,xhr) { + var success = (xhr.status >= 200 && xhr.status < 300) ? "complete" : "error", + headers = {}; + $tw.utils.each(xhr.getAllResponseHeaders().split("\r\n"),function(line) { + var pos = line.indexOf(":"); + if(pos !== -1) { + headers[line.substr(0,pos)] = line.substr(pos + 1).trim(); + } + }); + setBinding(bindStatus,success); + setBinding(bindProgress,"100"); + var results = { + status: xhr.status.toString(), + statusText: xhr.statusText, + error: (err || "").toString(), + data: (data || "").toString(), + headers: JSON.stringify(headers) + }; + // Update the request tracker tiddler + wiki.addTiddler(new $tw.Tiddler(wiki.getTiddler(requestTrackerTitle),{ + status: success, + })); + wiki.invokeActionString(completionActions,undefined,$tw.utils.extend({},contextVariables,results),{parentWidget: $tw.rootWidget}); + // console.log("Back!",err,data,xhr); + }, + progress: function(lengthComputable,loaded,total) { + if(lengthComputable) { + setBinding(bindProgress,"" + Math.floor((loaded/total) * 100)) + } + wiki.invokeActionString(progressActions,undefined,{ + lengthComputable: lengthComputable ? "yes" : "no", + loaded: loaded, + total: total + },{parentWidget: $tw.rootWidget}); + } + }); + } +}; + +exports.HttpClient = HttpClient; + +/* +Make an HTTP request. Options are: url: URL to retrieve headers: hashmap of headers to send type: GET, PUT, POST etc callback: function invoked with (err,data,xhr) + progress: optional function invoked with (lengthComputable,loaded,total) returnProp: string name of the property to return as first argument of callback */ exports.httpRequest = function(options) { @@ -83,8 +199,16 @@ exports.httpRequest = function(options) { options.callback($tw.language.getString("Error/XMLHttpRequest") + ": " + this.status,null,this); } }; + // Handle progress + if(options.progress) { + request.onprogress = function(event) { + console.log("Progress event",event) + options.progress(event.lengthComputable,event.loaded,event.total); + }; + } // Make the request request.open(type,url,true); + // Headers if(headers) { $tw.utils.each(headers,function(header,headerTitle,object) { request.setRequestHeader(headerTitle,header); @@ -96,6 +220,7 @@ exports.httpRequest = function(options) { if(!hasHeader("X-Requested-With") && !isSimpleRequest(type,headers)) { request.setRequestHeader("X-Requested-With","TiddlyWiki"); } + // Send data try { request.send(data); } catch(e) { @@ -104,4 +229,19 @@ exports.httpRequest = function(options) { return request; }; +exports.setQueryStringParameter = function(url,paramName,paramValue) { + var URL = $tw.browser ? window.URL : require("url").URL, + newUrl; + try { + newUrl = new URL(url); + } catch(e) { + } + if(newUrl && paramName) { + newUrl.searchParams.set(paramName,paramValue || ""); + return newUrl.toString(); + } else { + return url; + } +}; + })(); diff --git a/core/modules/wiki.js b/core/modules/wiki.js index 6ae16a2b4..85cacaa32 100755 --- a/core/modules/wiki.js +++ b/core/modules/wiki.js @@ -1415,6 +1415,14 @@ exports.checkTiddlerText = function(title,targetText,options) { return text === targetText; } +/* +Execute an action string without an associated context widget +*/ +exports.invokeActionString = function(actions,event,variables,options) { + var widget = this.makeWidget(null,{parentWidget: options.parentWidget}); + widget.invokeActionString(actions,null,event,variables); +}; + /* Read an array of browser File objects, invoking callback(tiddlerFieldsArray) once they're all read */ diff --git a/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request Example Zotero.tid b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request Example Zotero.tid new file mode 100644 index 000000000..9419f526d --- /dev/null +++ b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request Example Zotero.tid @@ -0,0 +1,112 @@ +title: WidgetMessage: tm-http-request Example - Zotero +tags: $:/tags/Macro + +\procedure select-zotero-group() +Specify the Zotero group ID to import +<$edit-text tiddler="$:/config/zotero-group" tag="input"/> or +<$select tiddler="$:/config/zotero-group"> + + + + +\end + +\procedure zotero-save-item(item) +<$action-createtiddler + $basetitle={{{ =[[_zotero_import ]] =[jsonget[key]] =[[ ]] =[jsonget[title]] +[join[]] }}} + text={{{ [jsonget[title]] }}} + tags="$:/tags/ZoteroImport" +> + <$action-setmultiplefields $tiddler=<> $fields="[jsonindexes[]addprefix[zotero-]]" $values="[jsonindexes[]] :map[jsongetelse[.XXXXX.]]"/> + <$list filter="[jsonindexes[creators]]" variable="creatorIndex"> + <$action-setmultiplefields $tiddler=<> $fields="[jsonget[creators],,[creatorType]addprefix[zotero-]]" $values="[jsonget[creators],,[lastName]] [jsonget[creators],,[firstName]] +[join[, ]] :else[jsonget[creators],,[name]] "/> + + +\end zotero-save-item + +\procedure zotero-save-items(data) +<$list filter="[jsonindexes[]] :map[jsonextract,[data]]" variable="item"> + <$macrocall $name="zotero-save-item" item=<>/> + +\end zotero-save-items + +\procedure zotero-get-items(start:"0",limit:"25") + +\procedure completion() +\import [[$:/core/ui/PageMacros]] [all[shadows+tiddlers]tag[$:/tags/Macro]!has[draft.of]] + <$action-log msg="In completion"/> + <$action-log/> + + <$list filter="[compare:number:gteq[200]compare:number:lteq[299]]" variable="ignore"> + + <$macrocall $name="zotero-save-items" data=<>/> + + <$list filter="[jsonget[total-results]subtractsubtractcompare:number:gt[0]]" variable="ignore"> + <$macrocall $name="zotero-get-items" start={{{ [add] }}} limit=<>/> + + +\end completion + +\procedure progress() +\import [[$:/core/ui/PageMacros]] [all[shadows+tiddlers]tag[$:/tags/Macro]!has[draft.of]] + <$action-log message="In progress-actions"/> +\end progress + +\procedure request-url() +\rules only transcludeinline transcludeblock filteredtranscludeinline filteredtranscludeblock +https://api.zotero.org/groups/{{$:/config/zotero-group}}/items/ +\end request-url + +<$wikify name="url" text=<>> + <$action-sendmessage + $message="tm-http-request" + url=<> + method="GET" + query-format="json" + query-sort="title" + query-start=<> + query-limit=<> + header-accept="application/json" + bind-status="$:/temp/zotero/status" + bind-progress="$:/temp/zotero/progress" + oncompletion=<> + onprogress=<> + var-start=<> + var-limit=<> + /> + +\end + +\procedure zotero-actions() +<$macrocall $name="zotero-get-items" start="0" limit="50"/> +\end + + +<> + +<$button actions=<>> +Start import from Zotero group + + +<$list filter="[tag[$:/tags/ZoteroImport]limit[1]]" variable="ignore"> + +!! Imported Tiddlers + +<$button> +<$action-deletetiddler $filter="[tag[$:/tags/ZoteroImport]]"/> +Delete these tiddlers + + +Export: <$macrocall $name="exportButton" exportFilter="[tag[$:/tags/ZoteroImport]]" lingoBase="$:/language/Buttons/ExportTiddlers/"/> + + + +
    +<$list filter="[tag[$:/tags/ZoteroImport]]"> +
  1. +<$link> +<$view field="title"/> + +
  2. + +
diff --git a/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request.tid b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request.tid new file mode 100644 index 000000000..f74fdfb55 --- /dev/null +++ b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request.tid @@ -0,0 +1,49 @@ +caption: tm-http-request +created: 20230429161453032 +modified: 20230429161453032 +tags: Messages +title: WidgetMessage: tm-http-request +type: text/vnd.tiddlywiki + +The ''tm-http-request'' message is used to make an HTTP request to a server. + +It uses the following properties on the `event` object: + +|!Name |!Description | +|param |Not used | +|paramObject |Hashmap of parameters (see below) | + +The following parameters are used: + +|!Name |!Description | +|method |HTTP method (eg "GET", "POST") | +|body |String data to be sent with the request | +|query-* |Query string parameters with string values | +|header-* |Headers with string values | +|password-header-* |Headers with values taken from the password store | +|password-query-* |Query string parameters with values taken from the password store | +|var-* |Variables to be passed to the completion and progress handlers (without the "var-" prefix) | +|bind-status |Title of tiddler to which the status of the request ("pending", "complete", "error") should be bound | +|bind-progress |Title of tiddler to which the progress of the request (0 to 100) should be bound | +|oncompletion |Action strings to be executed when the request completes | +|onprogress |Action strings to be executed when progress is reported | + +The following variables are passed to the completion handler: + +|!Name |!Description | +|status |HTTP result status code (see [[MDN|https://developer.mozilla.org/en-US/docs/Web/HTTP/Status]]) | +|statusText |HTTP result status text | +|error |Error string | +|data |Returned data | +|headers |Response headers as a JSON object | + +The following variables are passed to the progress handler: + +|!Name |!Description | +|lengthComputable |Whether the progress loaded and total figures are valid - "yes" or "no" | +|loaded |Number of bytes loaded so far | +|total |Total number bytes to be loaded | + +!! Examples + +* [[Zotero's|https://www.zotero.org/]] API for retrieving reference items: [[WidgetMessage: tm-http-request Example - Zotero]] diff --git a/editions/tw5.com/tiddlers/messages/config-zotero-group.tid b/editions/tw5.com/tiddlers/messages/config-zotero-group.tid new file mode 100644 index 000000000..2215c496a --- /dev/null +++ b/editions/tw5.com/tiddlers/messages/config-zotero-group.tid @@ -0,0 +1,2 @@ +title: $:/config/zotero-group +text: 4813312 \ No newline at end of file