-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtiddlywebadaptor.js
379 lines (349 loc) · 10.8 KB
/
tiddlywebadaptor.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
/*\
title: $:/plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js
type: application/javascript
module-type: syncadaptor
A sync adaptor module for synchronising with TiddlyWeb compatible servers
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var CONFIG_HOST_TIDDLER = "$:/config/tiddlyweb/host",
DEFAULT_HOST_TIDDLER = "$protocol$//$host$/";
function TiddlyWebAdaptor(options) {
this.wiki = options.wiki;
this.host = this.getHost();
this.recipe = undefined;
this.hasStatus = false;
this.logger = new $tw.utils.Logger("TiddlyWebAdaptor");
this.isLoggedIn = false;
this.isReadOnly = false;
this.logoutIsAvailable = true;
}
TiddlyWebAdaptor.prototype.name = "tiddlyweb";
TiddlyWebAdaptor.prototype.supportsLazyLoading = true;
TiddlyWebAdaptor.prototype.setLoggerSaveBuffer = function(loggerForSaving) {
this.logger.setSaveBuffer(loggerForSaving);
};
TiddlyWebAdaptor.prototype.isReady = function() {
return this.hasStatus;
};
TiddlyWebAdaptor.prototype.getHost = function() {
var text = this.wiki.getTiddlerText(CONFIG_HOST_TIDDLER,DEFAULT_HOST_TIDDLER),
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];
text = $tw.utils.replaceString(text,new RegExp("\\$" + s.name + "\\$","mg"),s.value);
}
return text;
};
TiddlyWebAdaptor.prototype.getTiddlerInfo = function(tiddler) {
return {
bag: tiddler.fields.bag
};
};
TiddlyWebAdaptor.prototype.getTiddlerRevision = function(title) {
var tiddler = this.wiki.getTiddler(title);
return tiddler.fields.revision;
};
/*
Get the current status of the TiddlyWeb connection
*/
TiddlyWebAdaptor.prototype.getStatus = function(callback) {
// Get status
var self = this;
this.logger.log("Getting status");
$tw.utils.httpRequest({
url: this.host + "status",
callback: function(err,data) {
self.hasStatus = true;
if(err) {
return callback(err);
}
//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();
}
// Decode the status JSON
var json = null;
try {
json = JSON.parse(data);
} catch (e) {
}
if(json) {
self.logger.log("Status:",data);
// Record the recipe
if(json.space) {
self.recipe = json.space.recipe;
}
// Check if we're logged in
self.isLoggedIn = json.username !== "GUEST";
self.isReadOnly = !!json["read_only"];
self.isAnonymous = !!json.anonymous;
self.logoutIsAvailable = "logout_is_available" in json ? !!json["logout_is_available"] : true;
}
// Invoke the callback if present
if(callback) {
callback(null,self.isLoggedIn,json.username,self.isReadOnly,self.isAnonymous);
}
}
});
};
/*
Attempt to login and invoke the callback(err)
*/
TiddlyWebAdaptor.prototype.login = function(username,password,callback) {
var options = {
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);
},
headers: {
"accept": "application/json",
"X-Requested-With": "TiddlyWiki"
}
};
this.logger.log("Logging in:",options);
$tw.utils.httpRequest(options);
};
/*
*/
TiddlyWebAdaptor.prototype.logout = function(callback) {
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);
}
};
/*
Retrieve the CSRF token from its cookie
*/
TiddlyWebAdaptor.prototype.getCsrfToken = function() {
var regex = /^(?:.*; )?csrf_token=([^(;|$)]*)(?:;|$)/,
match = regex.exec(document.cookie),
csrf = null;
if (match && (match.length === 2)) {
csrf = match[1];
}
return csrf;
};
/*
Get an array of skinny tiddler fields from the server
*/
TiddlyWebAdaptor.prototype.getSkinnyTiddlers = function(callback) {
var self = this;
$tw.utils.httpRequest({
url: this.host + "recipes/" + this.recipe + "/tiddlers.json",
data: {
filter: "[all[tiddlers]] -[[$:/isEncrypted]] -[prefix[$:/temp/]] -[prefix[$:/status/]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] -[[$:/library/sjcl.js]] -[[$:/core]]"
},
callback: function(err,data) {
// Check for errors
if(err) {
return callback(err);
}
// Process the tiddlers to make sure the revision is a string
var tiddlers = JSON.parse(data);
for(var t=0; t<tiddlers.length; t++) {
tiddlers[t] = self.convertTiddlerFromTiddlyWebFormat(tiddlers[t]);
}
// Invoke the callback with the skinny tiddlers
callback(null,tiddlers);
// 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();
}
}
});
};
/*
Save a tiddler and invoke the callback with (err,adaptorInfo,revision)
*/
TiddlyWebAdaptor.prototype.saveTiddler = function(tiddler,callback,options) {
var self = this;
if(this.isReadOnly) {
return callback(null);
}
$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);
}
//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)
}
// Save the details of the new revision of the tiddler
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
},etagInfo.revision);
}
}
});
};
/*
Load a tiddler and invoke the callback with (err,tiddlerFields)
*/
TiddlyWebAdaptor.prototype.loadTiddler = function(title,callback) {
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
callback(null,self.convertTiddlerFromTiddlyWebFormat(JSON.parse(data)));
}
});
};
/*
Delete a tiddler and invoke the callback with (err)
options include:
tiddlerInfo: the syncer's tiddlerInfo for this tiddler
*/
TiddlyWebAdaptor.prototype.deleteTiddler = function(title,callback,options) {
var self = this;
if(this.isReadOnly) {
return callback(null);
}
// 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
var bag = options.tiddlerInfo.adaptorInfo && options.tiddlerInfo.adaptorInfo.bag;
if(!bag) {
return callback(null,options.tiddlerInfo.adaptorInfo);
}
// Issue HTTP request to delete the tiddler
$tw.utils.httpRequest({
url: this.host + "bags/" + encodeURIComponent(bag) + "/tiddlers/" + encodeURIComponent(title),
type: "DELETE",
callback: function(err,data,request) {
if(err) {
return callback(err);
}
// Invoke the callback & return null adaptorInfo
callback(null,null);
}
});
};
/*
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;
}
});
}
// Default the content type
result.type = result.type || "text/vnd.tiddlywiki";
return JSON.stringify(result,null,$tw.config.preferences.jsonSpaces);
};
/*
Convert a field set in TiddlyWeb format into ordinary TiddlyWiki5 format
*/
TiddlyWebAdaptor.prototype.convertTiddlerFromTiddlyWebFormat = function(tiddlerFields) {
var self = this,
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];
}
});
// Make sure the revision is expressed as a string
if(typeof result.revision === "number") {
result.revision = result.revision.toString();
}
// 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 {
bag: $tw.utils.decodeURIComponentSafe(etag.substring(1,firstSlash)),
title: $tw.utils.decodeURIComponentSafe(etag.substring(firstSlash + 1,lastSlash)),
revision: etag.substring(lastSlash + 1,colon)
};
}
};
if($tw.browser && document.location.protocol.substr(0,4) === "http" ) {
exports.adaptorClass = TiddlyWebAdaptor;
}
})();