Index: main/trunk/greenstone3/web/interfaces/default/js/javascript-global-functions.js
===================================================================
--- main/trunk/greenstone3/web/interfaces/default/js/javascript-global-functions.js	(revision 31536)
+++ main/trunk/greenstone3/web/interfaces/default/js/javascript-global-functions.js	(revision 31537)
@@ -422,5 +422,7 @@
 }
 
-function callMetadataServer(callingFunction, url, responseFunction)
+// No function overloading in JavaScript. Can pass a custom object, however, see
+// http://stackoverflow.com/questions/456177/function-overloading-in-javascript-best-practices
+function callMetadataServer(callingFunction, url, responseFunction, opts)
 {
     var async_setting = true; // Internal processing of 'read' operations (get meta) is not order dependent
@@ -445,4 +447,10 @@
     } // otherwise, such as for get- metadata operation, we proceed as before, which will not require authentication
 
+    if (opts != null && opts["forceSync"] != null) {
+	async_setting = (!opts["forceSync"]);
+    }
+
+    console.log("Away to call: " + url);
+    var ajaxResponse = null;
 
     $.ajax(url, {async: async_setting})
@@ -451,4 +459,6 @@
 		console.log("(" + callingFunction + ") Response received from server: " + response);
 
+	    ajaxResponse = response;
+
 		//var xml = $.parseXML(response);
 		//console.log(xml);
@@ -456,5 +466,6 @@
 		if(responseFunction != null)
 		{
-			responseFunction(response);
+		    
+		    responseFunction(response);
 		}
 	})
@@ -463,4 +474,9 @@
 		console.log("(" + callingFunction + ") Failed");
 	});
+
+    console.log("Finished ajax call to: " + url);
+    
+    console.log("Got response: " + ajaxResponse);
+    return ajaxResponse;
 }
 
@@ -513,8 +529,71 @@
 }
 
+// New. Modified version of the GS2 version of this method in gsajaxapi.js.
+// The where parameter can be specified as one or more of: import, archives, index, live 
+// separated by |. If null, it is assumed to be index which is the original default 
+// behaviour of calling set-metadata-array. E.g. where=import|archives|index
+// THIS METHOD IS SYNCHRONOUS
+gs.functions.setMetadataArray = function(collection, site, docArray, metamode, where, responseFunction) 
+{
+    docArrayJSON = JSON.stringify(docArray);
+    
+    var params = "a=" + escape("set-metadata-array"); //"a=set-metadata-array";
+    if(where != null) {
+	params += "&where=" + escape(where); // if where not specified, meta-server will default to setting index meta
+	//} else {
+	//    params += "&where=import|archives|index";
+    }
+    params += "&c="+escape(collection);
+    params += "&site="+escape(site);
+    params += "&json="+escape(docArrayJSON);
+    
+    if (metamode!=null) {
+	params += "&metamode=" + escape(metamode);
+    }
+    
+
+    var response = callMetadataServer("Setting metadata in "+where, "cgi-bin/metadata-server.pl?"+params, responseFunction);
+
+    return response;
+    // return this.urlPostSync(mdserver,params); // gsajaxapi.js version for GS2
+}
+
+
 /*************************
 * GET METADATA FUNCTIONS *
 *************************/
 
