mirror of
				https://github.com/Jermolene/TiddlyWiki5
				synced 2025-10-31 15:42:59 +00:00 
			
		
		
		
	 a980390870
			
		
	
	a980390870
	
	
	
		
			
			It is now possible to create and edit tiddlers, using the existing tiddlywebadaptor syncing mechanism. There are a lot of hacks and lumpiness to make things compatible, so I think I will end up with an independent implementation
		
			
				
	
	
		
			381 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			381 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /*\
 | |
| 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},
 | |
| 			{name: "pathname", value: document.location.pathname}
 | |
| 		];
 | |
| 	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;
 | |
| }
 | |
| 
 | |
| })();
 |