zotero/chrome/content/zotero/xpcom/translation/translate.js
2015-04-13 02:01:43 -04:00

2792 lines
91 KiB
JavaScript

/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2009 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
/**
* @class
* Deprecated class for creating new Zotero.Translate instances<br/>
* <br/>
* New code should use Zotero.Translate.Web, Zotero.Translate.Import, Zotero.Translate.Export, or
* Zotero.Translate.Search
*/
Zotero.Translate = function(type) {
Zotero.debug("Translate: WARNING: new Zotero.Translate() is deprecated; please don't use this if you don't have to");
// hack
var translate = Zotero.Translate.newInstance(type);
for(var i in translate) {
this[i] = translate[i];
}
this.constructor = translate.constructor;
this.__proto__ = translate.__proto__;
}
/**
* Create a new translator by a string type
*/
Zotero.Translate.newInstance = function(type) {
return new Zotero.Translate[type.substr(0, 1).toUpperCase()+type.substr(1).toLowerCase()];
}
/**
* Namespace for Zotero sandboxes
* @namespace
*/
Zotero.Translate.Sandbox = {
/**
* Combines a sandbox with the base sandbox
*/
"_inheritFromBase":function(sandboxToMerge) {
var newSandbox = {};
for(var method in Zotero.Translate.Sandbox.Base) {
newSandbox[method] = Zotero.Translate.Sandbox.Base[method];
}
for(var method in sandboxToMerge) {
newSandbox[method] = sandboxToMerge[method];
}
return newSandbox;
},
/**
* Base sandbox. These methods are available to all translators.
* @namespace
*/
"Base": {
/**
* Called as {@link Zotero.Item#complete} from translators to save items to the database.
* @param {Zotero.Translate} translate
* @param {SandboxItem} An item created using the Zotero.Item class from the sandbox
*/
"_itemDone":function(translate, item) {
//Zotero.debug("Translate: Saving item");
// warn if itemDone called after translation completed
if(translate._complete) {
Zotero.debug("Translate: WARNING: Zotero.Item#complete() called after Zotero.done(); please fix your code", 2);
}
const allowedObjects = ["complete", "attachments", "seeAlso", "creators", "tags", "notes"];
delete item.complete;
for(var i in item) {
var val = item[i];
if(!val && val !== 0) {
// remove null, undefined, and false properties, and convert objects to strings
delete item[i];
continue;
}
var type = typeof val;
var isObject = type === "object" || type === "xml" || type === "function",
shouldBeObject = allowedObjects.indexOf(i) !== -1;
if(isObject && !shouldBeObject) {
// Convert things that shouldn't be objects to objects
translate._debug("Translate: WARNING: typeof "+i+" is "+type+"; converting to string");
item[i] = val.toString();
} else if(shouldBeObject && !isObject) {
translate._debug("Translate: WARNING: typeof "+i+" is "+type+"; converting to array");
item[i] = [val];
} else if(type === "string") {
// trim strings
item[i] = val.trim();
}
}
// if we're not supposed to save the item or we're in a child translator,
// just return the item array
if(translate._libraryID === false || translate._parentTranslator) {
translate.newItems.push(item);
translate._runHandler("itemDone", item, item);
return;
}
// We use this within the connector to keep track of items as they are saved
if(!item.id) item.id = Zotero.Utilities.randomString();
// don't save documents as documents in connector, since we can't pass them around
if(Zotero.isConnector) {
var attachments = item.attachments;
var nAttachments = attachments.length;
for(var j=0; j<nAttachments; j++) {
if(attachments[j].document) {
attachments[j].url = attachments[j].document.documentURI || attachments[j].document.URL;
attachments[j].mimeType = "text/html";
delete attachments[j].document;
}
}
}
// Fire itemSaving event
translate._runHandler("itemSaving", item);
if(translate instanceof Zotero.Translate.Web) {
// For web translators, we queue saves
translate.saveQueue.push(item);
} else {
// Save items
translate._saveItems([item]);
}
},
/**
* Gets translator options that were defined in displayOptions in translator header
*
* @param {Zotero.Translate} translate
* @param {String} option Option to be retrieved
*/
"getOption":function(translate, option) {
if(typeof option !== "string") {
throw(new Error("getOption: option must be a string"));
return;
}
return translate._displayOptions[option];
},
/**
* Gets a hidden preference that can be defined by hiddenPrefs in translator header
*
* @param {Zotero.Translate} translate
* @param {String} pref Prefernce to be retrieved
*/
"getHiddenPref":function(translate, pref) {
if(typeof(pref) != "string") {
throw(new Error("getPref: preference must be a string"));
}
var hp = translate._translatorInfo.hiddenPrefs || {};
var value;
try {
value = Zotero.Prefs.get('translators.' + pref);
} catch(e) {}
return (value !== undefined ? value : hp[pref]);
},
/**
* For loading other translators and accessing their methods
*
* @param {Zotero.Translate} translate
* @param {String} type Translator type ("web", "import", "export", or "search")
* @returns {Object} A safeTranslator object, which operates mostly like Zotero.Translate
*/
"loadTranslator":function(translate, type) {
const setDefaultHandlers = function(translate, translation) {
if(type !== "export"
&& (!translation._handlers['itemDone'] || !translation._handlers['itemDone'].length)) {
translation.setHandler("itemDone", function(obj, item) {
translate.Sandbox._itemDone(translate, item);
});
}
if(!translation._handlers['selectItems'] || !translation._handlers['selectItems'].length) {
translation.setHandler("selectItems", translate._handlers["selectItems"]);
}
}
if(typeof type !== "string") {
throw(new Error("loadTranslator: type must be a string"));
return;
}
Zotero.debug("Translate: Creating translate instance of type "+type+" in sandbox");
var translation = Zotero.Translate.newInstance(type);
translation._parentTranslator = translate;
if(translation instanceof Zotero.Translate.Export && !(translation instanceof Zotero.Translate.Export)) {
throw(new Error("Only export translators may call other export translators"));
}
/**
* @class Wrapper for {@link Zotero.Translate} for safely calling another translator
* from inside an existing translator
* @inner
*/
var safeTranslator = {};
safeTranslator.__exposedProps__ = {
"setSearch":"r",
"setDocument":"r",
"setHandler":"r",
"setString":"r",
"setTranslator":"r",
"getTranslators":"r",
"translate":"r",
"getTranslatorObject":"r"
};
safeTranslator.setSearch = function(arg) {
if(!Zotero.isBookmarklet) arg = JSON.parse(JSON.stringify(arg));
return translation.setSearch(arg);
};
safeTranslator.setDocument = function(arg) {
if (Zotero.isFx && !Zotero.isBookmarklet) {
if (arg.wrappedJSObject && arg.wrappedJSObject.__wrappedObject) {
arg = arg.wrappedJSObject.__wrappedObject;
}
return translation.setDocument(new XPCNativeWrapper(arg));
} else {
return translation.setDocument(arg);
}
};
var errorHandlerSet = false;
safeTranslator.setHandler = function(arg1, arg2) {
if(arg1 === "error") errorHandlerSet = true;
translation.setHandler(arg1,
function(obj, item) {
try {
item = item.wrappedJSObject ? item.wrappedJSObject : item;
if(arg1 == "itemDone") {
item.complete = translate._sandboxZotero.Item.prototype.complete;
} else if(arg1 == "translators" && Zotero.isFx && !Zotero.isBookmarklet) {
var translators = new translate._sandboxManager.sandbox.Array();
translators = translators.wrappedJSObject || translators;
for (var i=0; i<item.length; i++) {
translators.push(item[i]);
}
item = translators;
}
arg2(obj, item);
} catch(e) {
translate.complete(false, e);
}
}
);
};
safeTranslator.setString = function(arg) { translation.setString(arg) };
safeTranslator.setTranslator = function(arg) {
var success = translation.setTranslator(arg);
if(!success) {
throw new Error("Translator "+translate.translator[0].translatorID+" attempted to call invalid translatorID "+arg);
}
};
var translatorsHandlerSet = false;
safeTranslator.getTranslators = function() {
if(!translation._handlers["translators"] || !translation._handlers["translators"].length) {
throw new Error('Translator must register a "translators" handler to '+
'call getTranslators() in this translation environment.');
}
if(!translatorsHandlerSet) {
translation.setHandler("translators", function() {
translate.decrementAsyncProcesses("safeTranslator#getTranslators()");
});
}
translate.incrementAsyncProcesses("safeTranslator#getTranslators()");
return translation.getTranslators();
};
var doneHandlerSet = false;
safeTranslator.translate = function() {
translate.incrementAsyncProcesses("safeTranslator#translate()");
setDefaultHandlers(translate, translation);
if(!doneHandlerSet) {
doneHandlerSet = true;
translation.setHandler("done", function() { translate.decrementAsyncProcesses("safeTranslator#translate()") });
}
if(!errorHandlerSet) {
errorHandlerSet = true;
translation.setHandler("error", function(obj, error) { translate.complete(false, error) });
}
return translation.translate(false);
};
safeTranslator.getTranslatorObject = function(callback) {
if(callback) {
translate.incrementAsyncProcesses("safeTranslator#getTranslatorObject()");
} else {
throw new Error("Translator must pass a callback to getTranslatorObject() to "+
"operate in this translation environment.");
}
var translator = translation.translator[0];
(typeof translator === "object" ? Zotero.Promise.resolve(translator) : Zotero.Translators.get(translator)).
then(function(translator) {
return translation._loadTranslator(translator);
}).then(function() {
if(Zotero.isFx && !Zotero.isBookmarklet) {
// do same origin check
var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
.getService(Components.interfaces.nsIScriptSecurityManager);
var ioService = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
var outerSandboxURI = ioService.newURI(typeof translate._sandboxLocation === "object" ?
translate._sandboxLocation.location : translate._sandboxLocation, null, null);
var innerSandboxURI = ioService.newURI(typeof translation._sandboxLocation === "object" ?
translation._sandboxLocation.location : translation._sandboxLocation, null, null);
try {
secMan.checkSameOriginURI(outerSandboxURI, innerSandboxURI, false);
} catch(e) {
throw new Error("getTranslatorObject() may not be called from web or search "+
"translators to web or search translators from different origins.");
return;
}
}
translation._prepareTranslation();
setDefaultHandlers(translate, translation);
sandbox = translation._sandboxManager.sandbox;
if(!Zotero.Utilities.isEmpty(sandbox.exports)) {
sandbox.exports.Zotero = sandbox.Zotero;
sandbox = sandbox.exports;
} else {
translate._debug("COMPAT WARNING: "+translation.translator[0].label+" does "+
"not export any properties. Only detect"+translation._entryFunctionSuffix+
" and do"+translation._entryFunctionSuffix+" will be available in "+
"connectors.");
}
callback(sandbox);
translate.decrementAsyncProcesses("safeTranslator#getTranslatorObject()");
}).catch(function(e) {
translate.complete(false, e);
return;
});
};
if(Zotero.isFx && Zotero.platformMajorVersion >= 33) {
for(var i in safeTranslator) {
if (typeof(safeTranslator[i]) === "function") {
safeTranslator[i] = translate._sandboxManager._makeContentForwarder(function(func) {
return function() {
func.apply(safeTranslator, this.args.wrappedJSObject || this.args);
}
}(safeTranslator[i]));
}
}
}
return safeTranslator;
},
/**
* Enables asynchronous detection or translation
* @param {Zotero.Translate} translate
* @deprecated
*/
"wait":function(translate) {},
/**
* Sets the return value for detection
*
* @param {Zotero.Translate} translate
*/
"done":function(translate, returnValue) {
if(translate._currentState === "detect") {
translate._returnValue = returnValue;
}
},
/**
* Proxy for translator _debug function
*
* @param {Zotero.Translate} translate
* @param {String} string String to write to console
* @param {String} [level] Level to log as (1 to 5)
*/
"debug":function(translate, string, level) {
translate._debug(string, level);
}
},
/**
* Web functions exposed to sandbox
* @namespace
*/
"Web":{
/**
* Lets user pick which items s/he wants to put in his/her library
* @param {Zotero.Translate} translate
* @param {Object} items An set of id => name pairs in object format
*/
"selectItems":function(translate, items, callback) {
function transferObject(obj) {
return Zotero.isFx ? translate._sandboxManager.copyObject(obj) : obj;
}
if(Zotero.Utilities.isEmpty(items)) {
throw new Error("Translator called select items with no items");
}
// Some translators pass an array rather than an object to Zotero.selectItems.
// This will break messaging outside of Firefox, so we need to fix it.
if(Object.prototype.toString.call(items) === "[object Array]") {
translate._debug("WARNING: Zotero.selectItems should be called with an object, not an array");
var itemsObj = {};
for(var i in items) itemsObj[i] = items[i];
items = itemsObj;
}
if(translate._selectedItems) {
// if we have a set of selected items for this translation, use them
return transferObject(translate._selectedItems);
} else if(translate._handlers.select) {
// whether the translator supports asynchronous selectItems
var haveAsyncCallback = !!callback;
// whether the handler operates asynchronously
var haveAsyncHandler = false;
var returnedItems = null;
var callbackExecuted = false;
if(haveAsyncCallback) {
// if this translator provides an async callback for selectItems, rig things
// up to pop off the async process
var newCallback = function(selectedItems) {
callbackExecuted = true;
callback(transferObject(selectedItems));
if(haveAsyncHandler) translate.decrementAsyncProcesses("Zotero.selectItems()");
};
} else {
// if this translator doesn't provide an async callback for selectItems, set things
// up so that we can wait to see if the select handler returns synchronously. If it
// doesn't, we will need to restart translation.
var newCallback = function(selectedItems) {
callbackExecuted = true;
if(haveAsyncHandler) {
translate.translate(translate._libraryID, translate._saveAttachments, selectedItems);
} else {
returnedItems = transferObject(selectedItems);
}
};
}
var returnValue = translate._runHandler("select", items, newCallback);
if(returnValue !== undefined) {
// handler may have returned a value, which makes callback unnecessary
Zotero.debug("WARNING: Returning items from a select handler is deprecated. "+
"Please pass items as to the callback provided as the third argument to "+
"the handler.");
returnedItems = transferObject(returnValue);
haveAsyncHandler = false;
} else {
// if we don't have returnedItems set already, the handler is asynchronous
haveAsyncHandler = !callbackExecuted;
}
if(haveAsyncCallback) {
if(haveAsyncHandler) {
// we are running asynchronously, so increment async processes
translate.incrementAsyncProcesses("Zotero.selectItems()");
} else if(!callbackExecuted) {
// callback didn't get called from handler, so call it here
callback(returnedItems);
}
return false;
} else {
translate._debug("COMPAT WARNING: No callback was provided for "+
"Zotero.selectItems(). When executed outside of Firefox, a selectItems() call "+
"will require this translator to be called multiple times.", 1);
if(haveAsyncHandler) {
// The select handler is asynchronous, but this translator doesn't support
// asynchronous select. We return false to abort translation in this
// instance, and we will restart it later when the selectItems call is
// complete.
translate._aborted = true;
return false;
} else {
return returnedItems;
}
}
} else { // no handler defined; assume they want all of them
if(callback) callback(items);
return items;
}
},
/**
* Overloads {@link Zotero.Translate.Sandbox.Base._itemDone} to ensure that no standalone
* items are saved, that an item type is specified, and to add a libraryCatalog and
* shortTitle if relevant.
* @param {Zotero.Translate} translate
* @param {SandboxItem} An item created using the Zotero.Item class from the sandbox
*/
"_itemDone":function(translate, item) {
// Only apply checks if there is no parent translator
if(!translate._parentTranslator) {
if(!item.itemType) {
item.itemType = "webpage";
translate._debug("WARNING: No item type specified");
}
if(item.type == "attachment" || item.type == "note") {
Zotero.debug("Translate: Discarding standalone "+item.type+" in non-import translator", 2);
return;
}
// store library catalog if this item was captured from a website, and
// libraryCatalog is truly undefined (not false or "")
if(item.repository !== undefined) {
Zotero.debug("Translate: 'repository' field is now 'libraryCatalog'; please fix your code", 2);
item.libraryCatalog = item.repository;
delete item.repository;
}
// automatically set library catalog
if(item.libraryCatalog === undefined) {
item.libraryCatalog = translate.translator[0].label;
}
// automatically set access date if URL is set
if(item.url && typeof item.accessDate == 'undefined') {
item.accessDate = "CURRENT_TIMESTAMP";
}
//consider type-specific "title" alternatives
var altTitle = Zotero.ItemFields.getName(Zotero.ItemFields.getFieldIDFromTypeAndBase(item.itemType, 'title'));
if(altTitle && item[altTitle]) item.title = item[altTitle];
if(!item.title) {
translate.complete(false, new Error("No title specified for item"));
return;
}
// create short title
if(item.shortTitle === undefined && Zotero.Utilities.fieldIsValidForType("shortTitle", item.itemType)) {
// only set if changes have been made
var setShortTitle = false;
var title = item.title;
// shorten to before first colon
var index = title.indexOf(":");
if(index !== -1) {
title = title.substr(0, index);
setShortTitle = true;
}
// shorten to after first question mark
index = title.indexOf("?");
if(index !== -1) {
index++;
if(index != title.length) {
title = title.substr(0, index);
setShortTitle = true;
}
}
if(setShortTitle) item.shortTitle = title;
}
/* Clean up ISBNs
* Allow multiple ISBNs, but...
* (1) validate all ISBNs
* (2) convert all ISBNs to ISBN-13
* (3) remove any duplicates
* (4) separate them with space
*/
if (item.ISBN) {
// Match ISBNs with groups separated by various dashes or even spaces
var isbnRe = /\b(?:97[89][\s\x2D\xAD\u2010-\u2015\u2043\u2212]*)?(?:\d[\s\x2D\xAD\u2010-\u2015\u2043\u2212]*){9}[\dx](?![\x2D\xAD\u2010-\u2015\u2043\u2212])\b/gi,
validISBNs = [],
isbn;
while (isbn = isbnRe.exec(item.ISBN)) {
var validISBN = Zotero.Utilities.cleanISBN(isbn[0]);
if (!validISBN) {
// Back up and move up one character
isbnRe.lastIndex = isbn.index + 1;
continue;
}
var isbn13 = Zotero.Utilities.toISBN13(validISBN);
if (validISBNs.indexOf(isbn13) == -1) validISBNs.push(isbn13);
}
item.ISBN = validISBNs.join(' ');
}
// refuse to save very long tags
if(item.tags) {
for(var i=0; i<item.tags.length; i++) {
var tag = item.tags[i];
tagString = typeof tag === "string" ? tag :
typeof tag === "object" ? (tag.tag || tag.name) : null;
if(tagString && tagString.length > 255) {
translate._debug("WARNING: Skipping unsynchable tag "+JSON.stringify(tagString));
item.tags.splice(i--, 1);
}
}
}
for(var i=0; i<item.attachments.length; i++) {
var attachment = item.attachments[i];
// Web translators are not allowed to use attachment.path
if (attachment.path) {
if (!attachment.url) attachment.url = attachment.path;
delete attachment.path;
}
if(attachment.url) {
// Remap attachment (but not link) URLs
attachment.url = translate.resolveURL(attachment.url, attachment.snapshot === false);
}
}
}
// call super
Zotero.Translate.Sandbox.Base._itemDone(translate, item);
},
/**
* Tells Zotero to monitor changes to the DOM and re-trigger detectWeb
* Can only be set during the detectWeb call
* @param {DOMNode} target Document node to monitor for changes
* @param {MutationObserverInit} [config] specifies which DOM mutations should be reported
*/
"monitorDOMChanges":function(translate, target, config) {
if(translate._currentState != "detect") {
Zotero.debug("Translate: monitorDOMChanges can only be called during the 'detect' stage");
return;
}
var window = translate.document.defaultView
var mutationObserver = window && ( window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver );
if(!mutationObserver) {
Zotero.debug("Translate: This browser does not support mutation observers.");
return;
}
var translator = translate._potentialTranslators[0];
if(!translate._registeredDOMObservers[translator.translatorID])
translate._registeredDOMObservers[translator.translatorID] = [];
var obs = translate._registeredDOMObservers[translator.translatorID];
//do not re-register observer by the same translator for the same node
if(obs.indexOf(target) != -1) {
Zotero.debug("Translate: Already monitoring this node");
return;
}
obs.push(target);
var observer = new mutationObserver(function(mutations, observer) {
obs.splice(obs.indexOf(target),1);
observer.disconnect();
Zotero.debug("Translate: Page modified.");
//we don't really care what got updated
var doc = mutations[0].target.ownerDocument;
translate._runHandler("pageModified", doc);
});
observer.observe(target, config || {childList: true, subtree: true});
Zotero.debug("Translate: Mutation observer registered on <" + target.nodeName + "> node");
}
},
/**
* Import functions exposed to sandbox
* @namespace
*/
"Import":{
/**
* Saves a collection to the DB
* Called as {@link Zotero.Collection#complete} from the sandbox
* @param {Zotero.Translate} translate
* @param {SandboxCollection} collection
*/
"_collectionDone":function(translate, collection) {
if(translate._libraryID == false) {
translate.newCollections.push(collection);
translate._runHandler("collectionDone", collection);
} else {
var newCollection = translate._itemSaver.saveCollection(collection);
translate.newCollections.push(newCollection);
translate._runHandler("collectionDone", newCollection);
}
},
/**
* Sets the value of the progress indicator associated with export as a percentage
* @param {Zotero.Translate} translate
* @param {Number} value
*/
"setProgress":function(translate, value) {
if(typeof value !== "number") {
translate._progress = null;
} else {
translate._progress = value;
}
}
},
/**
* Export functions exposed to sandbox
* @namespace
*/
"Export":{
/**
* Retrieves the next item to be exported
* @param {Zotero.Translate} translate
* @return {SandboxItem}
*/
"nextItem":function(translate) {
var item = translate._itemGetter.nextItem();
if(translate._displayOptions.hasOwnProperty("exportTags") && !translate._displayOptions["exportTags"]) {
item.tags = [];
}
translate._runHandler("itemDone", item);
return item;
},
/**
* Retrieves the next collection to be exported
* @param {Zotero.Translate} translate
* @return {SandboxCollection}
*/
"nextCollection":function(translate) {
if(!translate._translatorInfo.configOptions || !translate._translatorInfo.configOptions.getCollections) {
throw(new Error("getCollections configure option not set; cannot retrieve collection"));
}
return translate._itemGetter.nextCollection();
},
/**
* @borrows Zotero.Translate.Sandbox.Import.setProgress as this.setProgress
*/
"setProgress":function(translate, value) {
Zotero.Translate.Sandbox.Import.setProgress(translate, value);
}
},
/**
* Search functions exposed to sandbox
* @namespace
*/
"Search":{
/**
* @borrows Zotero.Translate.Sandbox.Web._itemDone as this._itemDone
*/
"_itemDone":function(translate, item) {
// Always set library catalog, even if we have a parent translator
if(item.libraryCatalog === undefined) {
item.libraryCatalog = translate.translator[0].label;
}
Zotero.Translate.Sandbox.Web._itemDone(translate, item);
}
}
}
/**
* @class Base class for all translation types
*
* @property {String} type The type of translator. This is deprecated; use instanceof instead.
* @property {Zotero.Translator[]} translator The translator currently in use. Usually, only the
* first entry of the Zotero.Translator array is populated; subsequent entries represent
* translators to be used if the first fails.
* @property {String} path The path or URI string of the target
* @property {String} newItems Items created when translate() was called
* @property {String} newCollections Collections created when translate() was called
* @property {Number} runningAsyncProcesses The number of async processes that are running. These
* need to terminate before Zotero.done() is called.
*/
Zotero.Translate.Base = function() {}
Zotero.Translate.Base.prototype = {
/**
* Initializes a Zotero.Translate instance
*/
"init":function() {
this._handlers = [];
this._currentState = null;
this._translatorInfo = null;
this.document = null;
this.location = null;
},
/**
* Sets the location to operate upon
*
* @param {String|nsIFile} location The URL to which the sandbox should be bound or path to local file
*/
"setLocation":function(location) {
this.location = location;
if(typeof this.location == "object") { // if a file
this.path = location.path;
} else { // if a url
this.path = location;
}
},
/**
* Sets the translator to be used for import/export
*
* @param {Zotero.Translator|string} Translator object or ID
*/
"setTranslator":function(translator) {
if(!translator) {
throw new Error("No translator specified");
}
this.translator = null;
if(typeof(translator) == "object") { // passed an object and not an ID
if(translator.translatorID) {
this.translator = [translator];
} else {
throw(new Error("No translatorID specified"));
}
} else {
this.translator = [translator];
}
return !!this.translator;
},
/**
* Registers a handler function to be called when translation is complete
*
* @param {String} type Type of handler to register. Legal values are:
* select
* valid: web
* called: when the user needs to select from a list of available items
* passed: an associative array in the form id => text
* returns: a numerically indexed array of ids, as extracted from the passed
* string
* itemDone
* valid: import, web, search
* called: when an item has been processed; may be called asynchronously
* passed: an item object (see Zotero.Item)
* returns: N/A
* collectionDone
* valid: import
* called: when a collection has been processed, after all items have been
* added; may be called asynchronously
* passed: a collection object (see Zotero.Collection)
* returns: N/A
* done
* valid: all
* called: when all processing is finished
* passed: true if successful, false if an error occurred
* returns: N/A
* debug
* valid: all
* called: when Zotero.debug() is called
* passed: string debug message
* returns: true if message should be logged to the console, false if not
* error
* valid: all
* called: when a fatal error occurs
* passed: error object (or string)
* returns: N/A
* translators
* valid: all
* called: when a translator search initiated with Zotero.Translate.getTranslators() is
* complete
* passed: an array of appropriate translators
* returns: N/A
* pageModified
* valid: web
* called: when a web page has been modified
* passed: the document object for the modified page
* returns: N/A
* @param {Function} handler Callback function. All handlers will be passed the current
* translate instance as the first argument. The second argument is dependent on the handler.
*/
"setHandler":function(type, handler) {
if(!this._handlers[type]) {
this._handlers[type] = new Array();
}
this._handlers[type].push(handler);
},
/**
* Clears all handlers for a given function
* @param {String} type See {@link Zotero.Translate.Base#setHandler} for valid values
*/
"clearHandlers":function(type) {
this._handlers[type] = new Array();
},
/**
* Clears a single handler for a given function
* @param {String} type See {@link Zotero.Translate.Base#setHandler} for valid values
* @param {Function} handler Callback function to remove
*/
"removeHandler":function(type, handler) {
var handlerIndex = this._handlers[type].indexOf(handler);
if(handlerIndex !== -1) this._handlers[type].splice(handlerIndex, 1);
},
/**
* Indicates that a new async process is running
*/
"incrementAsyncProcesses":function(f) {
this._runningAsyncProcesses++;
if(this._parentTranslator) {
this._parentTranslator.incrementAsyncProcesses(f+" from child translator");
} else {
//Zotero.debug("Translate: Incremented asynchronous processes to "+this._runningAsyncProcesses+" for "+f, 4);
//Zotero.debug((new Error()).stack);
}
},
/**
* Indicates that a new async process is finished
*/
"decrementAsyncProcesses":function(f, by) {
this._runningAsyncProcesses -= (by ? by : 1);
if(!this._parentTranslator) {
//Zotero.debug("Translate: Decremented asynchronous processes to "+this._runningAsyncProcesses+" for "+f, 4);
//Zotero.debug((new Error()).stack);
}
if(this._runningAsyncProcesses === 0) {
this.complete();
}
if(this._parentTranslator) this._parentTranslator.decrementAsyncProcesses(f+" from child translator", by);
},
/**
* Clears all handlers for a given function
* @param {String} type See {@link Zotero.Translate.Base#setHandler} for valid values
* @param {Any} argument Argument to be passed to handler
*/
"_runHandler":function(type) {
var returnValue = undefined;
if(this._handlers[type]) {
// compile list of arguments
if(this._parentTranslator) {
// if there is a parent translator, make sure we don't pass the Zotero.Translate
// object, since it could open a security hole
var args = [null];
} else {
var args = [this];
}
for(var i=1; i<arguments.length; i++) {
args.push(arguments[i]);
}
var handlers = this._handlers[type].slice();
for(var i=0, n=handlers.length; i<n; i++) {
Zotero.debug("Translate: Running handler "+i+" for "+type, 5);
try {
returnValue = handlers[i].apply(null, args);
} catch(e) {
if(this._parentTranslator) {
// throw handler errors if they occur when a translator is
// called from another translator, so that the
// "Could Not Translate" dialog will appear if necessary
throw(e);
} else {
// otherwise, fail silently, so as not to interfere with
// interface cleanup
Zotero.debug("Translate: "+e+' in handler '+i+' for '+type, 5);
Zotero.logError(e);
}
}
}
}
return returnValue;
},
/**
* Gets all applicable translators of a given type
*
* For import, you should call this after setLocation; otherwise, you'll just get a list of all
* import filters, not filters equipped to handle a specific file
*
* @param {Boolean} [getAllTranslators] Whether all applicable translators should be returned,
* rather than just the first available.
* @param {Boolean} [checkSetTranslator] If true, the appropriate detect function is run on the
* set document/text/etc. using the translator set by setTranslator.
* getAllTranslators parameter is meaningless in this context.
* @return {Promise} Promise for an array of {@link Zotero.Translator} objects
*/
"getTranslators":function(getAllTranslators, checkSetTranslator) {
var potentialTranslators;
// do not allow simultaneous instances of getTranslators
if(this._currentState === "detect") throw new Error("getTranslators: detection is already running");
this._currentState = "detect";
this._getAllTranslators = getAllTranslators;
if(checkSetTranslator) {
// setTranslator must be called beforehand if checkSetTranslator is set
if( !this.translator || !this.translator[0] ) {
return Zotero.Promise.reject(new Error("getTranslators: translator must be set via setTranslator before calling" +
" getTranslators with the checkSetTranslator flag"));
}
var promises = new Array();
var t;
for(var i=0, n=this.translator.length; i<n; i++) {
if(typeof(this.translator[i]) == 'string') {
t = Zotero.Translators.get(this.translator[i]);
if(!t) Zotero.debug("getTranslators: could not retrieve translator '" + this.translator[i] + "'");
} else {
t = this.translator[i];
}
/**TODO: check that the translator is of appropriate type?*/
if(t) promises.push(t);
}
if(!promises.length) return Zotero.Promise.reject(new Error("getTranslators: no valid translators were set"));
potentialTranslators = Zotero.Promise.all(promises);
} else {
potentialTranslators = this._getTranslatorsGetPotentialTranslators();
}
// if detection returns immediately, return found translators
var me = this;
return potentialTranslators.spread(function(allPotentialTranslators, properToProxyFunctions) {
me._potentialTranslators = [];
me._foundTranslators = [];
// this gets passed out by Zotero.Translators.getWebTranslatorsForLocation() because it is
// specific for each translator, but we want to avoid making a copy of a translator whenever
// possible.
me._properToProxyFunctions = properToProxyFunctions ? properToProxyFunctions : null;
me._waitingForRPC = false;
for(var i=0, n=allPotentialTranslators.length; i<n; i++) {
var translator = allPotentialTranslators[i];
if(translator.runMode === Zotero.Translator.RUN_MODE_IN_BROWSER) {
me._potentialTranslators.push(translator);
} else if(me instanceof Zotero.Translate.Web && Zotero.Connector) {
me._waitingForRPC = true;
}
}
// Attach handler for translators, so that we can return a
// promise that provides them.
// TODO make me._detect() return a promise
var deferred = Zotero.Promise.defer(),
translatorsHandler = function(obj, translators) {
me.removeHandler("translators", translatorsHandler);
deferred.resolve(translators);
}
me.setHandler("translators", translatorsHandler);
me._detect();
if(me._waitingForRPC) {
// Try detect in Zotero Standalone. If this fails, it fails; we shouldn't
// get hung up about it.
Zotero.Connector.callMethod("detect", {"uri":me.location.toString(),
"cookie":me.document.cookie,
"html":me.document.documentElement.innerHTML}).then(function(rpcTranslators) {
me._waitingForRPC = false;
// if there are translators, add them to the list of found translators
if(rpcTranslators) {
for(var i=0, n=rpcTranslators.length; i<n; i++) {
rpcTranslators[i].runMode = Zotero.Translator.RUN_MODE_ZOTERO_STANDALONE;
}
me._foundTranslators = me._foundTranslators.concat(rpcTranslators);
}
// call _detectTranslatorsCollected to return detected translators
if(me._currentState === null) {
me._detectTranslatorsCollected();
}
});
}
return deferred.promise;
}).catch(function(e) {
Zotero.logError(e);
me.complete(false, e);
});
},
/**
* Get all potential translators (without running detect)
* @return {Promise} Promise for an array of {@link Zotero.Translator} objects
*/
"_getTranslatorsGetPotentialTranslators":function() {
return Zotero.Translators.getAllForType(this.type).
then(function(translators) { return [translators] });
},
/**
* Begins the actual translation. At present, this returns immediately for import/export
* translators, but new code should use {@link Zotero.Translate.Base#setHandler} to register a
* "done" handler to determine when execution of web/search translators is complete.
*
* @param {Integer|FALSE} [libraryID] Library in which to save items,
* or NULL for default library;
* if FALSE, don't save items
* @param {Boolean} [saveAttachments=true] Exclude attachments (e.g., snapshots) on import
*/
"translate":function(libraryID, saveAttachments) { // initialize properties specific to each translation
if(!this.translator || !this.translator.length) {
var args = arguments;
Zotero.debug("Translate: translate called without specifying a translator. Running detection first.");
this.setHandler('translators', function(me, translators) {
if(!translators.length) {
me.complete(false, "Could not find an appropriate translator");
} else {
me.setTranslator(translators);
Zotero.Translate.Base.prototype.translate.apply(me, args);
}
});
this.getTranslators();
return;
}
this._currentState = "translate";
this._libraryID = libraryID;
this._saveAttachments = saveAttachments === undefined || saveAttachments;
this._savingAttachments = [];
this._savingItems = 0;
this._waitingForSave = false;
var me = this;
if(typeof this.translator[0] === "object") {
// already have a translator object, so use it
this._loadTranslator(this.translator[0]).then(function() { me._translateTranslatorLoaded() });
} else {
// need to get translator first
Zotero.Translators.get(this.translator[0]).
then(function(translator) {
me.translator[0] = translator;
me._loadTranslator(translator).then(function() { me._translateTranslatorLoaded() });
});
}
},
/**
* Called when translator has been retrieved and loaded
*/
"_translateTranslatorLoaded":function() {
// set display options to default if they don't exist
if(!this._displayOptions) this._displayOptions = this._translatorInfo.displayOptions || {};
// prepare translation
this._prepareTranslation();
Zotero.debug("Translate: Beginning translation with "+this.translator[0].label);
this.incrementAsyncProcesses("Zotero.Translate#translate()");
// translate
try {
Function.prototype.apply.call(this._sandboxManager.sandbox["do"+this._entryFunctionSuffix], null, this._getParameters());
} catch(e) {
this.complete(false, e);
return false;
}
this.decrementAsyncProcesses("Zotero.Translate#translate()");
},
/**
* Return the progress of the import operation, or null if progress cannot be determined
*/
"getProgress":function() { return null },
/**
* Translate a URL to a form that goes through the appropriate proxy, or
* convert a relative URL to an absolute one
*
* @param {String} url
* @param {Boolean} dontUseProxy If true, don't convert URLs to variants
* that use the proxy
* @type String
* @private
*/
"resolveURL":function(url, dontUseProxy) {
const hostPortRe = /^((?:http|https|ftp):)\/\/([^\/]+)/i;
// resolve local URL
var resolved = "";
// convert proxy to proper if applicable
if(hostPortRe.test(url)) {
if(this.translator && this.translator[0]
&& this.translator[0].properToProxy && !dontUseProxy) {
resolved = this.translator[0].properToProxy(url);
} else {
resolved = url;
}
} else if(Zotero.isFx && this.location) {
resolved = Components.classes["@mozilla.org/network/io-service;1"].
getService(Components.interfaces.nsIIOService).
newURI(this.location, "", null).resolve(url);
} else if(Zotero.isNode && this.location) {
resolved = require('url').resolve(this.location, url);
} else if (this.document) {
var a = this.document.createElement('a');
a.href = url;
resolved = a.href;
} else if (url.indexOf('//') == 0) {
// Protocol-relative URL with no associated web page
// Use HTTP by default
resolved = 'http:' + url;
} else {
throw new Error('Cannot resolve relative URL without an associated web page: ' + url);
}
/*var m = hostPortRe.exec(resolved);
if(!m) {
throw new Error("Invalid URL supplied for HTTP request: "+url);
} else if(this._translate.document && this._translate.document.location) {
var loc = this._translate.document.location;
if(this._translate._currentState !== "translate" && loc
&& (m[1].toLowerCase() !== loc.protocol.toLowerCase()
|| m[2].toLowerCase() !== loc.host.toLowerCase())) {
throw new Error("Attempt to access "+m[1]+"//"+m[2]+" from "+loc.protocol+"//"+loc.host
+" blocked: Cross-site requests are only allowed during translation");
}
}*/
return resolved;
},
/**
* Executed on translator completion, either automatically from a synchronous scraper or as
* done() from an asynchronous scraper. Finishes things up and calls callback function(s).
* @param {Boolean|String} returnValue An item type or a boolean true or false
* @param {String|Exception} [error] An error that occurred during translation.
* @returm {String|NULL} The exception serialized to a string, or null if translation
* completed successfully.
*/
"complete":function(returnValue, error) {
// allow translation to be aborted for re-running after selecting items
if(this._aborted) return;
// Make sure this isn't called twice
if(this._currentState === null) {
if(!returnValue) {
Zotero.debug("Translate: WARNING: Zotero.done() called after translator completion with error");
Zotero.debug(error);
} else {
var e = new Error();
Zotero.debug("Translate: WARNING: Zotero.done() called after translation completion. This should never happen. Please examine the stack below.");
Zotero.debug(e.stack);
}
return;
}
// reset async processes and propagate them to parent
if(this._parentTranslator && this._runningAsyncProcesses) {
this._parentTranslator.decrementAsyncProcesses("Zotero.Translate#complete", this._runningAsyncProcesses);
}
this._runningAsyncProcesses = 0;
if(!returnValue && this._returnValue) returnValue = this._returnValue;
var errorString = null;
if(!returnValue && error) errorString = this._generateErrorString(error);
if(this._currentState === "detect") {
if(this._potentialTranslators.length) {
var lastTranslator = this._potentialTranslators.shift();
var lastProperToProxyFunction = this._properToProxyFunctions ? this._properToProxyFunctions.shift() : null;
if(returnValue) {
var dupeTranslator = {"properToProxy":lastProperToProxyFunction};
for(var i in lastTranslator) dupeTranslator[i] = lastTranslator[i];
if(Zotero.isBookmarklet && returnValue === "server") {
// In the bookmarklet, the return value from detectWeb can be "server" to
// indicate the translator should be run on the Zotero server
dupeTranslator.runMode = Zotero.Translator.RUN_MODE_ZOTERO_SERVER;
} else {
// Usually the return value from detectWeb will be either an item type or
// the string "multiple"
dupeTranslator.itemType = returnValue;
}
this._foundTranslators.push(dupeTranslator);
} else if(error) {
this._debug("Detect using "+lastTranslator.label+" failed: \n"+errorString, 2);
}
}
if(this._potentialTranslators.length && (this._getAllTranslators || !returnValue)) {
// more translators to try; proceed to next translator
this._detect();
} else {
this._currentState = null;
if(!this._waitingForRPC) this._detectTranslatorsCollected();
}
} else {
// unset return value is equivalent to true
if(returnValue === undefined) returnValue = true;
if(returnValue) {
if(this.saveQueue.length) {
this._waitingForSave = true;
this._saveItems(this.saveQueue);
this.saveQueue = [];
return;
}
this._debug("Translation successful");
} else {
if(error) {
// report error to console
Zotero.logError(error);
// report error to debug log
this._debug("Translation using "+(this.translator && this.translator[0] && this.translator[0].label ? this.translator[0].label : "no translator")+" failed: \n"+errorString, 2);
}
this._runHandler("error", error);
}
this._currentState = null;
// call handlers
this._runHandler("itemsDone", returnValue);
if(returnValue) {
this._checkIfDone();
} else {
this._runHandler("done", returnValue);
}
}
return errorString;
},
/**
* Saves items to the database, taking care to defer attachmentProgress notifications
* until after save
*/
"_saveItems":function(items) {
var me = this,
itemDoneEventsDispatched = false,
deferredProgress = [],
attachmentsWithProgress = [];
this._savingItems++;
this._itemSaver.saveItems(items.slice(), function(returnValue, newItems) {
if(returnValue) {
// Remove attachments not being saved from item.attachments
for(var i=0; i<items.length; i++) {
var item = items[i];
for(var j=0; j<item.attachments.length; j++) {
if(attachmentsWithProgress.indexOf(item.attachments[j]) === -1) {
item.attachments.splice(j--, 1);
}
}
}
// Trigger itemDone events
for(var i=0, nItems = items.length; i<nItems; i++) {
me._runHandler("itemDone", newItems[i], items[i]);
}
// Specify that itemDone event was dispatched, so that we don't defer
// attachmentProgress notifications anymore
itemDoneEventsDispatched = true;
// Run deferred attachmentProgress notifications
for(var i=0; i<deferredProgress.length; i++) {
me._runHandler("attachmentProgress", deferredProgress[i][0],
deferredProgress[i][1], deferredProgress[i][2]);
}
me.newItems = me.newItems.concat(newItems);
me._savingItems--;
me._checkIfDone();
} else {
Zotero.logError(newItems);
me.complete(returnValue, newItems);
}
},
function(attachment, progress, error) {
var attachmentIndex = me._savingAttachments.indexOf(attachment);
if(progress === false || progress === 100) {
if(attachmentIndex !== -1) {
me._savingAttachments.splice(attachmentIndex, 1);
}
} else if(attachmentIndex === -1) {
me._savingAttachments.push(attachment);
}
if(itemDoneEventsDispatched) {
// itemDone event has already fired, so we can fire attachmentProgress
// notifications
me._runHandler("attachmentProgress", attachment, progress, error);
me._checkIfDone();
} else {
// Defer until after we fire the itemDone event
deferredProgress.push([attachment, progress, error]);
attachmentsWithProgress.push(attachment);
}
});
},
/**
* Checks if saving done, and if so, fires done event
*/
"_checkIfDone":function() {
if(!this._savingItems && !this._savingAttachments.length && (!this._currentState || this._waitingForSave)) {
this._runHandler("done", true);
}
},
/**
* Begins running detect code for a translator, first loading it
*/
"_detect":function() {
// there won't be any translators if we need an RPC call
if(!this._potentialTranslators.length) {
this.complete(true);
return;
}
var me = this;
this._loadTranslator(this._potentialTranslators[0]).
then(function() { me._detectTranslatorLoaded() });
},
/**
* Runs detect code for a translator
*/
"_detectTranslatorLoaded":function() {
this._prepareDetection();
this.incrementAsyncProcesses("Zotero.Translate#getTranslators");
try {
var returnValue = Function.prototype.apply.call(this._sandboxManager.sandbox["detect"+this._entryFunctionSuffix], null, this._getParameters());
} catch(e) {
this.complete(false, e);
return;
}
if(returnValue !== undefined) this._returnValue = returnValue;
this.decrementAsyncProcesses("Zotero.Translate#getTranslators");
},
/**
* Called when all translators have been collected for detection
*/
"_detectTranslatorsCollected":function() {
Zotero.debug("Translate: All translator detect calls and RPC calls complete:");
this._foundTranslators.sort(function(a, b) { return a.priority-b.priority });
if (this._foundTranslators.length) {
this._foundTranslators.forEach(function(t) {
Zotero.debug("\t" + t.label + ": " + t.priority);
});
} else {
Zotero.debug("\tNo suitable translators found");
}
this._runHandler("translators", this._foundTranslators);
},
/**
* Loads the translator into its sandbox
* @param {Zotero.Translator} translator
* @return {Boolean} Whether the translator could be successfully loaded
*/
"_loadTranslator":function(translator) {
var sandboxLocation = this._getSandboxLocation();
if(!this._sandboxLocation || sandboxLocation !== this._sandboxLocation) {
this._sandboxLocation = sandboxLocation;
this._generateSandbox();
}
this._currentTranslator = translator;
this._runningAsyncProcesses = 0;
this._returnValue = undefined;
this._aborted = false;
this.saveQueue = [];
var me = this;
return translator.getCode().then(function(code) {
Zotero.debug("Translate: Parsing code for " + translator.label + " "
+ "(" + translator.translatorID + ", " + translator.lastUpdated + ")", 4);
me._sandboxManager.eval("var exports = {}, ZOTERO_TRANSLATOR_INFO = "+code,
["detect"+me._entryFunctionSuffix, "do"+me._entryFunctionSuffix, "exports",
"ZOTERO_TRANSLATOR_INFO"],
(translator.file ? translator.file.path : translator.label));
me._translatorInfo = me._sandboxManager.sandbox.ZOTERO_TRANSLATOR_INFO;
}).catch(function(e) {
me.complete(false, e);
});
},
/**
* Generates a sandbox for scraping/scraper detection
*/
"_generateSandbox":function() {
Zotero.debug("Translate: Binding sandbox to "+(typeof this._sandboxLocation == "object" ? this._sandboxLocation.document.location : this._sandboxLocation), 4);
if (this._parentTranslator && this._parentTranslator._sandboxManager.newChild) {
this._sandboxManager = this._parentTranslator._sandboxManager.newChild();
} else {
this._sandboxManager = new Zotero.Translate.SandboxManager(this._sandboxLocation);
}
const createArrays = "['creators', 'notes', 'tags', 'seeAlso', 'attachments']";
var src = "var Zotero = {};"+
"Zotero.Item = function (itemType) {"+
"const createArrays = "+createArrays+";"+
"this.itemType = itemType;"+
"for(var i=0, n=createArrays.length; i<n; i++) {"+
"this[createArrays[i]] = [];"+
"}"+
"};";
if(this instanceof Zotero.Translate.Export || this instanceof Zotero.Translate.Import) {
src += "Zotero.Collection = function () {};"+
"Zotero.Collection.prototype.complete = function() { Zotero._collectionDone(this); };";
}
src += "Zotero.Item.prototype.complete = function() { Zotero._itemDone(this); }";
this._sandboxManager.eval(src);
this._sandboxManager.importObject(this.Sandbox, this);
this._sandboxManager.importObject({"Utilities":new Zotero.Utilities.Translate(this)});
this._sandboxZotero = this._sandboxManager.sandbox.Zotero;
if(Zotero.isFx) {
if(this._sandboxZotero.wrappedJSObject) this._sandboxZotero = this._sandboxZotero.wrappedJSObject;
}
this._sandboxZotero.Utilities.HTTP = this._sandboxZotero.Utilities;
this._sandboxZotero.isBookmarklet = Zotero.isBookmarklet || false;
this._sandboxZotero.isConnector = Zotero.isConnector || false;
this._sandboxZotero.isServer = Zotero.isServer || false;
this._sandboxZotero.parentTranslator = this._parentTranslator
&& this._parentTranslator._currentTranslator ?
this._parentTranslator._currentTranslator.translatorID : null;
// create shortcuts
this._sandboxManager.sandbox.Z = this._sandboxZotero;
this._sandboxManager.sandbox.ZU = this._sandboxZotero.Utilities;
this._transferItem = this._sandboxZotero._transferItem;
},
/**
* Logs a debugging message
* @param {String} string Debug string to log
* @param {Integer} level Log level (1-5, higher numbers are higher priority)
*/
"_debug":function(string, level) {
if(level !== undefined && typeof level !== "number") {
Zotero.debug("debug: level must be an integer");
return;
}
// if handler does not return anything explicitly false, show debug
// message in console
if(this._runHandler("debug", string) !== false) {
if(typeof string == "string") string = "Translate: "+string;
Zotero.debug(string, level);
}
},
/**
* Generates a string from an exception
* @param {String|Exception} error
*/
"_generateErrorString":function(error) {
var errorString = "";
if(typeof(error) == "string") {
errorString = "\nthrown exception => "+error;
} else {
var haveStack = false;
for(var i in error) {
if(typeof(error[i]) != "object") {
if(i === "stack") haveStack = true;
errorString += "\n"+i+' => '+error[i];
}
}
errorString += "\nstring => "+error.toString();
if(!haveStack && error.stack) {
// In case the stack is not enumerable
errorString += "\nstack => "+error.stack.toString();
}
}
errorString += "\nurl => "+this.path
+ "\ndownloadAssociatedFiles => "+Zotero.Prefs.get("downloadAssociatedFiles")
+ "\nautomaticSnapshots => "+Zotero.Prefs.get("automaticSnapshots");
return errorString.substr(1);
},
/**
* Determines the location where the sandbox should be bound
* @return {String|document} The location to which to bind the sandbox
*/
"_getSandboxLocation":function() {
return (this._parentTranslator ? this._parentTranslator._sandboxLocation : "http://www.example.com/");
},
/**
* Gets parameters to be passed to detect* and do* functions
* @return {Array} A list of parameters
*/
"_getParameters":function() { return []; },
/**
* No-op for preparing detection
*/
"_prepareDetection":function() {},
/**
* No-op for preparing translation
*/
"_prepareTranslation":function() {}
}
/**
* @class Web translation
*
* @property {Document} document The document object to be used for web scraping (set with setDocument)
* @property {Zotero.CookieSandbox} cookieSandbox A CookieSandbox to manage cookies for
* this Translate instance.
*/
Zotero.Translate.Web = function() {
this._registeredDOMObservers = {}
this.init();
}
Zotero.Translate.Web.prototype = new Zotero.Translate.Base();
Zotero.Translate.Web.prototype.type = "web";
Zotero.Translate.Web.prototype._entryFunctionSuffix = "Web";
Zotero.Translate.Web.prototype.Sandbox = Zotero.Translate.Sandbox._inheritFromBase(Zotero.Translate.Sandbox.Web);
/**
* Sets the browser to be used for web translation
* @param {Document} doc An HTML document
*/
Zotero.Translate.Web.prototype.setDocument = function(doc) {
this.document = doc;
this.setLocation(doc.location.href);
}
/**
* Sets a Zotero.CookieSandbox to handle cookie management for XHRs initiated from this
* translate instance
*
* @param {Zotero.CookieSandbox} cookieSandbox
*/
Zotero.Translate.Web.prototype.setCookieSandbox = function(cookieSandbox) {
this.cookieSandbox = cookieSandbox;
}
/**
* Sets the location to operate upon
*
* @param {String} location The URL of the page to translate
*/
Zotero.Translate.Web.prototype.setLocation = function(location) {
this.location = location;
this.path = this.location;
}
/**
* Get potential web translators
*/
Zotero.Translate.Web.prototype._getTranslatorsGetPotentialTranslators = function() {
return Zotero.Translators.getWebTranslatorsForLocation(this.location);
}
/**
* Bind sandbox to document being translated
*/
Zotero.Translate.Web.prototype._getSandboxLocation = function() {
if(this._parentTranslator) {
return this._parentTranslator._sandboxLocation;
} else if(this.document.defaultView
&& (this.document.defaultView.toString().indexOf("Window") !== -1
|| this.document.defaultView.toString().indexOf("XrayWrapper") !== -1)) {
return this.document.defaultView;
} else {
return this.document.location.toString();
}
}
/**
* Pass document and location to detect* and do* functions
*/
Zotero.Translate.Web.prototype._getParameters = function() {
if (Zotero.Translate.DOMWrapper &&
Zotero.Translate.DOMWrapper.isWrapped(this.document) &&
Zotero.platformMajorVersion >= 35) {
return [this._sandboxManager.wrap(Zotero.Translate.DOMWrapper.unwrap(this.document), null,
this.document.__wrapperOverrides), this.location];
} else {
return [this.document, this.location];
}
};
/**
* Prepare translation
*/
Zotero.Translate.Web.prototype._prepareTranslation = function() {
this._itemSaver = new Zotero.Translate.ItemSaver(this._libraryID,
Zotero.Translate.ItemSaver[(this._saveAttachments ? "ATTACHMENT_MODE_DOWNLOAD" : "ATTACHMENT_MODE_IGNORE")], 1,
this.document, this._cookieSandbox, this.location);
this.newItems = [];
}
/**
* Overload translate to set selectedItems
*/
Zotero.Translate.Web.prototype.translate = function(libraryID, saveAttachments, selectedItems) {
this._selectedItems = selectedItems;
Zotero.Translate.Base.prototype.translate.apply(this, [libraryID, saveAttachments]);
}
/**
* Overload _translateTranslatorLoaded to send an RPC call if necessary
*/
Zotero.Translate.Web.prototype._translateTranslatorLoaded = function() {
var runMode = this.translator[0].runMode;
if(runMode === Zotero.Translator.RUN_MODE_IN_BROWSER || this._parentTranslator) {
Zotero.Translate.Base.prototype._translateTranslatorLoaded.apply(this);
} else if(runMode === Zotero.Translator.RUN_MODE_ZOTERO_STANDALONE ||
(runMode === Zotero.Translator.RUN_MODE_ZOTERO_SERVER && Zotero.Connector.isOnline)) {
var me = this;
Zotero.Connector.callMethod("savePage", {
"uri":this.location.toString(),
"translatorID":(typeof this.translator[0] === "object"
? this.translator[0].translatorID : this.translator[0]),
"cookie":this.document.cookie,
"html":this.document.documentElement.innerHTML
}, function(obj) { me._translateRPCComplete(obj) });
} else if(runMode === Zotero.Translator.RUN_MODE_ZOTERO_SERVER) {
var me = this;
Zotero.API.createItem({"url":this.document.location.href.toString()},
function(statusCode, response) {
me._translateServerComplete(statusCode, response);
});
}
}
/**
* Called when an call to Zotero Standalone for translation completes
*/
Zotero.Translate.Web.prototype._translateRPCComplete = function(obj, failureCode) {
if(!obj) this.complete(false, failureCode);
if(obj.selectItems) {
// if we have to select items, call the selectItems handler and do it
var me = this;
this._runHandler("select", obj.selectItems,
function(selectedItems) {
Zotero.Connector.callMethod("selectItems",
{"instanceID":obj.instanceID, "selectedItems":selectedItems},
function(obj) { me._translateRPCComplete(obj) })
}
);
} else {
// if we don't have to select items, continue
for(var i=0, n=obj.items.length; i<n; i++) {
this._runHandler("itemDone", null, obj.items[i]);
}
this.newItems = obj.items;
this.complete(true);
}
}
/**
* Called when an call to the Zotero Translator Server for translation completes
*/
Zotero.Translate.Web.prototype._translateServerComplete = function(statusCode, response) {
if(statusCode === 300) {
// Multiple Choices
try {
response = JSON.parse(response);
} catch(e) {
Zotero.logError(e);
this.complete(false, "Invalid JSON response received from server");
return;
}
var me = this;
this._runHandler("select", response,
function(selectedItems) {
Zotero.API.createItem({
"url":me.document.location.href.toString(),
"items":selectedItems
},
function(statusCode, response) {
me._translateServerComplete(statusCode, response);
});
}
);
} else if(statusCode === 201) {
// Created
try {
response = (new DOMParser()).parseFromString(response, "application/xml");
} catch(e) {
Zotero.logError(e);
this.complete(false, "Invalid XML response received from server");
return;
}
// Extract items from ATOM/JSON response
var items = [], contents;
if("getElementsByTagNameNS" in response) {
contents = response.getElementsByTagNameNS("http://www.w3.org/2005/Atom", "content");
} else { // IE...
contents = response.getElementsByTagName("content");
}
for(var i=0, n=contents.length; i<n; i++) {
var content = contents[i];
if("getAttributeNS" in content) {
if(content.getAttributeNS("http://zotero.org/ns/api", "type") != "json") continue;
} else if(content.getAttribute("zapi:type") != "json") { // IE...
continue;
}
try {
var item = JSON.parse("textContent" in content ?
content.textContent : content.text);
} catch(e) {
Zotero.logError(e);
this.complete(false, "Invalid JSON response received from server");
return;
}
if(!("attachments" in item)) item.attachments = [];
this._runHandler("itemDone", null, item);
items.push(item);
}
this.newItems = items;
this.complete(true);
} else {
this.complete(false, response);
}
}
/**
* Overload complete to report translation failure
*/
Zotero.Translate.Web.prototype.complete = function(returnValue, error) {
// call super
var oldState = this._currentState;
var errorString = Zotero.Translate.Base.prototype.complete.apply(this, [returnValue, error]);
// Report translation failure if we failed
if(oldState == "translate" && errorString && !this._parentTranslator && this.translator.length
&& this.translator[0].inRepository && Zotero.Prefs.get("reportTranslationFailure")) {
// Don't report failure if in private browsing mode
if(Zotero.isFx && !Zotero.isBookmarklet && !Zotero.isStandalone && Components.classes["@mozilla.org/privatebrowsing;1"]) {
var pbs = Components.classes["@mozilla.org/privatebrowsing;1"]
.getService(Components.interfaces.nsIPrivateBrowsingService);
if (pbs.privateBrowsingEnabled) {
return;
}
}
var translator = this.translator[0];
Zotero.getSystemInfo(function(info) {
var postBody = "id=" + encodeURIComponent(translator.translatorID) +
"&lastUpdated=" + encodeURIComponent(translator.lastUpdated) +
"&diagnostic=" + encodeURIComponent(info) +
"&errorData=" + encodeURIComponent(errorString);
Zotero.HTTP.doPost(ZOTERO_CONFIG.REPOSITORY_URL + "report", postBody);
});
}
}
/**
* @class Import translation
*/
Zotero.Translate.Import = function() {
this.init();
}
Zotero.Translate.Import.prototype = new Zotero.Translate.Base();
Zotero.Translate.Import.prototype.type = "import";
Zotero.Translate.Import.prototype._entryFunctionSuffix = "Import";
Zotero.Translate.Import.prototype._io = false;
Zotero.Translate.Import.prototype.Sandbox = Zotero.Translate.Sandbox._inheritFromBase(Zotero.Translate.Sandbox.Import);
/**
* Sets string for translation and initializes string IO
*/
Zotero.Translate.Import.prototype.setString = function(string) {
this._string = string;
this._io = false;
}
/**
* Overload {@link Zotero.Translate.Base#complete} to close file
*/
Zotero.Translate.Import.prototype.complete = function(returnValue, error) {
if(this._io) {
this._progress = null;
this._io.close(false);
}
// call super
Zotero.Translate.Base.prototype.complete.apply(this, [returnValue, error]);
}
/**
* Get all potential import translators, ordering translators with the right file extension first
*/
Zotero.Translate.Import.prototype._getTranslatorsGetPotentialTranslators = function() {
return (this.location ?
Zotero.Translators.getImportTranslatorsForLocation(this.location) :
Zotero.Translators.getAllForType(this.type)).
then(function(translators) { return [translators] });;
}
/**
* Overload {@link Zotero.Translate.Base#getTranslators} to return all translators immediately only
* if no string or location is set
*/
Zotero.Translate.Import.prototype.getTranslators = function() {
if(!this._string && !this.location) {
if(this._currentState === "detect") throw new Error("getTranslators: detection is already running");
this._currentState = "detect";
var me = this;
return Zotero.Translators.getAllForType(this.type).
then(function(translators) {
me._potentialTranslators = [];
me._foundTranslators = translators;
me.complete(true);
return me._foundTranslators;
});
} else {
return Zotero.Translate.Base.prototype.getTranslators.call(this);
}
}
/**
* Overload {@link Zotero.Translate.Base#_loadTranslator} to prepare translator IO
*/
Zotero.Translate.Import.prototype._loadTranslator = function(translator, callback) {
// call super
var me = this;
return Zotero.Translate.Base.prototype._loadTranslator.call(this, translator).
then(function() {
me._loadTranslatorPrepareIO(translator, callback);
});
}
/**
* Prepare translator IO
*/
Zotero.Translate.Import.prototype._loadTranslatorPrepareIO = function(translator, callback) {
var configOptions = this._translatorInfo.configOptions;
var dataMode = configOptions ? configOptions["dataMode"] : "";
var me = this;
var initCallback = function(status, err) {
if(!status) {
me.complete(false, err);
} else {
me._sandboxManager.importObject(me._io);
if(callback) callback();
}
};
var err = false;
if(!this._io) {
if(Zotero.Translate.IO.Read && this.location && this.location instanceof Components.interfaces.nsIFile) {
try {
this._io = new Zotero.Translate.IO.Read(this.location, this._sandboxManager);
} catch(e) {
err = e;
}
} else {
try {
this._io = new Zotero.Translate.IO.String(this._string, this.path ? this.path : "", this._sandboxManager);
} catch(e) {
err = e;
}
}
if(err) {
this.complete(false, err);
return;
}
}
try {
this._io.init(dataMode, initCallback);
} catch(e) {
err = e;
}
if(err) {
this.complete(false, err);
return;
}
}
/**
* Prepare translation
*/
Zotero.Translate.Import.prototype._prepareTranslation = function() {
this._progress = undefined;
var baseURI = null;
if(this.location) {
try {
baseURI = Components.classes["@mozilla.org/network/io-service;1"].
getService(Components.interfaces.nsIIOService).newFileURI(this.location);
} catch(e) {}
}
this._itemSaver = new Zotero.Translate.ItemSaver(this._libraryID,
Zotero.Translate.ItemSaver[(this._saveAttachments ? "ATTACHMENT_MODE_FILE" : "ATTACHMENT_MODE_IGNORE")],
undefined, undefined, undefined, baseURI);
this.newItems = [];
this.newCollections = [];
}
/**
* Return the progress of the import operation, or null if progress cannot be determined
*/
Zotero.Translate.Import.prototype.getProgress = function() {
if(this._progress !== undefined) return this._progress;
if(Zotero.Translate.IO.rdfDataModes.indexOf(this._mode) !== -1 || this._mode === "xml/e4x" || this._mode == "xml/dom" || !this._io) {
return null;
}
return this._io.bytesRead/this._io.contentLength*100;
};
/**
* @class Export translation
*/
Zotero.Translate.Export = function() {
this.init();
}
Zotero.Translate.Export.prototype = new Zotero.Translate.Base();
Zotero.Translate.Export.prototype.type = "export";
Zotero.Translate.Export.prototype._entryFunctionSuffix = "Export";
Zotero.Translate.Export.prototype.Sandbox = Zotero.Translate.Sandbox._inheritFromBase(Zotero.Translate.Sandbox.Export);
/**
* Sets the items to be exported
* @param {Zotero.Item[]} items
*/
Zotero.Translate.Export.prototype.setItems = function(items) {
this._items = items;
delete this._collection;
}
/**
* Sets the collection to be exported (overrides setItems)
* @param {Zotero.Collection[]} collection
*/
Zotero.Translate.Export.prototype.setCollection = function(collection) {
this._collection = collection;
delete this._items;
}
/**
* Sets the translator to be used for export
*
* @param {Zotero.Translator|string} Translator object or ID. If this contains a displayOptions
* attribute, setDisplayOptions is automatically called with the specified value.
*/
Zotero.Translate.Export.prototype.setTranslator = function(translator) {
if(typeof translator == "object" && translator.displayOptions) {
this._displayOptions = translator.displayOptions;
}
return Zotero.Translate.Base.prototype.setTranslator.apply(this, [translator]);
}
/**
* Sets translator display options. you can also pass a translator (not ID) to
* setTranslator that includes a displayOptions argument
*/
Zotero.Translate.Export.prototype.setDisplayOptions = function(displayOptions) {
this._displayOptions = displayOptions;
}
/**
* Overload {@link Zotero.Translate.Base#complete} to close file and set complete
*/
Zotero.Translate.Export.prototype.complete = function(returnValue, error) {
if(this._io) {
this._progress = null;
this._io.close(true);
if(this._io instanceof Zotero.Translate.IO.String) {
this.string = this._io.string;
}
}
// call super
Zotero.Translate.Base.prototype.complete.apply(this, [returnValue, error]);
}
/**
* Overload {@link Zotero.Translate.Base#getTranslators} to return all translators immediately
*/
Zotero.Translate.Export.prototype.getTranslators = function() {
if(this._currentState === "detect") {
return Zotero.Promise.reject(new Error("getTranslators: detection is already running"));
}
var me = this;
return Zotero.Translators.getAllForType(this.type).then(function(translators) {
me._currentState = "detect";
me._foundTranslators = translators;
me._potentialTranslators = [];
me.complete(true);
return me._foundTranslators;
});
}
/**
* Does the actual export, after code has been loaded and parsed
*/
Zotero.Translate.Export.prototype._prepareTranslation = function() {
this._progress = undefined;
// initialize ItemGetter
this._itemGetter = new Zotero.Translate.ItemGetter();
var configOptions = this._translatorInfo.configOptions || {},
getCollections = configOptions.getCollections || false;
if(this._collection) {
this._itemGetter.setCollection(this._collection, getCollections);
delete this._collection;
} else if(this._items) {
this._itemGetter.setItems(this._items);
delete this._items;
} else {
this._itemGetter.setAll(getCollections);
}
// export file data, if requested
if(this._displayOptions["exportFileData"]) {
this.location = this._itemGetter.exportFiles(this.location, this.translator[0].target);
}
// initialize IO
// this is currently hackish since we pass null callbacks to the init function (they have
// callbacks to be consistent with import, but they are synchronous, so we ignore them)
if(!this.location) {
this._io = new Zotero.Translate.IO.String(null, this.path ? this.path : "", this._sandboxManager);
this._io.init(configOptions["dataMode"], function() {});
} else if(!Zotero.Translate.IO.Write) {
throw new Error("Writing to files is not supported in this build of Zotero.");
} else {
this._io = new Zotero.Translate.IO.Write(this.location);
this._io.init(configOptions["dataMode"],
this._displayOptions["exportCharset"] ? this._displayOptions["exportCharset"] : null,
function() {});
}
this._sandboxManager.importObject(this._io);
}
/**
* Overload Zotero.Translate.Base#translate to make sure that
* Zotero.Translate.Export#translate is not called without setting a
* translator first. Doesn't make sense to run detection for export.
*/
Zotero.Translate.Export.prototype.translate = function() {
if(!this.translator || !this.translator.length) {
this.complete(false, new Error("Export translation initiated without setting a translator"));
} else {
Zotero.Translate.Base.prototype.translate.apply(this, arguments);
}
};
/**
* Return the progress of the import operation, or null if progress cannot be determined
*/
Zotero.Translate.Export.prototype.getProgress = function() {
if(this._progress !== undefined) return this._progress;
if(!this._itemGetter) {
return null;
}
return (1-this._itemGetter.numItemsRemaining/this._itemGetter.numItems)*100;
};
/**
* @class Search translation
* @property {Array[]} search Item (in {@link Zotero.Item#serialize} format) to extrapolate data
* (set with setSearch)
*/
Zotero.Translate.Search = function() {
this.init();
};
Zotero.Translate.Search.prototype = new Zotero.Translate.Base();
Zotero.Translate.Search.prototype.type = "search";
Zotero.Translate.Search.prototype._entryFunctionSuffix = "Search";
Zotero.Translate.Search.prototype.Sandbox = Zotero.Translate.Sandbox._inheritFromBase(Zotero.Translate.Sandbox.Search);
/**
* @borrows Zotero.Translate.Web#setCookieSandbox
*/
Zotero.Translate.Search.prototype.setCookieSandbox = Zotero.Translate.Web.prototype.setCookieSandbox;
/**
* Sets the item to be used for searching
* @param {Object} item An item, with as many fields as desired, in the format returned by
* {@link Zotero.Item#serialize}
*/
Zotero.Translate.Search.prototype.setSearch = function(search) {
this.search = search;
}
/**
* Overloads {@link Zotero.Translate.Base#getTranslators} to always return all potential translators
*/
Zotero.Translate.Search.prototype.getTranslators = function() {
return Zotero.Translate.Base.prototype.getTranslators.call(this, true);
}
/**
* Sets the translator or translators to be used for search
*
* @param {Zotero.Translator|string} Translator object or ID
*/
Zotero.Translate.Search.prototype.setTranslator = function(translator) {
if(typeof translator == "object" && !translator.translatorID) {
// we have an array of translators
// accept a list of objects
this.translator = [];
for(var i=0, n=translator.length; i<n; i++) {
this.translator.push(translator[i]);
}
return true;
} else {
return Zotero.Translate.Base.prototype.setTranslator.apply(this, [translator]);
}
}
/**
* Overload Zotero.Translate.Base#complete to move onto the next translator if
* translation fails
*/
Zotero.Translate.Search.prototype.complete = function(returnValue, error) {
if(this._currentState == "translate" && (!this.newItems || !this.newItems.length)
&& this.translator.length) { //length is 0 only when translate was called without translators
Zotero.debug("Translate: Could not find a result using "+this.translator[0].label, 3);
if(error) Zotero.debug(this._generateErrorString(error), 3);
if(this.translator.length > 1) {
this.translator.shift();
this.translate(this._libraryID, this._saveAttachments);
return;
} else {
error = "No items returned from any translator";
returnValue = false;
}
}
// call super
Zotero.Translate.Base.prototype.complete.apply(this, [returnValue, error]);
}
/**
* Pass search item to detect* and do* functions
*/
Zotero.Translate.Search.prototype._getParameters = function() {
if(Zotero.isFx) {
return [this._sandboxManager.copyObject(this.search)];
}
return [this.search];
};
/**
* Extract sandbox location from translator target
*/
Zotero.Translate.Search.prototype._getSandboxLocation = function() {
// generate sandbox for search by extracting domain from translator target
if(this.translator && this.translator[0] && this.translator[0].target) {
// so that web translators work too
const searchSandboxRe = /^http:\/\/[\w.]+\//;
var tempURL = this.translator[0].target.replace(/\\/g, "").replace(/\^/g, "");
var m = searchSandboxRe.exec(tempURL);
if(m) return m[0];
}
return Zotero.Translate.Base.prototype._getSandboxLocation.call(this);
}
Zotero.Translate.Search.prototype._prepareTranslation = Zotero.Translate.Web.prototype._prepareTranslation;
/**
* IO-related functions
* @namespace
*/
Zotero.Translate.IO = {
/**
* Parses XML using DOMParser
*/
"parseDOMXML":function(input, charset, size) {
try {
var dp = new DOMParser();
} catch(e) {
try {
var dp = Components.classes["@mozilla.org/xmlextras/domparser;1"]
.createInstance(Components.interfaces.nsIDOMParser);
} catch(e) {
throw new Error("DOMParser not supported");
}
}
if(typeof input == "string") {
var nodes = dp.parseFromString(input, "text/xml");
} else {
var nodes = dp.parseFromStream(input, charset, size, "text/xml");
}
if(nodes.getElementsByTagName("parsererror").length) {
throw "DOMParser error: loading data into data store failed";
}
if("normalize" in nodes) nodes.normalize();
return nodes;
},
/**
* Names of RDF data modes
*/
"rdfDataModes":["rdf", "rdf/xml", "rdf/n3"]
};
/******* String support *******/
/**
* @class Translate backend for translating from a string
*/
Zotero.Translate.IO.String = function(string, uri, sandboxManager) {
if(string && typeof string === "string") {
this.string = string;
} else {
this.string = "";
}
this.contentLength = this.string.length;
this.bytesRead = 0;
this._uri = uri;
this._sandboxManager = sandboxManager;
}
Zotero.Translate.IO.String.prototype = {
"__exposedProps__":{
"RDF":"r",
"read":"r",
"write":"r",
"setCharacterSet":"r",
"getXML":"r"
},
"_initRDF":function(callback) {
Zotero.debug("Translate: Initializing RDF data store");
this._dataStore = new Zotero.RDF.AJAW.IndexedFormula();
this.RDF = new Zotero.Translate.IO._RDFSandbox(this._dataStore);
if(this.contentLength) {
try {
var xml = Zotero.Translate.IO.parseDOMXML(this.string);
} catch(e) {
this._xmlInvalid = true;
throw e;
}
var parser = new Zotero.RDF.AJAW.RDFParser(this._dataStore);
parser.parse(xml, this._uri);
}
callback(true);
},
"setCharacterSet":function(charset) {},
"read":function(bytes) {
// if we are reading in RDF data mode and no string is set, serialize current RDF to the
// string
if(Zotero.Translate.IO.rdfDataModes.indexOf(this._mode) !== -1 && this.string === "") {
this.string = this.RDF.serialize();
}
// return false if string has been read
if(this.bytesRead >= this.contentLength) {
return false;
}
if(bytes !== undefined) {
if(this.bytesRead >= this.contentLength) return false;
var oldPointer = this.bytesRead;
this.bytesRead += bytes;
return this.string.substr(oldPointer, bytes);
} else {
// bytes not specified; read a line
var oldPointer = this.bytesRead;
var lfIndex = this.string.indexOf("\n", this.bytesRead);
if(lfIndex !== -1) {
// in case we have a CRLF
this.bytesRead = lfIndex+1;
if(this.contentLength > lfIndex && this.string.substr(lfIndex-1, 1) === "\r") {
lfIndex--;
}
return this.string.substr(oldPointer, lfIndex-oldPointer);
}
if(!this._noCR) {
var crIndex = this.string.indexOf("\r", this.bytesRead);
if(crIndex === -1) {
this._noCR = true;
} else {
this.bytesRead = crIndex+1;
return this.string.substr(oldPointer, crIndex-oldPointer-1);
}
}
this.bytesRead = this.contentLength;
return this.string.substr(oldPointer);
}
},
"write":function(data) {
this.string += data;
this.contentLength = this.string.length;
},
"getXML":function() {
try {
var xml = Zotero.Translate.IO.parseDOMXML(this.string);
} catch(e) {
this._xmlInvalid = true;
throw e;
}
return (Zotero.isFx && !Zotero.isBookmarklet ? this._sandboxManager.wrap(xml) : xml);
},
"init":function(newMode, callback) {
this.bytesRead = 0;
this._noCR = undefined;
this._mode = newMode;
if(newMode === "xml/e4x") {
throw "E4X is not supported";
} else if(newMode && (Zotero.Translate.IO.rdfDataModes.indexOf(newMode) !== -1
|| newMode.substr(0, 3) === "xml/dom") && this._xmlInvalid) {
throw "XML known invalid";
} else if(Zotero.Translate.IO.rdfDataModes.indexOf(this._mode) !== -1) {
this._initRDF(callback);
} else {
callback(true);
}
},
"close":function(serialize) {
// if we are writing in RDF data mode and no string is set, serialize current RDF to the
// string
if(serialize && Zotero.Translate.IO.rdfDataModes.indexOf(this._mode) !== -1 && this.string === "") {
this.string = this.RDF.serialize();
}
}
}
/****** RDF DATA MODE ******/
/**
* @class An API for handling RDF from the sandbox. This is exposed to translators as Zotero.RDF.
*
* @property {Zotero.RDF.AJAW.IndexedFormula} _dataStore
* @property {Integer[]} _containerCounts
* @param {Zotero.RDF.AJAW.IndexedFormula} dataStore
*/
Zotero.Translate.IO._RDFSandbox = function(dataStore) {
this._dataStore = dataStore;
}
Zotero.Translate.IO._RDFSandbox.prototype = {
"_containerCounts":[],
"__exposedProps__":{
"addStatement":"r",
"newResource":"r",
"newContainer":"r",
"addContainerElement":"r",
"getContainerElements":"r",
"addNamespace":"r",
"getAllResources":"r",
"getResourceURI":"r",
"getArcsIn":"r",
"getArcsOut":"r",
"getSources":"r",
"getTargets":"r",
"getStatementsMatching":"r",
"serialize":"r"
},
/**
* Gets a resource as a Zotero.RDF.AJAW.Symbol, rather than a string
* @param {String|Zotero.RDF.AJAW.Symbol} about
* @return {Zotero.RDF.AJAW.Symbol}
*/
"_getResource":function(about) {
return (typeof about == "object" ? about : new Zotero.RDF.AJAW.Symbol(about));
},
/**
* Runs a callback to initialize this RDF store
*/
"_init":function() {
if(this._prepFunction) {
this._dataStore = this._prepFunction();
delete this._prepFunction;
}
},
/**
* Serializes the current RDF to a string
*/
"serialize":function(dataMode) {
var serializer = Zotero.RDF.AJAW.Serializer(this._dataStore);
for(var prefix in this._dataStore.namespaces) {
serializer.suggestPrefix(prefix, this._dataStore.namespaces[prefix]);
}
// serialize in appropriate format
if(dataMode == "rdf/n3") {
return serializer.statementsToN3(this._dataStore.statements);
}
return serializer.statementsToXML(this._dataStore.statements);
},
/**
* Adds an RDF triple
* @param {String|Zotero.RDF.AJAW.Symbol} about
* @param {String|Zotero.RDF.AJAW.Symbol} relation
* @param {String|Zotero.RDF.AJAW.Symbol} value
* @param {Boolean} literal Whether value should be treated as a literal (true) or a resource
* (false)
*/
"addStatement":function(about, relation, value, literal) {
if(about === null || about === undefined) {
throw new Error("about must be defined in Zotero.RDF.addStatement");
}
if(relation === null || relation === undefined) {
throw new Error("relation must be defined in Zotero.RDF.addStatement");
}
if(value === null || value === undefined) {
throw new Error("value must be defined in Zotero.RDF.addStatement");
}
if(literal) {
// zap chars that Mozilla will mangle
value = value.toString().replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
} else {
value = this._getResource(value);
}
this._dataStore.add(this._getResource(about), this._getResource(relation), value);
},
/**
* Creates a new anonymous resource
* @return {Zotero.RDF.AJAW.Symbol}
*/
"newResource":function() {
return new Zotero.RDF.AJAW.BlankNode();
},
/**
* Creates a new container resource
* @param {String} type The type of the container ("bag", "seq", or "alt")
* @param {String|Zotero.RDF.AJAW.Symbol} about The URI of the resource
* @return {Zotero.Translate.RDF.prototype.newContainer
*/
"newContainer":function(type, about) {
const rdf = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
const containerTypes = {"bag":"Bag", "seq":"Seq", "alt":"Alt"};
type = type.toLowerCase();
if(!containerTypes[type]) {
throw new Error("Invalid container type in Zotero.RDF.newContainer");
}
var about = this._getResource(about);
this.addStatement(about, rdf+"type", rdf+containerTypes[type], false);
this._containerCounts[about.toNT()] = 1;
return about;
},
/**
* Adds a new element to a container
* @param {String|Zotero.RDF.AJAW.Symbol} about The container
* @param {String|Zotero.RDF.AJAW.Symbol} element The element to add to the container
* @param {Boolean} literal Whether element should be treated as a literal (true) or a resource
* (false)
*/
"addContainerElement":function(about, element, literal) {
const rdf = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
var about = this._getResource(about);
this._dataStore.add(about, new Zotero.RDF.AJAW.Symbol(rdf+"_"+(this._containerCounts[about.toNT()]++)), element, literal);
},
/**
* Gets all elements within a container
* @param {String|Zotero.RDF.AJAW.Symbol} about The container
* @return {Zotero.RDF.AJAW.Symbol[]}
*/
"getContainerElements":function(about) {
const liPrefix = "http://www.w3.org/1999/02/22-rdf-syntax-ns#_";
var about = this._getResource(about);
var statements = this._dataStore.statementsMatching(about);
var containerElements = [];
// loop over arcs out looking for list items
for(var i=0; i<statements.length; i++) {
var statement = statements[i];
if(statement.predicate.uri.substr(0, liPrefix.length) == liPrefix) {
var number = statement.predicate.uri.substr(liPrefix.length);
// make sure these are actually numeric list items
var intNumber = parseInt(number);
if(number == intNumber.toString()) {
// add to element array
containerElements[intNumber-1] = (statement.object.termType == "literal" ? statement.object.toString() : statement.object);
}
}
}
return containerElements;
},
/**
* Adds a namespace for a specific URI
* @param {String} prefix Namespace prefix
* @param {String} uri Namespace URI
*/
"addNamespace":function(prefix, uri) {
this._dataStore.setPrefixForURI(prefix, uri);
},
/**
* Gets the URI a specific resource
* @param {String|Zotero.RDF.AJAW.Symbol} resource
* @return {String}
*/
"getResourceURI":function(resource) {
if(typeof(resource) == "string") return resource;
const rdf = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
var values = this.getTargets(resource, rdf + 'value');
if(values && values.length) return this.getResourceURI(values[0]);
if(resource.uri) return resource.uri;
if(resource.toNT == undefined) throw new Error("Zotero.RDF: getResourceURI called on invalid resource");
return resource.toNT();
},
/**
* Gets all resources in the RDF data store
* @return {Zotero.RDF.AJAW.Symbol[]}
*/
"getAllResources":function() {
var returnArray = [];
for(var i in this._dataStore.subjectIndex) {
returnArray.push(this._dataStore.subjectIndex[i][0].subject);
}
return returnArray;
},
/**
* Gets all arcs (predicates) into a resource
* @return {Zotero.RDF.AJAW.Symbol[]}
* @deprecated Since 2.1. Use {@link Zotero.Translate.IO["rdf"]._RDFBase#getStatementsMatching}
*/
"getArcsIn":function(resource) {
var statements = this._dataStore.objectIndex[this._dataStore.canon(this._getResource(resource))];
if(!statements) return false;
var returnArray = [];
for(var i=0; i<statements.length; i++) {
returnArray.push(statements[i].predicate.uri);
}
return returnArray;
},
/**
* Gets all arcs (predicates) out of a resource
* @return {Zotero.RDF.AJAW.Symbol[]}
* @deprecated Since 2.1. Use {@link Zotero.Translate.IO["rdf"]._RDFBase#getStatementsMatching}
*/
"getArcsOut":function(resource) {
var statements = this._dataStore.subjectIndex[this._dataStore.canon(this._getResource(resource))];
if(!statements) return false;
var returnArray = [];
for(var i=0; i<statements.length; i++) {
returnArray.push(statements[i].predicate.uri);
}
return returnArray;
},
/**
* Gets all subjects whose predicates point to a resource
* @param {String|Zotero.RDF.AJAW.Symbol} resource Subject that predicates should point to
* @param {String|Zotero.RDF.AJAW.Symbol} property Predicate
* @return {Zotero.RDF.AJAW.Symbol[]}
* @deprecated Since 2.1. Use {@link Zotero.Translate.IO["rdf"]._RDFBase#getStatementsMatching}
*/
"getSources":function(resource, property) {
var statements = this._dataStore.statementsMatching(undefined, this._getResource(property), this._getResource(resource));
if(!statements.length) return false;
var returnArray = [];
for(var i=0; i<statements.length; i++) {
returnArray.push(statements[i].subject);
}
return returnArray;
},
/**
* Gets all objects of a given subject with a given predicate
* @param {String|Zotero.RDF.AJAW.Symbol} resource Subject
* @param {String|Zotero.RDF.AJAW.Symbol} property Predicate
* @return {Zotero.RDF.AJAW.Symbol[]}
* @deprecated Since 2.1. Use {@link Zotero.Translate.IO["rdf"]._RDFBase#getStatementsMatching}
*/
"getTargets":function(resource, property) {
var statements = this._dataStore.statementsMatching(this._getResource(resource), this._getResource(property));
if(!statements.length) return false;
var returnArray = [];
for(var i=0; i<statements.length; i++) {
returnArray.push(statements[i].object.termType == "literal" ? statements[i].object.toString() : statements[i].object);
}
return returnArray;
},
/**
* Gets statements matching a certain pattern
*
* @param {String|Zotero.RDF.AJAW.Symbol} subj Subject
* @param {String|Zotero.RDF.AJAW.Symbol} predicate Predicate
* @param {String|Zotero.RDF.AJAW.Symbol} obj Object
* @param {Boolean} objLiteral Whether the object is a literal (as
* opposed to a URI)
* @param {Boolean} justOne Whether to stop when a single result is
* retrieved
*/
"getStatementsMatching":function(subj, pred, obj, objLiteral, justOne) {
var statements = this._dataStore.statementsMatching(
(subj ? this._getResource(subj) : undefined),
(pred ? this._getResource(pred) : undefined),
(obj ? (objLiteral ? objLiteral : this._getResource(obj)) : undefined),
undefined, justOne);
if(!statements.length) return false;
var returnArray = [];
for(var i=0; i<statements.length; i++) {
returnArray.push([statements[i].subject, statements[i].predicate, (statements[i].object.termType == "literal" ? statements[i].object.toString() : statements[i].object)]);
}
return returnArray;
}
};