+// New. Modified version of the GS2 version of this method in gsajaxapi.js.
+// See description for setMetadataArray above for information about the 'where' parameter.
+// THIS METHOD IS SYNCHRONOUS BY DEFAULT. Set forceSync to false to override this default behaviour
+gs.functions.getMetadataArray = function(collection, site, docArray, where, forceSync, responseFunction)
+{
+    docArrayJSON = JSON.stringify(docArray);
+	
+    var params = "a=" + escape("get-metadata-array"); //"a=set-metadata-array";
+    if(where != null) {
+	params += "&where=" + escape(where); // if where not specified, meta-server will default to setting index meta
+	//} else {
+	//    params += "&where=import|archives|index";
+    }
+    params += "&c="+escape(collection);
+    params += "&site="+escape(site);
+    params += "&json="+escape(docArrayJSON);
+        
+    // get operations are generally asynchronous, but allow calling function to force ajax call 
+    // to be synchronous or not. Default is synchronous, as it was for GS2
+    if(forceSync == null) {
+	forceSync = true;
+    }
+    // Objects/maps can use identifiers or strings for property names
+    // http://stackoverflow.com/questions/456177/function-overloading-in-javascript-best-practices
+    // https://www.w3schools.com/js/js_objects.asp
+    var response = callMetadataServer("Getting metadata from "+where, "cgi-bin/metadata-server.pl?"+params, responseFunction, {"forceSync":forceSync});
+
+    return response;
+    //return this.urlPostSync(mdserver,params);	// gsajaxapi.js version for GS2
+}
+
+
 gs.functions.getImportMetadata = function(collection, site, documentID, metadataName, responseFunction)
 {
Index: main/trunk/greenstone3/web/interfaces/default/js/user_comments.js
===================================================================
--- main/trunk/greenstone3/web/interfaces/default/js/user_comments.js	(revision 31537)
+++ main/trunk/greenstone3/web/interfaces/default/js/user_comments.js	(revision 31537)
@@ -0,0 +1,317 @@
+// http://toddmotto.com/avoiding-anonymous-javascript-functions
+// https://toddmotto.com/everything-you-wanted-to-know-about-javascript-scope/
+// pass event to addEventListener onmouseover
+// http://stackoverflow.com/questions/256754/how-to-pass-arguments-to-addeventlistener-listener-function
+// http://stackoverflow.com/questions/8714472/cant-pass-event-to-addeventlistener-closure-issue
+
+
+/***************
+* USER COMMENTS
+****************/
+// http://stackoverflow.com/questions/6312993/javascript-seconds-to-time-with-format-hhmmss
+// Call as: alert(timestamp.printTime());
+function formatTime(timestamp) {
+    var int_timestamp    = parseInt(timestamp, 10); // don't forget the second param
+    var date = new Date(int_timestamp);
+    return date.toLocaleDateString() + " " + date.toLocaleTimeString();   
+}
+
+function loadUserComments() {
+    /*
+
+    if(gs.variables) {
+	var listing = "";
+	for (prop in gs.variables) {
+	    listing = listing + prop.toString() + ": " + gs.variables[prop] + "\n";
+	}
+	alert ("gs variables:\n" + listing);
+    } else {
+	alert("LOADING USER COMMENTS - no gs.variables set");
+    }
+    */
+    
+    // don't bother loading comments if we're not on a document page (in which case there's no usercommentdiv)
+    var usercommentdiv = document.getElementById("usercomments");
+    if(usercommentdiv == undefined || usercommentdiv == null) {
+	return;
+    }
+    
+    // else, if we have a usercommentdiv, we would have a docid. Get toplevel section of the docid
+    var doc_id = gs.variables["d"]; ///"_cgiargdJssafe_"; //escape("cgiargd");
+    var period = doc_id.indexOf(".");
+    if(period != -1) {
+	doc_id = doc_id.substring(0, period);
+    }
+    
+    var username_rec = {
+	metaname: "username",
+	metapos: "all"
+    };
+    
+    var timestamp_rec = {
+	metaname: "usertimestamp",
+	metapos: "all"
+    };
+    
+    var comment_rec = {
+	metaname: "usercomment",
+	metapos: "all"
+    };
+
+    var doc_rec = {
+	docid: doc_id,
+	metatable: [username_rec, timestamp_rec, comment_rec]	    
+    };
+
+    var docArray = [doc_rec];
+    //alert(JSON.stringify(docArray));
+
+    var json_result_str = gs.functions.getMetadataArray(gs.variables["c"], gs.variables["site"], docArray, "index");
+    //	alert(json_result_str);
+    var result = JSON.parse(json_result_str);
+    // result contains only one docrec (result[0]), since we asked for the usercomments of one docid
+    var metatable = result[0].metatable;
+    //	alert(JSON.stringify(metatable));
+
+    var i = 0;
+    var looping = true;
+    var print_heading = false; // UNUSED SECTION if(print_heading) below
+    
+    // metatable[0] = list of usernames, metatable[1] = list of timestamps, metatable[2] = list of comments	
+    // the 3 lists/arrays should be of even length. Assuming this, loop as long as there's another username
+    while(looping) {
+	var metaval_rec = metatable[0].metavals[i];
+	if(metaval_rec == undefined) {
+	    looping = false;
+	    } 
+	else {
+	    
+// BEGIN UNUSED
+	    if(print_heading) {
+		var heading=document.createElement("div");
+		var attr=document.createAttribute("class");
+		attr.nodeValue="usercommentheading";
+		heading.setAttributeNode(attr);
+		var txt=document.createTextNode(gs.variables["textusercommentssection"]); ///"_textusercommentssection_");
+		heading.appendChild(txt);
+		usercommentdiv.appendChild(heading);
+		
+ 		print_heading = false;
+	    }
+// END UNUSED
+	    
+    	    var username = metaval_rec.metavalue;
+	    var timestamp = metatable[1].metavals[i].metavalue;  
+	    var comment = metatable[2].metavals[i].metavalue; 
+	    
+	    //alert("Comment: " + username + " " + timestamp + " " + comment);
+	    
+	    // No need to sort by time, as the meta are already stored sorted 
+	    // and hence retrieved in the right order by using the i (metapos) counter
+	    // If sorting the array of comment records, which would be by timestamp, see
+	    // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/sort
+	    
+	    // for each usercomment, create a child div with the username, timestamp and comment
+	    displayInUserCommentList(usercommentdiv, username, timestamp, comment);
+	    
+	    i++;
+	}		
+    }
+
+}
+
+
+function displayInUserCommentList(usercommentdiv, username, timestamp, comment) {
+    
+    //alert("Comment: " + username + " " + timestamp + " " + comment);
+
+    var divgroup=document.createElement("div");
+    var attr=document.createAttribute("class");
+    attr.nodeValue="usercomment";
+    divgroup.setAttributeNode(attr);
+    
+    var divuser=document.createElement("div");
+    var divtime=document.createElement("div");
+    var divcomment=document.createElement("div");
+    
+    
+    divgroup.appendChild(divuser);
+    var txt=document.createTextNode(username);
+    divuser.appendChild(txt);
+    
+    divgroup.appendChild(divtime);
+    txt=document.createTextNode(formatTime(timestamp)); // format timestamp for date/time display
+    divtime.appendChild(txt);
+    
+    // any quotes and colons in the fields would have been protected for transmitting as JSON
+    // so decode their entity values
+    comment = comment.replace(/&quot;/gmi, '"');
+    comment = comment.replace(/&58;/gmi, ':');
+    
+    divgroup.appendChild(divcomment);
+    txt=document.createTextNode(comment);
+    divcomment.appendChild(txt);
+    
+    usercommentdiv.appendChild(divgroup);
+    
+}
+
+
+// Unused. Replaced in favour of call to escape() in setMetaArray function that calls urlPostSync
+// http://stackoverflow.com/questions/6020714/escape-html-using-jquery
+function safeHTML(str) {
+    return str.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"',"&quot;").replace("'","&#x27;").replace("/", "&#x2F;"); //"\\""
+} 
+
+
+function addUserComment(_username, _comment, _docid, doc) {
+    
+    // don't add empty strings for name/comment		
+    
+    // http://stackoverflow.com/questions/498970/how-do-i-trim-a-string-in-javascript
+    //var trimmed_username=_username.replace(/^\s+|\s+$/g, '');
+    var trimmed_comment = _comment.replace(/^\s+|\s+$/g, '');
+    
+    if(!trimmed_comment) { // || !trimmed_username
+	doc.AddUserCommentForm.comment.value = "";		      
+	//doc.AddUserCommentForm.username.value = "";
+	doc.getElementById("usercommentfeedback").innerHTML = gs.variables["textisempty"]; ///"_textisempty_";
+	return;
+    }
+    
+    // Need to the add user comment meta of username, timestamp and comment to the
+    // topmost section of the document. So only get the docId up to any period mark:
+    var period = _docid.indexOf(".");
+    if(period != -1) {
+	_docid = _docid.substring(0, period);
+    }
+    
+    
+    // Want to store username, timestamp and comment in import/metadata.xml, archives/doc.xml
+    // and index/col.gdb.
+    
+    // For getting the current time, see
+    // http://stackoverflow.com/questions/3830244/get-current-date-time-in-seconds
+    var _timestamp = new Date().getTime(); // div by 1000 to get seconds. valueOf() may return string
+    
+    //alert("username:" + _username
+    //+ "\\ncomment: " + _comment
+    //+ "\\ncollection: " + collection 
+    //+ "\\ndocid: " + _docid
+    //+ "\\ntimestamp: " + _timestamp); 
+    
+    
+    // Entity encode the values before storing (at least <, >, forward slash.
+    // And single and double quote, ampersand)
+    // http://stackoverflow.com/questions/6020714/escape-html-using-jquery
+    // setMetadataArray escapes the entire JSON, is that better than escaping individually here?
+    //_docid = escape(_docid);
+    //_timestamp = escape(_timestamp);
+    //_username = escape(_username); //safeHTML(_username);
+    //_comment = escape(_comment); //safeHTML(_comment);
+    
+    // Use this if making individual api calls to set username meta, then timestamp then comment meta
+    // GSAPI already knows the collection
+    //gsapi.setMetadata(_docid, "username", null, _username, "accumulate", "import|archives|index");
+    //gsapi.setMetadata(_docid, "usertimestamp", null, _timestamp, "accumulate", "import|archives|index");
+    //gsapi.setMetadata(_docid, "usercomment", null, _comment, "accumulate", "import|archives|index");
+    
+    
+    // Use the new JSON metatable format to set username, timestamp and comment meta for docid in one go
+    
+    // For creating the JSON object that gets turned into a string, see
+    // http://msdn.microsoft.com/en-us/library/ie/cc836459%28v=vs.94%29.aspx
+    // http://jsfiddle.net/qmacro/W54hy/
+    
+    var username_rec = {
+	metaname: "username",
+	metavals: [_username]
+    };
+    
+    var timestamp_rec = {
+	metaname: "usertimestamp",
+	metavals: [_timestamp]
+    };
+    
+    var comment_rec = {
+	metaname: "usercomment",
+	metavals: [_comment]
+    };
+    
+    var doc_rec = {
+	docid: _docid,
+	metatable: [username_rec, timestamp_rec, comment_rec],
+	metamode: "accumulate"
+    };
+    
+    var docArray = [doc_rec];
+    
+    //alert(JSON.stringify(docArray));
+    
+    // GSAPI already knows the collection
+    var result = gs.functions.setMetadataArray(gs.variables["c"], gs.variables["site"], docArray, "accumulate", "import|archives|index");
+    
+    // clear the comment field as it has now been submitted, but not the username field
+    // as the user is logged in, so they should be able to commit again under their username.
+    doc.AddUserCommentForm.comment.value = ""; 
+    
+    // check for locked collection error
+    var errorIndex = result.indexOf("ERROR");
+    // check for any error discovered on the server side
+    var responseErrorIndex = result.indexOf("<error");
+
+    if(errorIndex != -1) {
+	var endIndex = result.indexOf("\\n");
+	var error = result.substring(errorIndex,endIndex);
+	errormessage="ERROR: Unable to add comment. " + error;
+	doc.getElementById("usercommentfeedback").innerHTML = errormessage;
+	//alert("Result: " + result);
+    } 
+    else if (responseErrorIndex != -1) {
+	var endIndex = result.indexOf("</error>");
+	var startIndex = result.indexOf(">", responseErrorIndex+1);
+	var error = result.substring(startIndex+1,endIndex);
+	errormessage="ERROR: Unable to add comment. " + error;
+	doc.getElementById("usercommentfeedback").innerHTML = errormessage;
+	//alert("Result: " + result);
+    }
+    else { // success!
+	doc.getElementById("usercommentfeedback").innerHTML = gs.variables["textcommentsubmitted"]; ///"_textcommentsubmitted_";		
+	
+	// update display of existing user comments to show the newly added comment
+	var usercommentdiv = document.getElementById("usercomments");
+	if(usercommentdiv != undefined) {
+	    displayInUserCommentList(usercommentdiv, _username, _timestamp, _comment);
+     	}
+    }     
+}
+
+function commentAreaSetup() {
+    loadUserComments();
+    //$("div#commentarea").html("<textarea required=\"required\" name=\"comment\" rows=\"10\" cols=\"64\" placeholder=\"Add your comment here...\"></textarea>");
+    $("div#commentarea").html('<textarea required="required" name="comment" rows="10" cols="64" placeholder="Add your comment here..."></textarea>');
+
+}
+
+
+// "Handlers added via $(document).ready() don't overwrite each other, but rather execute in turn"
+// as explained at http://stackoverflow.com/questions/15564029/adding-to-window-onload-event
+
+$(document).ready(commentAreaSetup);
+
+/*
+// http://stackoverflow.com/questions/807878/javascript-that-executes-after-page-load
+// ensure we don't replace any other onLoad() functions, but append the loadUserComments() 
+// function to the existing onLoad handlers
+
+if(window.onload) {
+    var curronload = window.onload;
+    var newonload = function() {
+        curronload();
+        loadUserComments();
+    };
+    window.onload = newonload;
+} else {
+    window.onload = loadUserComments;
+}
+*/
Index: main/trunk/greenstone3/web/interfaces/default/style/core.css
===================================================================
--- main/trunk/greenstone3/web/interfaces/default/style/core.css	(revision 31536)
+++ main/trunk/greenstone3/web/interfaces/default/style/core.css	(revision 31537)
@@ -1240,2 +1240,40 @@
 }
 
