
- Still experimental and incomplete, with no lock support and not much error handling Also: - New expiry date for sync functions - Attachment character set was being dropped during syncing - Possibly improves sizing issues with preferences window - Fixes problems with attachment filenames with extended characters - Fixes some problem with tags that I don't remember - Makes XMLHTTPRequest calls are now background requests (no auth windows or other prompts) - Z.U.HTTP.doOptions() now takes an nsIURI instead of a URL spec - New methods: - Zotero.Utilities.rand(min, max) - Zotero.Utilities.probability(x) - Zotero.Utilities.Base64.encode(str) and decode(str) - Zotero.getTempDirectory() - Zotero.Date.dateToISO(date) - convert JS Date object to ISO 8601 UTC date/time - Zotero.Date.isoToDate(isoDate) - convert an ISO 8601 UTC date/time to a JS Date object
1321 lines
33 KiB
JavaScript
1321 lines
33 KiB
JavaScript
/*
|
|
***** BEGIN LICENSE BLOCK *****
|
|
|
|
Copyright (c) 2006 Center for History and New Media
|
|
George Mason University, Fairfax, Virginia, USA
|
|
http://chnm.gmu.edu
|
|
|
|
Licensed under the Educational Community License, Version 1.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.opensource.org/licenses/ecl1.php
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
|
|
|
|
Utilities based in part on code taken from Piggy Bank 2.1.1 (BSD-licensed)
|
|
|
|
|
|
***** END LICENSE BLOCK *****
|
|
*/
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
//
|
|
// Zotero.Utilities
|
|
//
|
|
/////////////////////////////////////////////////////////////////
|
|
|
|
Zotero.Utilities = function () {}
|
|
|
|
/*
|
|
* See Zotero.Date
|
|
*/
|
|
Zotero.Utilities.prototype.formatDate = function(date) {
|
|
return Zotero.Date.formatDate(date);
|
|
}
|
|
Zotero.Utilities.prototype.strToDate = function(date) {
|
|
return Zotero.Date.strToDate(date);
|
|
}
|
|
Zotero.Utilities.prototype.strToISO = function(date) {
|
|
return Zotero.Date.strToISO(date);
|
|
}
|
|
|
|
/*
|
|
* Cleans extraneous punctuation off an author name
|
|
*/
|
|
Zotero.Utilities._allCapsRe = /^[A-Z]+$/;
|
|
Zotero.Utilities.prototype.cleanAuthor = function(author, type, useComma) {
|
|
if(typeof(author) != "string") {
|
|
throw "cleanAuthor: author must be a string";
|
|
}
|
|
|
|
author = author.replace(/^[\s\.\,\/\[\]\:]+/, '');
|
|
author = author.replace(/[\s\,\/\[\]\:\.]+$/, '');
|
|
author = author.replace(/ +/, ' ');
|
|
if(useComma) {
|
|
// Add spaces between periods
|
|
author = author.replace(/\.([^ ])/, ". $1");
|
|
|
|
var splitNames = author.split(/, ?/);
|
|
if(splitNames.length > 1) {
|
|
var lastName = splitNames[0];
|
|
var firstName = splitNames[1];
|
|
} else {
|
|
var lastName = author;
|
|
}
|
|
} else {
|
|
var spaceIndex = author.lastIndexOf(" ");
|
|
var lastName = author.substring(spaceIndex+1);
|
|
var firstName = author.substring(0, spaceIndex);
|
|
}
|
|
|
|
if(firstName && Zotero.Utilities._allCapsRe.test(firstName) &&
|
|
firstName.length < 4 &&
|
|
(firstName.length == 1 || lastName.toUpperCase() != lastName)) {
|
|
// first name is probably initials
|
|
var newFirstName = "";
|
|
for(var i=0; i<firstName.length; i++) {
|
|
newFirstName += " "+firstName[i]+".";
|
|
}
|
|
firstName = newFirstName.substr(1);
|
|
}
|
|
|
|
return {firstName:firstName, lastName:lastName, creatorType:type};
|
|
}
|
|
|
|
|
|
/*
|
|
* Removes leading and trailing whitespace from a string
|
|
*/
|
|
Zotero.Utilities.prototype.trim = function(s) {
|
|
if (typeof(s) != "string") {
|
|
throw "trim: argument must be a string";
|
|
}
|
|
|
|
s = s.replace(/^\s+/, "");
|
|
return s.replace(/\s+$/, "");
|
|
}
|
|
|
|
|
|
/*
|
|
* Cleans whitespace off a string and replaces multiple spaces with one
|
|
*/
|
|
Zotero.Utilities.prototype.trimInternal = function(s) {
|
|
if (typeof(s) != "string") {
|
|
throw "trimInternal: argument must be a string";
|
|
}
|
|
|
|
s = s.replace(/[\xA0\r\n\s]+/g, " ");
|
|
return this.trim(s);
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* Cleans whitespace off a string and replaces multiple spaces with one
|
|
*
|
|
* DEPRECATED: use trimInternal()
|
|
*/
|
|
Zotero.Utilities.prototype.cleanString = function(s) {
|
|
Zotero.debug("cleanString() is deprecated; use trimInternal() instead", 2);
|
|
return this.trimInternal(s);
|
|
}
|
|
|
|
/*
|
|
* Cleans any non-word non-parenthesis characters off the ends of a string
|
|
*/
|
|
Zotero.Utilities.prototype.superCleanString = function(x) {
|
|
if(typeof(x) != "string") {
|
|
throw "superCleanString: argument must be a string";
|
|
}
|
|
|
|
var x = x.replace(/^[\x00-\x27\x29-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/, "");
|
|
return x.replace(/[\x00-\x28\x2A-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+$/, "");
|
|
}
|
|
|
|
/*
|
|
* Eliminates HTML tags, replacing <br>s with /ns
|
|
*/
|
|
Zotero.Utilities.prototype.cleanTags = function(x) {
|
|
if(typeof(x) != "string") {
|
|
throw "cleanTags: argument must be a string";
|
|
}
|
|
|
|
x = x.replace(/<br[^>]*>/gi, "\n");
|
|
return x.replace(/<[^>]+>/g, "");
|
|
}
|
|
|
|
/*
|
|
* Encode special XML/HTML characters
|
|
*
|
|
* Certain entities can be inserted manually:
|
|
*
|
|
* <ZOTEROBREAK/> => <br/>
|
|
* <ZOTEROHELLIP/> => …
|
|
*/
|
|
Zotero.Utilities.prototype.htmlSpecialChars = function(str) {
|
|
if (typeof str != 'string') {
|
|
throw "Argument '" + str + "' must be a string in Zotero.Utilities.htmlSpecialChars()";
|
|
}
|
|
|
|
if (!str) {
|
|
return '';
|
|
}
|
|
|
|
var chars = ['&', '"',"'",'<','>'];
|
|
var entities = ['amp', 'quot', 'apos', 'lt', 'gt'];
|
|
|
|
var newString = str;
|
|
for (var i = 0; i < chars.length; i++) {
|
|
var re = new RegExp(chars[i], 'g');
|
|
newString = newString.replace(re, '&' + entities[i] + ';');
|
|
}
|
|
|
|
newString = newString.replace(/<ZOTERO([^\/]+)\/>/g, function (str, p1, offset, s) {
|
|
switch (p1) {
|
|
case 'BREAK':
|
|
return '<br/>';
|
|
case 'HELLIP':
|
|
return '…';
|
|
default:
|
|
return p1;
|
|
}
|
|
});
|
|
|
|
return newString;
|
|
}
|
|
|
|
|
|
Zotero.Utilities.prototype.unescapeHTML = function(str) {
|
|
var nsISUHTML = Components.classes["@mozilla.org/feed-unescapehtml;1"]
|
|
.getService(Components.interfaces.nsIScriptableUnescapeHTML);
|
|
return nsISUHTML.unescape(str);
|
|
}
|
|
|
|
|
|
/*
|
|
* Parses a text string for HTML/XUL markup and returns an array of parts
|
|
*
|
|
* Currently only finds HTML links (<a> tags)
|
|
*
|
|
* Returns an array of objects with the following form:
|
|
* {
|
|
* type: 'text'|'link',
|
|
* text: "text content",
|
|
* [ attributes: { key1: val [ , key2: val, ...] }
|
|
* }
|
|
*/
|
|
Zotero.Utilities.prototype.parseMarkup = function(str) {
|
|
var parts = [];
|
|
var splits = str.split(/(<a [^>]+>[^<]*<\/a>)/);
|
|
|
|
for each(var split in splits) {
|
|
// Link
|
|
if (split.indexOf('<a ') == 0) {
|
|
var matches = split.match(/<a ([^>]+)>([^<]*)<\/a>/);
|
|
if (matches) {
|
|
// Attribute pairs
|
|
var attributes = {};
|
|
var pairs = matches[1].match(/([^ =]+)="([^"]+")/g);
|
|
for each (var pair in pairs) {
|
|
var [key, val] = pair.split(/=/);
|
|
attributes[key] = val.substr(1, val.length - 2);
|
|
}
|
|
|
|
parts.push({
|
|
type: 'link',
|
|
text: matches[2],
|
|
attributes: attributes
|
|
});
|
|
continue;
|
|
}
|
|
}
|
|
|
|
parts.push({
|
|
type: 'text',
|
|
text: split
|
|
});
|
|
}
|
|
|
|
return parts;
|
|
}
|
|
|
|
|
|
Zotero.Utilities.prototype.min3 = function (a, b, c) {
|
|
var min = a;
|
|
if (b < min) {
|
|
min = b;
|
|
}
|
|
if (c < min) {
|
|
min = c;
|
|
}
|
|
return min;
|
|
}
|
|
|
|
|
|
Zotero.Utilities.prototype.levenshtein = function (a, b) {
|
|
var aLen = a.length;
|
|
var bLen = b.length;
|
|
|
|
var arr = new Array(aLen+1);
|
|
var i, j, cost;
|
|
|
|
for (i = 0; i <= aLen; i++) {
|
|
arr[i] = new Array(bLen);
|
|
arr[i][0] = i;
|
|
}
|
|
|
|
for (j = 0; j <= bLen; j++) {
|
|
arr[0][j] = j;
|
|
}
|
|
|
|
for (i = 1; i <= aLen; i++) {
|
|
for (j = 1; j <= bLen; j++) {
|
|
cost = (a[i-1] == b[j-1]) ? 0 : 1;
|
|
arr[i][j] = this.min3(arr[i-1][j] + 1, arr[i][j-1] + 1, arr[i-1][j-1] + cost);
|
|
}
|
|
}
|
|
|
|
return arr[aLen][bLen];
|
|
}
|
|
|
|
|
|
/*
|
|
* Test if a string is an integer
|
|
*/
|
|
Zotero.Utilities.prototype.isInt = function(x) {
|
|
if(parseInt(x) == x) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Generate a random integer between min and max inclusive
|
|
*
|
|
* @param {Integer} min
|
|
* @param {Integer} max
|
|
* @return {Integer}
|
|
*/
|
|
Zotero.Utilities.prototype.rand = function (min, max) {
|
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
}
|
|
|
|
|
|
/**
|
|
* Return true according to a given probability
|
|
*
|
|
* @param {Integer} x Will return true every x times on average
|
|
* @return {Boolean} On average, TRUE every x times
|
|
* the function is called
|
|
*/
|
|
Zotero.Utilities.prototype.probability = function (x) {
|
|
return this.rand(1, x) == this.rand(1, x);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Determine the necessary data type for SQLite parameter binding
|
|
*
|
|
* @return int 0 for string, 32 for int32, 64 for int64
|
|
*/
|
|
Zotero.Utilities.prototype.getSQLDataType = function(value) {
|
|
var strVal = value + '';
|
|
if (strVal.match(/^[1-9]+[0-9]*$/)) {
|
|
// These upper bounds also specified in Zotero.DB
|
|
//
|
|
// Store as 32-bit signed integer
|
|
if (value <= 2147483647) {
|
|
return 32;
|
|
}
|
|
// Store as 64-bit signed integer
|
|
// 2^53 is JS's upper-bound for decimal integers
|
|
else if (value < 9007199254740992) {
|
|
return 64;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
/*
|
|
* From http://developer.mozilla.org/en/docs/nsICryptoHash#Computing_the_Hash_of_a_String
|
|
*/
|
|
Zotero.Utilities.prototype.md5 = function(str) {
|
|
var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
|
|
createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
|
|
converter.charset = "UTF-8";
|
|
var result = {};
|
|
var data = converter.convertToByteArray(str, result);
|
|
var ch = Components.classes["@mozilla.org/security/hash;1"]
|
|
.createInstance(Components.interfaces.nsICryptoHash);
|
|
ch.init(ch.MD5);
|
|
ch.update(data, data.length);
|
|
var hash = ch.finish(false);
|
|
|
|
// return the two-digit hexadecimal code for a byte
|
|
function toHexString(charCode) {
|
|
return ("0" + charCode.toString(16)).slice(-2);
|
|
}
|
|
|
|
// convert the binary hash data to a hex string.
|
|
return [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
|
|
}
|
|
|
|
|
|
/*
|
|
* Get current zotero version
|
|
*/
|
|
Zotero.Utilities.prototype.getVersion = function() {
|
|
return Zotero.version;
|
|
}
|
|
|
|
/*
|
|
* Get a page range, given a user-entered set of pages
|
|
*/
|
|
Zotero.Utilities.prototype._pageRangeRegexp = /^\s*([0-9]+)-([0-9]+)\s*$/;
|
|
Zotero.Utilities.prototype.getPageRange = function(pages) {
|
|
var pageNumbers;
|
|
var m = this._pageRangeRegexp.exec(pages);
|
|
if(m) {
|
|
// A page range
|
|
pageNumbers = [m[1], m[2]];
|
|
} else {
|
|
// Assume start and end are the same
|
|
pageNumbers = [pages, pages];
|
|
}
|
|
return pageNumbers;
|
|
}
|
|
|
|
/*
|
|
* provide inArray function
|
|
*/
|
|
Zotero.Utilities.prototype.inArray = Zotero.inArray;
|
|
|
|
/*
|
|
* pads a number or other string with a given string on the left
|
|
*/
|
|
Zotero.Utilities.prototype.lpad = function(string, pad, length) {
|
|
string = string ? string + '' : '';
|
|
while(string.length < length) {
|
|
string = pad + string;
|
|
}
|
|
return string;
|
|
}
|
|
|
|
/*
|
|
* returns true if an item type exists, false if it does not
|
|
*/
|
|
Zotero.Utilities.prototype.itemTypeExists = function(type) {
|
|
if(Zotero.ItemTypes.getID(type)) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* returns an array of all (string) creatorTypes valid for a (string) itemType
|
|
*/
|
|
Zotero.Utilities.prototype.getCreatorsForType = function(type) {
|
|
var types = Zotero.CreatorTypes.getTypesForItemType(Zotero.ItemTypes.getID(type));
|
|
var cleanTypes = new Array();
|
|
for each(var type in types) {
|
|
cleanTypes.push(type.name);
|
|
}
|
|
return cleanTypes;
|
|
}
|
|
|
|
/*
|
|
* returns a localized creatorType name
|
|
*/
|
|
Zotero.Utilities.prototype.getLocalizedCreatorType = function(type) {
|
|
try {
|
|
return Zotero.getString("creatorTypes."+type);
|
|
} catch(e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Cleans a title, capitalizing the proper words and replacing " :" with ":"
|
|
*
|
|
* Follows capitalizeTitles pref, unless |force| is true
|
|
*/
|
|
Zotero.Utilities.prototype.capitalizeTitle = function(string, force) {
|
|
string = this.trimInternal(string);
|
|
if(Zotero.Prefs.get('capitalizeTitles') || force) {
|
|
// fix colons
|
|
string = string.replace(" : ", ": ", "g");
|
|
string = Zotero.Text.titleCase(string.replace(/ : /g, ": "));
|
|
}
|
|
return string;
|
|
}
|
|
|
|
/*
|
|
* END ZOTERO FOR FIREFOX EXTENSIONS
|
|
*/
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
//
|
|
// Zotero.Utilities.Ingester
|
|
//
|
|
/////////////////////////////////////////////////////////////////
|
|
// Zotero.Utilities.Ingester extends Zotero.Utilities, offering additional
|
|
// classes relating to data extraction specifically from HTML documents.
|
|
|
|
Zotero.Utilities.Ingester = function(translate, proxiedURL) {
|
|
this.translate = translate;
|
|
}
|
|
|
|
Zotero.Utilities.Ingester.prototype = new Zotero.Utilities();
|
|
|
|
// Takes an XPath query and returns the results
|
|
Zotero.Utilities.Ingester.prototype.gatherElementsOnXPath = function(doc, parentNode, xpath, nsResolver) {
|
|
var elmts = [];
|
|
|
|
var iterator = doc.evaluate(xpath, parentNode, nsResolver, Components.interfaces.nsIDOMXPathResult.ANY_TYPE,null);
|
|
var elmt = iterator.iterateNext();
|
|
var i = 0;
|
|
while (elmt) {
|
|
elmts[i++] = elmt;
|
|
elmt = iterator.iterateNext();
|
|
}
|
|
return elmts;
|
|
}
|
|
|
|
/*
|
|
* Gets a given node as a string containing all child nodes
|
|
*
|
|
* WARNING: This is DEPRECATED and may be removed in the final release. Use
|
|
* doc.evaluate and the "nodeValue" or "textContent" property
|
|
*/
|
|
Zotero.Utilities.Ingester.prototype.getNodeString = function(doc, contextNode, xpath, nsResolver) {
|
|
var elmts = this.gatherElementsOnXPath(doc, contextNode, xpath, nsResolver);
|
|
var returnVar = "";
|
|
for(var i=0; i<elmts.length; i++) {
|
|
returnVar += elmts[i].nodeValue;
|
|
}
|
|
return returnVar;
|
|
}
|
|
|
|
/*
|
|
* Grabs items based on URLs
|
|
*/
|
|
Zotero.Utilities.Ingester.prototype.getItemArray = function(doc, inHere, urlRe, rejectRe) {
|
|
var availableItems = new Object(); // Technically, associative arrays are objects
|
|
|
|
// Require link to match this
|
|
if(urlRe) {
|
|
if(urlRe.exec) {
|
|
var urlRegexp = urlRe;
|
|
} else {
|
|
var urlRegexp = new RegExp();
|
|
urlRegexp.compile(urlRe, "i");
|
|
}
|
|
}
|
|
// Do not allow text to match this
|
|
if(rejectRe) {
|
|
if(rejectRe.exec) {
|
|
var rejectRegexp = rejectRe;
|
|
} else {
|
|
var rejectRegexp = new RegExp();
|
|
rejectRegexp.compile(rejectRe, "i");
|
|
}
|
|
}
|
|
|
|
if(!inHere.length) {
|
|
inHere = new Array(inHere);
|
|
}
|
|
|
|
for(var j=0; j<inHere.length; j++) {
|
|
var links = inHere[j].getElementsByTagName("a");
|
|
for(var i=0; i<links.length; i++) {
|
|
if(!urlRe || urlRegexp.test(links[i].href)) {
|
|
var text = links[i].textContent;
|
|
if(text) {
|
|
text = this.trimInternal(text);
|
|
if(!rejectRe || !rejectRegexp.test(text)) {
|
|
if(availableItems[links[i].href]) {
|
|
if(text != availableItems[links[i].href]) {
|
|
availableItems[links[i].href] += " "+text;
|
|
}
|
|
} else {
|
|
availableItems[links[i].href] = text;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return availableItems;
|
|
}
|
|
|
|
Zotero.Utilities.Ingester.prototype.lookupContextObject = function(co, done, error) {
|
|
return Zotero.OpenURL.lookupContextObject(co, done, error);
|
|
}
|
|
|
|
Zotero.Utilities.Ingester.prototype.parseContextObject = function(co, item) {
|
|
return Zotero.OpenURL.parseContextObject(co, item);
|
|
}
|
|
|
|
|
|
// Ingester adapters for Zotero.Utilities.HTTP to handle proxies
|
|
|
|
Zotero.Utilities.Ingester.prototype.loadDocument = function(url, succeeded, failed) {
|
|
this.processDocuments([ url ], succeeded, null, failed);
|
|
}
|
|
|
|
Zotero.Utilities.Ingester._protocolRe = new RegExp();
|
|
Zotero.Utilities.Ingester._protocolRe.compile("^(?:(?:http|https|ftp):|[^:](?:/.*)?$)", "i");
|
|
Zotero.Utilities.Ingester.prototype.processDocuments = function(urls, processor, done, exception) {
|
|
if(this.translate.locationIsProxied) {
|
|
for(var i in urls) {
|
|
if(this.translate.locationIsProxied) {
|
|
urls[i] = Zotero.Proxies.properToProxy(urls[i]);
|
|
}
|
|
// check for a protocol colon
|
|
if(!Zotero.Utilities.Ingester._protocolRe.test(urls[i])) {
|
|
throw("invalid URL in processDocuments");
|
|
}
|
|
}
|
|
}
|
|
|
|
// unless the translator has proposed some way to handle an error, handle it
|
|
// by throwing a "scraping error" message
|
|
if(!exception) {
|
|
var translate = this.translate;
|
|
exception = function(e) {
|
|
translate.error(false, e);
|
|
}
|
|
}
|
|
|
|
Zotero.Utilities.HTTP.processDocuments(null, urls, processor, done, exception);
|
|
}
|
|
|
|
Zotero.Utilities.Ingester.HTTP = function(translate) {
|
|
this.translate = translate;
|
|
}
|
|
|
|
Zotero.Utilities.Ingester.HTTP.prototype.doGet = function(urls, processor, done, responseCharset) {
|
|
var callAgain = false;
|
|
|
|
if(typeof(urls) == "string") {
|
|
var url = urls;
|
|
} else {
|
|
if(urls.length > 1) callAgain = true;
|
|
var url = urls.shift();
|
|
}
|
|
|
|
if(this.translate.locationIsProxied) {
|
|
url = Zotero.Proxies.properToProxy(url);
|
|
}
|
|
if(!Zotero.Utilities.Ingester._protocolRe.test(url)) {
|
|
throw("invalid URL in processDocuments");
|
|
}
|
|
|
|
var me = this;
|
|
|
|
Zotero.Utilities.HTTP.doGet(url, function(xmlhttp) {
|
|
try {
|
|
if(processor) {
|
|
processor(xmlhttp.responseText, xmlhttp, url);
|
|
}
|
|
|
|
if(callAgain) {
|
|
me.doGet(urls, processor, done);
|
|
} else {
|
|
if(done) {
|
|
done();
|
|
}
|
|
}
|
|
} catch(e) {
|
|
me.translate.error(false, e);
|
|
}
|
|
}, responseCharset);
|
|
}
|
|
|
|
Zotero.Utilities.Ingester.HTTP.prototype.doPost = function(url, body, onDone, requestContentType, responseCharset) {
|
|
if(this.translate.locationIsProxied) {
|
|
url = Zotero.Proxies.properToProxy(url);
|
|
}
|
|
if(!Zotero.Utilities.Ingester._protocolRe.test(url)) {
|
|
throw("invalid URL in processDocuments");
|
|
}
|
|
|
|
var translate = this.translate;
|
|
Zotero.Utilities.HTTP.doPost(url, body, function(xmlhttp) {
|
|
try {
|
|
onDone(xmlhttp.responseText, xmlhttp);
|
|
} catch(e) {
|
|
translate.error(false, e);
|
|
}
|
|
}, requestContentType, responseCharset);
|
|
}
|
|
|
|
// These are front ends for XMLHttpRequest. XMLHttpRequest can't actually be
|
|
// accessed outside the sandbox, and even if it could, it wouldn't let scripts
|
|
// access across domains, so everything's replicated here.
|
|
Zotero.Utilities.HTTP = new function() {
|
|
this.doGet = doGet;
|
|
this.doPost = doPost;
|
|
this.doHead = doHead;
|
|
this.browserIsOffline = browserIsOffline;
|
|
|
|
this.WebDAV = {};
|
|
|
|
/**
|
|
* Send an HTTP GET request via XMLHTTPRequest
|
|
*
|
|
* Returns false if browser is offline
|
|
*
|
|
* doGet can be called as:
|
|
* Zotero.Utilities.HTTP.doGet(url, onDone)
|
|
*
|
|
* Returns the XMLHTTPRequest object
|
|
**/
|
|
function doGet(url, onDone, responseCharset) {
|
|
Zotero.debug("HTTP GET "+url);
|
|
if (this.browserIsOffline()){
|
|
return false;
|
|
}
|
|
|
|
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
|
.createInstance();
|
|
// Prevent certificate/authentication dialogs from popping up
|
|
xmlhttp.mozBackgroundRequest = true;
|
|
xmlhttp.open('GET', url, true);
|
|
|
|
xmlhttp.onreadystatechange = function(){
|
|
_stateChange(xmlhttp, onDone, responseCharset);
|
|
};
|
|
|
|
// Temporarily set cookieBehavior to 0 for Firefox 3
|
|
// https://www.zotero.org/trac/ticket/1070
|
|
try {
|
|
var prefService = Components.classes["@mozilla.org/preferences-service;1"].
|
|
getService(Components.interfaces.nsIPrefBranch);
|
|
var cookieBehavior = prefService.getIntPref("network.cookie.cookieBehavior");
|
|
prefService.setIntPref("network.cookie.cookieBehavior", 0);
|
|
|
|
xmlhttp.send(null);
|
|
}
|
|
finally {
|
|
prefService.setIntPref("network.cookie.cookieBehavior", cookieBehavior);
|
|
}
|
|
|
|
return xmlhttp;
|
|
}
|
|
|
|
|
|
/**
|
|
* Send an HTTP POST request via XMLHTTPRequest
|
|
*
|
|
* Returns false if browser is offline
|
|
*
|
|
* doPost can be called as:
|
|
* Zotero.Utilities.HTTP.doPost(url, body, onDone)
|
|
*
|
|
* Returns the XMLHTTPRequest object
|
|
**/
|
|
function doPost(url, body, onDone, requestContentType, responseCharset) {
|
|
var bodyStart = body.substr(0, 1024);
|
|
// Don't display password in console
|
|
bodyStart = bodyStart.replace(/password=[^&]+/, 'password=********');
|
|
|
|
Zotero.debug("HTTP POST "
|
|
+ (body.length > 1024 ?
|
|
bodyStart + '... (' + body.length + ' chars)' : bodyStart)
|
|
+ " to " + url);
|
|
|
|
if (this.browserIsOffline()){
|
|
return false;
|
|
}
|
|
|
|
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
|
.createInstance();
|
|
// Prevent certificate/authentication dialogs from popping up
|
|
xmlhttp.mozBackgroundRequest = true;
|
|
xmlhttp.open('POST', url, true);
|
|
xmlhttp.setRequestHeader("Content-Type", (requestContentType ? requestContentType : "application/x-www-form-urlencoded" ));
|
|
|
|
xmlhttp.onreadystatechange = function(){
|
|
_stateChange(xmlhttp, onDone, responseCharset);
|
|
};
|
|
|
|
// Temporarily set cookieBehavior to 0 for Firefox 3
|
|
// https://www.zotero.org/trac/ticket/1070
|
|
try {
|
|
var prefService = Components.classes["@mozilla.org/preferences-service;1"].
|
|
getService(Components.interfaces.nsIPrefBranch);
|
|
var cookieBehavior = prefService.getIntPref("network.cookie.cookieBehavior");
|
|
prefService.setIntPref("network.cookie.cookieBehavior", 0);
|
|
|
|
xmlhttp.send(body);
|
|
}
|
|
finally {
|
|
prefService.setIntPref("network.cookie.cookieBehavior", cookieBehavior);
|
|
}
|
|
|
|
return xmlhttp;
|
|
}
|
|
|
|
|
|
function doHead(url, onDone) {
|
|
Zotero.debug("HTTP HEAD "+url);
|
|
if (this.browserIsOffline()){
|
|
return false;
|
|
}
|
|
|
|
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
|
.createInstance();
|
|
// Prevent certificate/authentication dialogs from popping up
|
|
xmlhttp.mozBackgroundRequest = true;
|
|
xmlhttp.open('HEAD', url, true);
|
|
|
|
xmlhttp.onreadystatechange = function(){
|
|
_stateChange(xmlhttp, onDone);
|
|
};
|
|
|
|
// Temporarily set cookieBehavior to 0 for Firefox 3
|
|
// https://www.zotero.org/trac/ticket/1070
|
|
try {
|
|
var prefService = Components.classes["@mozilla.org/preferences-service;1"].
|
|
getService(Components.interfaces.nsIPrefBranch);
|
|
var cookieBehavior = prefService.getIntPref("network.cookie.cookieBehavior");
|
|
prefService.setIntPref("network.cookie.cookieBehavior", 0);
|
|
|
|
xmlhttp.send(null);
|
|
}
|
|
finally {
|
|
prefService.setIntPref("network.cookie.cookieBehavior", cookieBehavior);
|
|
}
|
|
|
|
return xmlhttp;
|
|
}
|
|
|
|
|
|
/**
|
|
* Send an HTTP OPTIONS request via XMLHTTPRequest
|
|
*
|
|
* @param {nsIURI} url
|
|
* @param {Function} onDone
|
|
* @return {XMLHTTPRequest}
|
|
*/
|
|
this.doOptions = function (uri, callback) {
|
|
// Don't display password in console
|
|
var disp = uri.clone();
|
|
disp.password = "********";
|
|
Zotero.debug("HTTP OPTIONS to " + disp.spec);
|
|
|
|
if (Zotero.Utilities.HTTP.browserIsOffline()){
|
|
return false;
|
|
}
|
|
|
|
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
|
.createInstance();
|
|
// Prevent certificate/authentication dialogs from popping up
|
|
xmlhttp.mozBackgroundRequest = true;
|
|
xmlhttp.open('OPTIONS', uri.spec, true);
|
|
|
|
xmlhttp.onreadystatechange = function() {
|
|
_stateChange(xmlhttp, callback);
|
|
};
|
|
|
|
// Temporarily set cookieBehavior to 0 for Firefox 3
|
|
// https://www.zotero.org/trac/ticket/1070
|
|
try {
|
|
var prefService = Components.classes["@mozilla.org/preferences-service;1"].
|
|
getService(Components.interfaces.nsIPrefBranch);
|
|
var cookieBehavior = prefService.getIntPref("network.cookie.cookieBehavior");
|
|
prefService.setIntPref("network.cookie.cookieBehavior", 0);
|
|
|
|
xmlhttp.send(null);
|
|
}
|
|
finally {
|
|
prefService.setIntPref("network.cookie.cookieBehavior", cookieBehavior);
|
|
}
|
|
|
|
return xmlhttp;
|
|
}
|
|
|
|
|
|
//
|
|
// WebDAV methods
|
|
//
|
|
|
|
|
|
/**
|
|
* Send a WebDAV PROP* request via XMLHTTPRequest
|
|
*
|
|
* Returns false if browser is offline
|
|
*
|
|
* @param {String} method PROPFIND or PROPPATCH
|
|
* @param {nsIURI} uri
|
|
* @param {String} body XML string
|
|
* @param {Function} callback
|
|
* @param {Object} requestHeaders e.g. { Depth: 0 }
|
|
*/
|
|
this.WebDAV.doProp = function (method, uri, body, callback, requestHeaders) {
|
|
switch (method) {
|
|
case 'PROPFIND':
|
|
case 'PROPPATCH':
|
|
break;
|
|
|
|
default:
|
|
throw ("Invalid method '" + method
|
|
+ "' in Zotero.Utilities.HTTP.doProp");
|
|
}
|
|
|
|
if (requestHeaders && requestHeaders.depth != undefined) {
|
|
var depth = requestHeaders.depth;
|
|
}
|
|
|
|
// Don't display password in console
|
|
var disp = uri.clone();
|
|
disp.password = "********";
|
|
|
|
var bodyStart = body.substr(0, 1024);
|
|
Zotero.debug("HTTP " + method + " "
|
|
+ (depth != undefined ? "(depth " + depth + ") " : "")
|
|
+ (body.length > 1024 ?
|
|
bodyStart + "... (" + body.length + " chars)" : bodyStart)
|
|
+ " to " + disp.spec);
|
|
|
|
if (Zotero.Utilities.HTTP.browserIsOffline()) {
|
|
Zotero.debug("Browser is offline", 2);
|
|
return false;
|
|
}
|
|
|
|
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
|
.createInstance();
|
|
// Prevent certificate/authentication dialogs from popping up
|
|
xmlhttp.mozBackgroundRequest = true;
|
|
xmlhttp.open(method, uri.spec, true);
|
|
|
|
if (requestHeaders) {
|
|
for (var header in requestHeaders) {
|
|
xmlhttp.setRequestHeader(header, requestHeaders[header]);
|
|
}
|
|
}
|
|
|
|
xmlhttp.setRequestHeader("Content-Type", 'text/xml; charset="utf-8"');
|
|
|
|
xmlhttp.onreadystatechange = function() {
|
|
_stateChange(xmlhttp, callback);
|
|
};
|
|
|
|
xmlhttp.send(body);
|
|
|
|
return xmlhttp;
|
|
}
|
|
|
|
|
|
/**
|
|
* Send a WebDAV MKCOL request via XMLHTTPRequest
|
|
*
|
|
* @param {nsIURI} url
|
|
* @param {Function} onDone
|
|
* @return {XMLHTTPRequest}
|
|
*/
|
|
this.WebDAV.doMkCol = function (uri, callback) {
|
|
// Don't display password in console
|
|
var disp = uri.clone();
|
|
disp.password = "********";
|
|
Zotero.debug("HTTP MKCOL to " + disp.spec);
|
|
|
|
if (Zotero.Utilities.HTTP.browserIsOffline()) {
|
|
return false;
|
|
}
|
|
|
|
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
|
.createInstance();
|
|
// Prevent certificate/authentication dialogs from popping up
|
|
xmlhttp.mozBackgroundRequest = true;
|
|
xmlhttp.open('MKCOL', uri.spec, true);
|
|
xmlhttp.onreadystatechange = function() {
|
|
_stateChange(xmlhttp, callback);
|
|
};
|
|
xmlhttp.send(null);
|
|
return xmlhttp;
|
|
}
|
|
|
|
|
|
/**
|
|
* Send a WebDAV PUT request via XMLHTTPRequest
|
|
*
|
|
* @param {nsIURI} url
|
|
* @param {String} body String body to PUT
|
|
* @param {Function} onDone
|
|
* @return {XMLHTTPRequest}
|
|
*/
|
|
this.WebDAV.doPut = function (uri, body, callback) {
|
|
// Don't display password in console
|
|
var disp = uri.clone();
|
|
disp.password = "********";
|
|
|
|
var bodyStart = "'" + body.substr(0, 1024) + "'";
|
|
Zotero.debug("HTTP PUT "
|
|
+ (body.length > 1024 ?
|
|
bodyStart + "... (" + body.length + " chars)" : bodyStart)
|
|
+ " to " + disp.spec);
|
|
|
|
if (Zotero.Utilities.HTTP.browserIsOffline()) {
|
|
return false;
|
|
}
|
|
|
|
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
|
.createInstance();
|
|
// Prevent certificate/authentication dialogs from popping up
|
|
xmlhttp.mozBackgroundRequest = true;
|
|
xmlhttp.open("PUT", uri.spec, true);
|
|
xmlhttp.onreadystatechange = function() {
|
|
_stateChange(xmlhttp, callback);
|
|
};
|
|
xmlhttp.send(body);
|
|
return xmlhttp;
|
|
}
|
|
|
|
|
|
/**
|
|
* Send a WebDAV PUT request via XMLHTTPRequest
|
|
*
|
|
* @param {nsIURI} url
|
|
* @param {Function} onDone
|
|
* @return {XMLHTTPRequest}
|
|
*/
|
|
this.WebDAV.doDelete = function (uri, callback) {
|
|
// Don't display password in console
|
|
var disp = uri.clone();
|
|
disp.password = "********";
|
|
|
|
Zotero.debug("WebDAV DELETE to " + disp.spec);
|
|
|
|
if (Zotero.Utilities.HTTP.browserIsOffline()) {
|
|
return false;
|
|
}
|
|
|
|
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
|
.createInstance();
|
|
// Prevent certificate/authentication dialogs from popping up
|
|
xmlhttp.mozBackgroundRequest = true;
|
|
xmlhttp.open("DELETE", uri.spec, true);
|
|
xmlhttp.onreadystatechange = function() {
|
|
_stateChange(xmlhttp, callback);
|
|
};
|
|
xmlhttp.send(null);
|
|
return xmlhttp;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the Authorization header used by a channel
|
|
*
|
|
* As of Firefox 3.0.1 subsequent requests to higher-level directories
|
|
* seem not to authenticate properly and just return 401s, so this
|
|
* can be used to manually include the Authorization header in a request
|
|
*
|
|
* It can also be used to check whether a request was forced to
|
|
* use authentication
|
|
*
|
|
* @param {nsIChannel} channel
|
|
* @return {String|FALSE} Authorization header, or FALSE if none
|
|
*/
|
|
this.getChannelAuthorization = function (channel) {
|
|
try {
|
|
channel.QueryInterface(Components.interfaces.nsIHttpChannel);
|
|
var authHeader = channel.getRequestHeader("Authorization");
|
|
return authHeader;
|
|
}
|
|
catch (e) {
|
|
Zotero.debug(e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
function browserIsOffline() {
|
|
return Components.classes["@mozilla.org/network/io-service;1"]
|
|
.getService(Components.interfaces.nsIIOService).offline;
|
|
}
|
|
|
|
|
|
function _stateChange(xmlhttp, callback, responseCharset, data) {
|
|
switch (xmlhttp.readyState){
|
|
// Request not yet made
|
|
case 1:
|
|
break;
|
|
|
|
case 2:
|
|
break;
|
|
|
|
// Called multiple times while downloading in progress
|
|
case 3:
|
|
break;
|
|
|
|
// Download complete
|
|
case 4:
|
|
if (callback) {
|
|
// Override the content charset
|
|
if (responseCharset) {
|
|
xmlhttp.channel.contentCharset = responseCharset;
|
|
}
|
|
callback(xmlhttp, data);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Downloads and processes documents with processor()
|
|
// firstDoc - the first document to process with the processor (if null,
|
|
// first document is processed without processor)
|
|
// urls - an array of URLs to load
|
|
// processor - a function to execute to process each document
|
|
// done - a function to execute when all document processing is complete
|
|
// exception - a function to execute if an exception occurs (exceptions are
|
|
// also logged in the Zotero for Firefox log)
|
|
// saveBrowser - whether to save the hidden browser object; usually, you don't
|
|
// want to do this, because it makes it easier to leak memory
|
|
Zotero.Utilities.HTTP.processDocuments = function(firstDoc, urls, processor, done, exception, saveBrowser) {
|
|
var hiddenBrowser = Zotero.Browser.createHiddenBrowser();
|
|
hiddenBrowser.docShell.allowImages = false;
|
|
var prevUrl, url;
|
|
|
|
if (urls.length == 0) {
|
|
if(firstDoc) {
|
|
processor(firstDoc, done);
|
|
} else {
|
|
done();
|
|
}
|
|
return;
|
|
}
|
|
var urlIndex = -1;
|
|
|
|
var removeListeners = function() {
|
|
hiddenBrowser.removeEventListener("pageshow", onLoad, true);
|
|
if(!saveBrowser) {
|
|
Zotero.Browser.deleteHiddenBrowser(hiddenBrowser);
|
|
}
|
|
}
|
|
var doLoad = function() {
|
|
urlIndex++;
|
|
if (urlIndex < urls.length) {
|
|
url = urls[urlIndex];
|
|
try {
|
|
Zotero.debug("loading "+url);
|
|
hiddenBrowser.loadURI(url);
|
|
} catch (e) {
|
|
removeListeners();
|
|
if(exception) {
|
|
exception(e);
|
|
return;
|
|
} else {
|
|
throw(e);
|
|
}
|
|
}
|
|
} else {
|
|
removeListeners();
|
|
if(done) {
|
|
done();
|
|
}
|
|
}
|
|
};
|
|
var onLoad = function() {
|
|
Zotero.debug(hiddenBrowser.contentDocument.location.href+" has been loaded");
|
|
if(hiddenBrowser.contentDocument.location.href != prevUrl) { // Just in case it fires too many times
|
|
prevUrl = hiddenBrowser.contentDocument.location.href;
|
|
try {
|
|
processor(hiddenBrowser.contentDocument);
|
|
} catch (e) {
|
|
removeListeners();
|
|
if(exception) {
|
|
exception(e);
|
|
return;
|
|
} else {
|
|
throw(e);
|
|
}
|
|
}
|
|
doLoad();
|
|
}
|
|
};
|
|
var init = function() {
|
|
hiddenBrowser.addEventListener("pageshow", onLoad, true);
|
|
|
|
if (firstDoc) {
|
|
processor(firstDoc, doLoad);
|
|
} else {
|
|
doLoad();
|
|
}
|
|
}
|
|
|
|
init();
|
|
}
|
|
|
|
|
|
/*
|
|
* This would probably be better as a separate XPCOM service
|
|
*/
|
|
Zotero.Utilities.AutoComplete = new function(){
|
|
this.getResultComment = getResultComment;
|
|
|
|
function getResultComment(textbox){
|
|
var controller = textbox.controller;
|
|
|
|
for (var i=0; i<controller.matchCount; i++)
|
|
{
|
|
if (controller.getValueAt(i) == textbox.value)
|
|
{
|
|
return controller.getCommentAt(i);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Base64 encode / decode
|
|
* From http://www.webtoolkit.info/
|
|
*/
|
|
Zotero.Utilities.Base64 = {
|
|
// private property
|
|
_keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
|
|
|
|
// public method for encoding
|
|
encode : function (input) {
|
|
var output = "";
|
|
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
|
|
var i = 0;
|
|
|
|
input = this._utf8_encode(input);
|
|
|
|
while (i < input.length) {
|
|
|
|
chr1 = input.charCodeAt(i++);
|
|
chr2 = input.charCodeAt(i++);
|
|
chr3 = input.charCodeAt(i++);
|
|
|
|
enc1 = chr1 >> 2;
|
|
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
|
|
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
|
|
enc4 = chr3 & 63;
|
|
|
|
if (isNaN(chr2)) {
|
|
enc3 = enc4 = 64;
|
|
} else if (isNaN(chr3)) {
|
|
enc4 = 64;
|
|
}
|
|
|
|
output = output +
|
|
this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
|
|
this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
|
|
|
|
}
|
|
|
|
return output;
|
|
},
|
|
|
|
// public method for decoding
|
|
decode : function (input) {
|
|
var output = "";
|
|
var chr1, chr2, chr3;
|
|
var enc1, enc2, enc3, enc4;
|
|
var i = 0;
|
|
|
|
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
|
|
|
|
while (i < input.length) {
|
|
|
|
enc1 = this._keyStr.indexOf(input.charAt(i++));
|
|
enc2 = this._keyStr.indexOf(input.charAt(i++));
|
|
enc3 = this._keyStr.indexOf(input.charAt(i++));
|
|
enc4 = this._keyStr.indexOf(input.charAt(i++));
|
|
|
|
chr1 = (enc1 << 2) | (enc2 >> 4);
|
|
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
|
|
chr3 = ((enc3 & 3) << 6) | enc4;
|
|
|
|
output = output + String.fromCharCode(chr1);
|
|
|
|
if (enc3 != 64) {
|
|
output = output + String.fromCharCode(chr2);
|
|
}
|
|
if (enc4 != 64) {
|
|
output = output + String.fromCharCode(chr3);
|
|
}
|
|
|
|
}
|
|
|
|
output = this._utf8_decode(output);
|
|
|
|
return output;
|
|
|
|
},
|
|
|
|
// private method for UTF-8 encoding
|
|
_utf8_encode : function (string) {
|
|
string = string.replace(/\r\n/g,"\n");
|
|
var utftext = "";
|
|
|
|
for (var n = 0; n < string.length; n++) {
|
|
|
|
var c = string.charCodeAt(n);
|
|
|
|
if (c < 128) {
|
|
utftext += String.fromCharCode(c);
|
|
}
|
|
else if((c > 127) && (c < 2048)) {
|
|
utftext += String.fromCharCode((c >> 6) | 192);
|
|
utftext += String.fromCharCode((c & 63) | 128);
|
|
}
|
|
else {
|
|
utftext += String.fromCharCode((c >> 12) | 224);
|
|
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
|
|
utftext += String.fromCharCode((c & 63) | 128);
|
|
}
|
|
|
|
}
|
|
|
|
return utftext;
|
|
},
|
|
|
|
// private method for UTF-8 decoding
|
|
_utf8_decode : function (utftext) {
|
|
var string = "";
|
|
var i = 0;
|
|
var c = c1 = c2 = 0;
|
|
|
|
while ( i < utftext.length ) {
|
|
|
|
c = utftext.charCodeAt(i);
|
|
|
|
if (c < 128) {
|
|
string += String.fromCharCode(c);
|
|
i++;
|
|
}
|
|
else if((c > 191) && (c < 224)) {
|
|
c2 = utftext.charCodeAt(i+1);
|
|
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
|
|
i += 2;
|
|
}
|
|
else {
|
|
c2 = utftext.charCodeAt(i+1);
|
|
c3 = utftext.charCodeAt(i+2);
|
|
string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
|
|
i += 3;
|
|
}
|
|
|
|
}
|
|
|
|
return string;
|
|
}
|
|
} |