2013-03-17 15:28:49 +00:00
|
|
|
/*\
|
|
|
|
title: $:/plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js
|
|
|
|
type: application/javascript
|
|
|
|
module-type: syncadaptor
|
|
|
|
|
2014-08-14 10:12:25 +00:00
|
|
|
A sync adaptor module for synchronising with TiddlyWeb compatible servers
|
2013-03-17 15:28:49 +00:00
|
|
|
|
|
|
|
\*/
|
|
|
|
(function(){
|
|
|
|
|
|
|
|
/*jslint node: true, browser: true */
|
|
|
|
/*global $tw: false */
|
|
|
|
"use strict";
|
|
|
|
|
2013-08-03 15:25:47 +00:00
|
|
|
var CONFIG_HOST_TIDDLER = "$:/config/tiddlyweb/host",
|
|
|
|
DEFAULT_HOST_TIDDLER = "$protocol$//$host$/";
|
|
|
|
|
2014-08-14 10:12:25 +00:00
|
|
|
function TiddlyWebAdaptor(options) {
|
|
|
|
this.wiki = options.wiki;
|
2013-08-03 15:25:47 +00:00
|
|
|
this.host = this.getHost();
|
2013-03-17 15:28:49 +00:00
|
|
|
this.recipe = undefined;
|
2016-07-05 10:29:59 +00:00
|
|
|
this.hasStatus = false;
|
2014-02-14 07:53:41 +00:00
|
|
|
this.logger = new $tw.utils.Logger("TiddlyWebAdaptor");
|
2018-07-18 15:54:43 +00:00
|
|
|
this.isLoggedIn = false;
|
|
|
|
this.isReadOnly = false;
|
2022-12-24 12:13:01 +00:00
|
|
|
this.logoutIsAvailable = true;
|
2013-03-17 15:28:49 +00:00
|
|
|
}
|
|
|
|
|
2017-02-04 17:25:30 +00:00
|
|
|
TiddlyWebAdaptor.prototype.name = "tiddlyweb";
|
|
|
|
|
2020-03-30 14:24:05 +00:00
|
|
|
TiddlyWebAdaptor.prototype.supportsLazyLoading = true;
|
|
|
|
|
|
|
|
TiddlyWebAdaptor.prototype.setLoggerSaveBuffer = function(loggerForSaving) {
|
|
|
|
this.logger.setSaveBuffer(loggerForSaving);
|
|
|
|
};
|
|
|
|
|
2016-07-05 10:29:59 +00:00
|
|
|
TiddlyWebAdaptor.prototype.isReady = function() {
|
|
|
|
return this.hasStatus;
|
|
|
|
};
|
|
|
|
|
2013-08-03 15:25:47 +00:00
|
|
|
TiddlyWebAdaptor.prototype.getHost = function() {
|
2014-08-14 10:12:25 +00:00
|
|
|
var text = this.wiki.getTiddlerText(CONFIG_HOST_TIDDLER,DEFAULT_HOST_TIDDLER),
|
2013-08-03 15:25:47 +00:00
|
|
|
substitutions = [
|
|
|
|
{name: "protocol", value: document.location.protocol},
|
|
|
|
{name: "host", value: document.location.host}
|
|
|
|
];
|
|
|
|
for(var t=0; t<substitutions.length; t++) {
|
|
|
|
var s = substitutions[t];
|
2016-08-04 14:54:33 +00:00
|
|
|
text = $tw.utils.replaceString(text,new RegExp("\\$" + s.name + "\\$","mg"),s.value);
|
2013-08-03 15:25:47 +00:00
|
|
|
}
|
|
|
|
return text;
|
|
|
|
};
|
|
|
|
|
2013-03-17 15:28:49 +00:00
|
|
|
TiddlyWebAdaptor.prototype.getTiddlerInfo = function(tiddler) {
|
|
|
|
return {
|
2014-08-30 20:32:55 +00:00
|
|
|
bag: tiddler.fields.bag
|
2013-03-17 15:28:49 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2020-03-30 14:24:05 +00:00
|
|
|
TiddlyWebAdaptor.prototype.getTiddlerRevision = function(title) {
|
|
|
|
var tiddler = this.wiki.getTiddler(title);
|
|
|
|
return tiddler.fields.revision;
|
|
|
|
};
|
|
|
|
|
2013-03-17 15:28:49 +00:00
|
|
|
/*
|
|
|
|
Get the current status of the TiddlyWeb connection
|
|
|
|
*/
|
|
|
|
TiddlyWebAdaptor.prototype.getStatus = function(callback) {
|
|
|
|
// Get status
|
2014-08-14 10:12:25 +00:00
|
|
|
var self = this;
|
2014-02-14 07:53:41 +00:00
|
|
|
this.logger.log("Getting status");
|
2013-03-17 15:28:49 +00:00
|
|
|
$tw.utils.httpRequest({
|
|
|
|
url: this.host + "status",
|
|
|
|
callback: function(err,data) {
|
2016-07-05 10:29:59 +00:00
|
|
|
self.hasStatus = true;
|
2013-03-17 15:28:49 +00:00
|
|
|
if(err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
2023-01-17 22:12:18 +00:00
|
|
|
//If Browser-Storage plugin is present, cache pre-loaded tiddlers and add back after sync from server completes
|
|
|
|
if($tw.browserStorage && $tw.browserStorage.isEnabled()) {
|
|
|
|
$tw.browserStorage.cachePreloadTiddlers();
|
|
|
|
}
|
2013-03-17 15:28:49 +00:00
|
|
|
// Decode the status JSON
|
2018-07-18 15:54:43 +00:00
|
|
|
var json = null;
|
2013-03-17 15:28:49 +00:00
|
|
|
try {
|
|
|
|
json = JSON.parse(data);
|
2024-08-20 15:33:07 +00:00
|
|
|
} catch(e) {
|
2013-03-17 15:28:49 +00:00
|
|
|
}
|
|
|
|
if(json) {
|
2014-02-14 07:53:41 +00:00
|
|
|
self.logger.log("Status:",data);
|
2013-03-17 15:28:49 +00:00
|
|
|
// Record the recipe
|
|
|
|
if(json.space) {
|
|
|
|
self.recipe = json.space.recipe;
|
|
|
|
}
|
|
|
|
// Check if we're logged in
|
2018-07-18 15:54:43 +00:00
|
|
|
self.isLoggedIn = json.username !== "GUEST";
|
|
|
|
self.isReadOnly = !!json["read_only"];
|
|
|
|
self.isAnonymous = !!json.anonymous;
|
2022-12-24 12:13:01 +00:00
|
|
|
self.logoutIsAvailable = "logout_is_available" in json ? !!json["logout_is_available"] : true;
|
2013-03-17 15:28:49 +00:00
|
|
|
}
|
|
|
|
// Invoke the callback if present
|
|
|
|
if(callback) {
|
2021-07-14 16:16:57 +00:00
|
|
|
callback(null,self.isLoggedIn,json.username,self.isReadOnly,self.isAnonymous);
|
2013-03-17 15:28:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Attempt to login and invoke the callback(err)
|
|
|
|
*/
|
|
|
|
TiddlyWebAdaptor.prototype.login = function(username,password,callback) {
|
2014-01-26 20:59:30 +00:00
|
|
|
var options = {
|
2013-03-17 15:28:49 +00:00
|
|
|
url: this.host + "challenge/tiddlywebplugins.tiddlyspace.cookie_form",
|
|
|
|
type: "POST",
|
|
|
|
data: {
|
|
|
|
user: username,
|
|
|
|
password: password,
|
|
|
|
tiddlyweb_redirect: "/status" // workaround to marginalize automatic subsequent GET
|
|
|
|
},
|
|
|
|
callback: function(err) {
|
|
|
|
callback(err);
|
2021-08-17 08:56:52 +00:00
|
|
|
},
|
|
|
|
headers: {
|
|
|
|
"accept": "application/json",
|
|
|
|
"X-Requested-With": "TiddlyWiki"
|
2013-03-17 15:28:49 +00:00
|
|
|
}
|
2014-01-26 20:59:30 +00:00
|
|
|
};
|
2014-02-14 07:53:41 +00:00
|
|
|
this.logger.log("Logging in:",options);
|
2014-01-26 20:59:30 +00:00
|
|
|
$tw.utils.httpRequest(options);
|
2013-03-17 15:28:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
*/
|
|
|
|
TiddlyWebAdaptor.prototype.logout = function(callback) {
|
2022-12-24 12:13:01 +00:00
|
|
|
if(this.logoutIsAvailable) {
|
|
|
|
var options = {
|
|
|
|
url: this.host + "logout",
|
|
|
|
type: "POST",
|
|
|
|
data: {
|
|
|
|
csrf_token: this.getCsrfToken(),
|
|
|
|
tiddlyweb_redirect: "/status" // workaround to marginalize automatic subsequent GET
|
|
|
|
},
|
|
|
|
callback: function(err,data,xhr) {
|
|
|
|
callback(err);
|
|
|
|
},
|
|
|
|
headers: {
|
|
|
|
"accept": "application/json",
|
|
|
|
"X-Requested-With": "TiddlyWiki"
|
|
|
|
}
|
|
|
|
};
|
|
|
|
this.logger.log("Logging out:",options);
|
|
|
|
$tw.utils.httpRequest(options);
|
|
|
|
} else {
|
|
|
|
alert("This server does not support logging out. If you are using basic authentication the only way to logout is close all browser windows");
|
|
|
|
callback(null);
|
|
|
|
}
|
2013-03-17 15:28:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Retrieve the CSRF token from its cookie
|
|
|
|
*/
|
|
|
|
TiddlyWebAdaptor.prototype.getCsrfToken = function() {
|
|
|
|
var regex = /^(?:.*; )?csrf_token=([^(;|$)]*)(?:;|$)/,
|
|
|
|
match = regex.exec(document.cookie),
|
|
|
|
csrf = null;
|
2024-08-20 15:33:07 +00:00
|
|
|
if(match && (match.length === 2)) {
|
2013-03-17 15:28:49 +00:00
|
|
|
csrf = match[1];
|
|
|
|
}
|
|
|
|
return csrf;
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Get an array of skinny tiddler fields from the server
|
|
|
|
*/
|
|
|
|
TiddlyWebAdaptor.prototype.getSkinnyTiddlers = function(callback) {
|
2013-11-08 20:18:26 +00:00
|
|
|
var self = this;
|
2013-03-17 15:28:49 +00:00
|
|
|
$tw.utils.httpRequest({
|
|
|
|
url: this.host + "recipes/" + this.recipe + "/tiddlers.json",
|
2020-03-30 14:24:05 +00:00
|
|
|
data: {
|
2020-08-14 10:06:08 +00:00
|
|
|
filter: "[all[tiddlers]] -[[$:/isEncrypted]] -[prefix[$:/temp/]] -[prefix[$:/status/]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] -[[$:/library/sjcl.js]] -[[$:/core]]"
|
2020-03-30 14:24:05 +00:00
|
|
|
},
|
2013-03-17 15:28:49 +00:00
|
|
|
callback: function(err,data) {
|
|
|
|
// Check for errors
|
|
|
|
if(err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
2013-03-18 10:13:36 +00:00
|
|
|
// Process the tiddlers to make sure the revision is a string
|
|
|
|
var tiddlers = JSON.parse(data);
|
|
|
|
for(var t=0; t<tiddlers.length; t++) {
|
2014-02-11 15:47:56 +00:00
|
|
|
tiddlers[t] = self.convertTiddlerFromTiddlyWebFormat(tiddlers[t]);
|
2013-03-18 10:13:36 +00:00
|
|
|
}
|
2013-03-17 15:28:49 +00:00
|
|
|
// Invoke the callback with the skinny tiddlers
|
2013-03-18 10:13:36 +00:00
|
|
|
callback(null,tiddlers);
|
2023-01-17 22:12:18 +00:00
|
|
|
// If Browswer Storage tiddlers were cached on reloading the wiki, add them after sync from server completes in the above callback.
|
|
|
|
if($tw.browserStorage && $tw.browserStorage.isEnabled()) {
|
|
|
|
$tw.browserStorage.addCachedTiddlers();
|
|
|
|
}
|
2013-03-17 15:28:49 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Save a tiddler and invoke the callback with (err,adaptorInfo,revision)
|
|
|
|
*/
|
2021-07-05 18:26:20 +00:00
|
|
|
TiddlyWebAdaptor.prototype.saveTiddler = function(tiddler,callback,options) {
|
2013-03-17 15:28:49 +00:00
|
|
|
var self = this;
|
2018-07-18 15:54:43 +00:00
|
|
|
if(this.isReadOnly) {
|
2022-12-30 19:41:41 +00:00
|
|
|
return callback(null);
|
2018-07-18 15:54:43 +00:00
|
|
|
}
|
2013-03-17 15:28:49 +00:00
|
|
|
$tw.utils.httpRequest({
|
|
|
|
url: this.host + "recipes/" + encodeURIComponent(this.recipe) + "/tiddlers/" + encodeURIComponent(tiddler.fields.title),
|
|
|
|
type: "PUT",
|
|
|
|
headers: {
|
|
|
|
"Content-type": "application/json"
|
|
|
|
},
|
|
|
|
data: this.convertTiddlerToTiddlyWebFormat(tiddler),
|
|
|
|
callback: function(err,data,request) {
|
|
|
|
if(err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
2023-01-17 22:12:18 +00:00
|
|
|
//If Browser-Storage plugin is present, remove tiddler from local storage after successful sync to the server
|
|
|
|
if($tw.browserStorage && $tw.browserStorage.isEnabled()) {
|
|
|
|
$tw.browserStorage.removeTiddlerFromLocalStorage(tiddler.fields.title)
|
|
|
|
}
|
2013-03-17 15:28:49 +00:00
|
|
|
// Save the details of the new revision of the tiddler
|
2021-01-03 11:46:40 +00:00
|
|
|
var etag = request.getResponseHeader("Etag");
|
|
|
|
if(!etag) {
|
|
|
|
callback("Response from server is missing required `etag` header");
|
|
|
|
} else {
|
|
|
|
var etagInfo = self.parseEtag(etag);
|
|
|
|
// Invoke the callback
|
|
|
|
callback(null,{
|
|
|
|
bag: etagInfo.bag
|
2022-12-30 19:41:41 +00:00
|
|
|
},etagInfo.revision);
|
2021-01-03 11:46:40 +00:00
|
|
|
}
|
2013-03-17 15:28:49 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Load a tiddler and invoke the callback with (err,tiddlerFields)
|
|
|
|
*/
|
2021-07-05 18:26:20 +00:00
|
|
|
TiddlyWebAdaptor.prototype.loadTiddler = function(title,callback) {
|
2013-03-17 15:28:49 +00:00
|
|
|
var self = this;
|
|
|
|
$tw.utils.httpRequest({
|
|
|
|
url: this.host + "recipes/" + encodeURIComponent(this.recipe) + "/tiddlers/" + encodeURIComponent(title),
|
|
|
|
callback: function(err,data,request) {
|
|
|
|
if(err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
// Invoke the callback
|
2013-11-08 20:18:26 +00:00
|
|
|
callback(null,self.convertTiddlerFromTiddlyWebFormat(JSON.parse(data)));
|
2013-03-17 15:28:49 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Delete a tiddler and invoke the callback with (err)
|
2014-08-14 10:12:25 +00:00
|
|
|
options include:
|
|
|
|
tiddlerInfo: the syncer's tiddlerInfo for this tiddler
|
2013-03-17 15:28:49 +00:00
|
|
|
*/
|
2021-07-05 18:26:20 +00:00
|
|
|
TiddlyWebAdaptor.prototype.deleteTiddler = function(title,callback,options) {
|
2018-07-18 15:54:43 +00:00
|
|
|
var self = this;
|
|
|
|
if(this.isReadOnly) {
|
2022-12-30 19:41:41 +00:00
|
|
|
return callback(null);
|
2018-07-18 15:54:43 +00:00
|
|
|
}
|
2014-01-26 20:59:30 +00:00
|
|
|
// If we don't have a bag it means that the tiddler hasn't been seen by the server, so we don't need to delete it
|
2020-03-30 14:24:05 +00:00
|
|
|
var bag = options.tiddlerInfo.adaptorInfo && options.tiddlerInfo.adaptorInfo.bag;
|
2014-01-26 20:59:30 +00:00
|
|
|
if(!bag) {
|
2021-02-04 16:11:07 +00:00
|
|
|
return callback(null,options.tiddlerInfo.adaptorInfo);
|
2014-01-26 20:59:30 +00:00
|
|
|
}
|
|
|
|
// Issue HTTP request to delete the tiddler
|
2013-03-17 15:28:49 +00:00
|
|
|
$tw.utils.httpRequest({
|
|
|
|
url: this.host + "bags/" + encodeURIComponent(bag) + "/tiddlers/" + encodeURIComponent(title),
|
|
|
|
type: "DELETE",
|
|
|
|
callback: function(err,data,request) {
|
|
|
|
if(err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
2021-02-04 16:11:07 +00:00
|
|
|
// Invoke the callback & return null adaptorInfo
|
|
|
|
callback(null,null);
|
2013-03-17 15:28:49 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Convert a tiddler to a field set suitable for PUTting to TiddlyWeb
|
|
|
|
*/
|
|
|
|
TiddlyWebAdaptor.prototype.convertTiddlerToTiddlyWebFormat = function(tiddler) {
|
|
|
|
var result = {},
|
|
|
|
knownFields = [
|
|
|
|
"bag", "created", "creator", "modified", "modifier", "permissions", "recipe", "revision", "tags", "text", "title", "type", "uri"
|
|
|
|
];
|
|
|
|
if(tiddler) {
|
|
|
|
$tw.utils.each(tiddler.fields,function(fieldValue,fieldName) {
|
|
|
|
var fieldString = fieldName === "tags" ?
|
|
|
|
tiddler.fields.tags :
|
|
|
|
tiddler.getFieldString(fieldName); // Tags must be passed as an array, not a string
|
|
|
|
|
|
|
|
if(knownFields.indexOf(fieldName) !== -1) {
|
|
|
|
// If it's a known field, just copy it across
|
|
|
|
result[fieldName] = fieldString;
|
|
|
|
} else {
|
|
|
|
// If it's unknown, put it in the "fields" field
|
|
|
|
result.fields = result.fields || {};
|
|
|
|
result.fields[fieldName] = fieldString;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2014-09-01 09:48:40 +00:00
|
|
|
// Default the content type
|
2013-11-08 20:18:26 +00:00
|
|
|
result.type = result.type || "text/vnd.tiddlywiki";
|
2013-03-17 15:28:49 +00:00
|
|
|
return JSON.stringify(result,null,$tw.config.preferences.jsonSpaces);
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Convert a field set in TiddlyWeb format into ordinary TiddlyWiki5 format
|
|
|
|
*/
|
2013-11-08 20:18:26 +00:00
|
|
|
TiddlyWebAdaptor.prototype.convertTiddlerFromTiddlyWebFormat = function(tiddlerFields) {
|
|
|
|
var self = this,
|
2013-03-17 15:28:49 +00:00
|
|
|
result = {};
|
|
|
|
// Transfer the fields, pulling down the `fields` hashmap
|
|
|
|
$tw.utils.each(tiddlerFields,function(element,title,object) {
|
|
|
|
if(title === "fields") {
|
|
|
|
$tw.utils.each(element,function(element,subTitle,object) {
|
|
|
|
result[subTitle] = element;
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
result[title] = tiddlerFields[title];
|
|
|
|
}
|
|
|
|
});
|
2013-03-18 10:13:36 +00:00
|
|
|
// Make sure the revision is expressed as a string
|
|
|
|
if(typeof result.revision === "number") {
|
|
|
|
result.revision = result.revision.toString();
|
|
|
|
}
|
2013-03-17 15:28:49 +00:00
|
|
|
// Some unholy freaking of content types
|
|
|
|
if(result.type === "text/javascript") {
|
|
|
|
result.type = "application/javascript";
|
|
|
|
} else if(!result.type || result.type === "None") {
|
|
|
|
result.type = "text/x-tiddlywiki";
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Split a TiddlyWeb Etag into its constituent parts. For example:
|
|
|
|
|
|
|
|
```
|
|
|
|
"system-images_public/unsyncedIcon/946151:9f11c278ccde3a3149f339f4a1db80dd4369fc04"
|
|
|
|
```
|
|
|
|
|
|
|
|
Note that the value includes the opening and closing double quotes.
|
|
|
|
|
|
|
|
The parts are:
|
|
|
|
|
|
|
|
```
|
|
|
|
<bag>/<title>/<revision>:<hash>
|
|
|
|
```
|
|
|
|
*/
|
|
|
|
TiddlyWebAdaptor.prototype.parseEtag = function(etag) {
|
|
|
|
var firstSlash = etag.indexOf("/"),
|
|
|
|
lastSlash = etag.lastIndexOf("/"),
|
|
|
|
colon = etag.lastIndexOf(":");
|
|
|
|
if(firstSlash === -1 || lastSlash === -1 || colon === -1) {
|
|
|
|
return null;
|
|
|
|
} else {
|
|
|
|
return {
|
2021-08-29 12:39:32 +00:00
|
|
|
bag: $tw.utils.decodeURIComponentSafe(etag.substring(1,firstSlash)),
|
|
|
|
title: $tw.utils.decodeURIComponentSafe(etag.substring(firstSlash + 1,lastSlash)),
|
2013-03-17 15:28:49 +00:00
|
|
|
revision: etag.substring(lastSlash + 1,colon)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2014-04-03 15:33:42 +00:00
|
|
|
if($tw.browser && document.location.protocol.substr(0,4) === "http" ) {
|
2013-03-17 15:28:49 +00:00
|
|
|
exports.adaptorClass = TiddlyWebAdaptor;
|
|
|
|
}
|
|
|
|
|
|
|
|
})();
|