+/* User comments area: display of existing user comments and display of the form and of the add user/logout links */
+
+#usercomments, #usercommentform, #usercommentlink a, #usercommentlogoutlink a {
+    font-family: sans-serif;
+    font-size: 12px;
+}
+
+#usercomments {
+    margin: 10px 0;
+}
+
+.usercommentheading {
+    font-weight: bold;
+    color: #006666;
+    border-top: solid 1px black;
+}
+
+.usercomment {
+    margin: 10px 0;
+}
+
+#usercommentlink, #usercommentlogoutlink {
+    margin: 10px 0 20px 0;
+}
+
+#usercommentlink a, #usercommentlogoutlink a {
+ text-decoration: none;
+ font-weight: bold;
+ color: #006666;
+}
+#usercommentlink a:visited, #usercommentlogoutlink a:visited {
+    color: #006666;
+}
+
+.centrediv {
+    width:50%;
+    margin: 0 auto;
+}
Index: main/trunk/greenstone3/web/interfaces/default/transform/config_format.xsl
===================================================================
--- main/trunk/greenstone3/web/interfaces/default/transform/config_format.xsl	(revision 31536)
+++ main/trunk/greenstone3/web/interfaces/default/transform/config_format.xsl	(revision 31537)
@@ -110,5 +110,5 @@
 	     when the XSLT processes it (which would then result in it
 	     being changed into a self-closing element, which then is
-	     incorrectly rendered as HTML).  Doing thing with the
+	     incorrectly rendered as HTML).  Doing this with the
 	     value-of is better then injecting an xsl:comment in
 	     (another approach we have used in the past) as the
@@ -116,5 +116,5 @@
 	     HTML.  This can lead to further complications if
 	     Javascript using the 'empty' div truely expects it to
-	     have no connent of any form. 
+	     have no content of any form. 
 	-->
 
Index: main/trunk/greenstone3/web/interfaces/default/transform/layouts/header.xsl
===================================================================
--- main/trunk/greenstone3/web/interfaces/default/transform/layouts/header.xsl	(revision 31536)
+++ main/trunk/greenstone3/web/interfaces/default/transform/layouts/header.xsl	(revision 31537)
@@ -409,4 +409,5 @@
     
   </xsl:template>
+
   <xsl:template name="generateLoginURL">
     <xsl:value-of select="$library_name"/>
@@ -431,4 +432,34 @@
     </xsl:for-each>
     
+  </xsl:template>
+
+  <!-- Writing the reverse of generateLoginURL since the toggleUserMenuScript does a lot more than I want. -->
+  <!-- https://www.w3schools.com/xml/xsl_functions.asp#string -->  
+  <xsl:template name="generateLogoutURL">
+    
+    <xsl:variable name="url" select="/page/pageRequest/@fullURL"/>
+    <xsl:variable name="tmpURL" select="substring-before($url, '&amp;amp;logout=')"/>
+    <xsl:variable name="beforeHash" select="substring-before($url, '#')"/>
+    <xsl:variable name="afterHash" select="substring-after($url, '#')"/>
+    <!-- Get rid of any lingering &amp;logout= already in the URL.
+	 Can't use fn:replace() as it's only been defined since XSLT 2.0. We use XSLT 1.x -->
+    <xsl:variable name="fullURL">
+      <xsl:choose>
+	<xsl:when test="$tmpURL != ''"><xsl:value-of select="$tmpURL" /></xsl:when>
+	<xsl:otherwise><xsl:value-of select="$url" /></xsl:otherwise>
+      </xsl:choose>
+    </xsl:variable>
+
+    <!-- Output the logout link: the current page's URL (with any lingering logout suffix removed) 
+	 followed by ?logout= or &amp;logout= followed by any # portion of the current page's URL -->
+    <xsl:choose>
+      <xsl:when test="$beforeHash != ''"><xsl:value-of select="$beforeHash" /></xsl:when>
+      <xsl:otherwise><xsl:value-of select="$fullURL" /></xsl:otherwise>
+    </xsl:choose>
+    <xsl:choose>
+      <xsl:when test="contains($fullURL, '?')"><xsl:text>&amp;logout=</xsl:text></xsl:when>
+      <xsl:otherwise>?logout=</xsl:otherwise>
+    </xsl:choose>
+    <xsl:if test="$afterHash != ''">#<xsl:value-of select="$afterHash" /></xsl:if>   
   </xsl:template>
   
Index: main/trunk/greenstone3/web/interfaces/default/transform/layouts/usercomments.xsl
===================================================================
--- main/trunk/greenstone3/web/interfaces/default/transform/layouts/usercomments.xsl	(revision 31537)
+++ main/trunk/greenstone3/web/interfaces/default/transform/layouts/usercomments.xsl	(revision 31537)
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<xsl:stylesheet version="1.0"
+	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+	xmlns:java="http://xml.apache.org/xslt/java"
+	xmlns:util="xalan://org.greenstone.gsdl3.util.XSLTUtil"
+	xmlns:gslib="http://www.greenstone.org/skinning"
+	xmlns:gsf="http://www.greenstone.org/greenstone3/schema/ConfigFormat"
+	extension-element-prefixes="java util"
+	exclude-result-prefixes="java util gsf">
+
+<xsl:template name="userCommentsSection">
+
+  <!-- 1. Make some variables available to javascript that the usercomments related js functions need -->
+  <gsf:variable name="d"><xsl:value-of select="/page/pageRequest/paramList/param[@name='d']/@value"/></gsf:variable>
+  <gsf:variable name="c"><xsl:value-of select="/page/pageRequest/paramList/param[@name='c']/@value"/></gsf:variable>
+  <gsf:variable name="site"><xsl:value-of select="/page/pageResponse/interfaceOptions/option[@name='site_name']/@value"/></gsf:variable>
+
+  <gsf:variable name="textusercommentssection"><xsl:value-of select="util:getInterfaceText($interface_name, /page/@lang, 'usercomments.heading')"/></gsf:variable>
+  <gsf:variable name="textisempty"><xsl:value-of select="util:getInterfaceText($interface_name, /page/@lang, 'usercomments.isempty')"/></gsf:variable>
+  <gsf:variable name="textcommentsubmitted"><xsl:value-of select="util:getInterfaceText($interface_name, /page/@lang, 'usercomments.submitted')"/></gsf:variable>
+
+  <!-- 2. Load the javascript, which will do stuff on window load/ready for which it needs the above gs.variables -->
+  <script type="text/javascript" src="interfaces/{$interface_name}/js/user_comments.js"><xsl:text> </xsl:text></script>
+
+  
+  <!-- 3. Set up the User comments section in the HTML -->
+  <div id="commentssection" class="centrediv">    
+    <div id="usercomments">
+      <!-- The div that loads the user comments that were already submitted -->
+      <div class="usercommentheading"><xsl:value-of select="util:getInterfaceText($interface_name, /page/@lang, 'usercomments.heading')"/></div>
+    </div>
+
+    <!-- If the user's logged in, show the comment form, else show the link to the login page -->
+    <xsl:choose>
+      <xsl:when test="/page/pageRequest/userInformation">	
+	<!-- Logged in, allow user to add a comment by displaying a form -->
+	<form name="AddUserCommentForm" id="usercommentform">
+	  <input type="hidden" name="username"><xsl:attribute name="value"><xsl:value-of select="/page/pageRequest/userInformation/@username"/></xsl:attribute></input>
+	  <div>
+	    <p><xsl:value-of select="util:getInterfaceText($interface_name, /page/@lang, 'usercomments.add')"/></p>
+	    <!-- The textarea will be added in by javascript into div#commentarea to avoid the problem of XML turning empty tags into self-closing ones and a self-closing text-area becomes invalid HTML -->
+	    <!--<textarea required="required" name="comment" rows="10" cols="64" placeholder="Add your comment here..."></textarea>-->
+	    <div id="commentarea">Comment area to appear here</div>
+	    <input type="hidden" name="d"><xsl:attribute name="value"><xsl:value-of select="/page/pageRequest/paramList/param[@name='d']/@value"/></xsl:attribute></input>
+	  </div>
+	  
+	  <input type="submit" onclick="addUserComment(document.AddUserCommentForm.username.value, document.AddUserCommentForm.comment.value, document.AddUserCommentForm.d.value, document); return false;"><xsl:attribute name="value"><xsl:value-of select="util:getInterfaceText($interface_name, /page/@lang, 'usercomments.submit')"/></xsl:attribute></input>
+	  <label id="usercommentfeedback"><xsl:comment>Text to prevent empty tags from becoming self-closing tags</xsl:comment></label>
+
+	  <div id="usercommentlogoutlink">
+	    <a><xsl:attribute name="href"><xsl:call-template name="generateLogoutURL"/></xsl:attribute>
+	    <xsl:value-of select="util:getInterfaceText($interface_name, /page/@lang, 'menu.logout')"/></a>
+	  </div>
+	</form>
+      </xsl:when>
+
+      <!-- User not logged in, "add comment" link allows user to login first -->
+      <xsl:otherwise>
+	<div id="usercommentlink">
+	  <a><xsl:attribute name="href"><xsl:call-template name="generateLoginURL"/></xsl:attribute>
+	  <xsl:value-of select="util:getInterfaceText($interface_name, /page/@lang, 'usercomments.add')"/></a>
+	</div>
+      </xsl:otherwise>
+    </xsl:choose>
+
+  </div>
+
+</xsl:template>
+
+</xsl:stylesheet>
Index: main/trunk/greenstone3/web/interfaces/default/transform/pages/document.xsl
===================================================================
--- main/trunk/greenstone3/web/interfaces/default/transform/pages/document.xsl	(revision 31536)
+++ main/trunk/greenstone3/web/interfaces/default/transform/pages/document.xsl	(revision 31537)
@@ -12,4 +12,7 @@
 	<xsl:import href="layouts/main.xsl"/>
 	<xsl:import href="layouts/toc.xsl"/>
+
+	<!-- templates for adding user comments -->
+	<xsl:import href="layouts/usercomments.xsl"/>
 	
 	<xsl:variable name="bookswitch">
@@ -367,4 +370,5 @@
 				<xsl:call-template name="documentHeading"/><br/>
 				<xsl:call-template name="documentContent"/>
+				<br /><xsl:call-template name="userCommentsSection"/>
 			</xsl:when>	
 			<xsl:otherwise> <!-- display the standard greenstone document -->
