translate.js (105453B)
1 /* 2 ***** BEGIN LICENSE BLOCK ***** 3 4 Copyright © 2009 Center for History and New Media 5 George Mason University, Fairfax, Virginia, USA 6 http://zotero.org 7 8 This file is part of Zotero. 9 10 Zotero is free software: you can redistribute it and/or modify 11 it under the terms of the GNU Affero General Public License as published by 12 the Free Software Foundation, either version 3 of the License, or 13 (at your option) any later version. 14 15 Zotero is distributed in the hope that it will be useful, 16 but WITHOUT ANY WARRANTY; without even the implied warranty of 17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 GNU Affero General Public License for more details. 19 20 You should have received a copy of the GNU Affero General Public License 21 along with Zotero. If not, see <http://www.gnu.org/licenses/>. 22 23 ***** END LICENSE BLOCK ***** 24 */ 25 26 /** 27 * @class 28 * Deprecated class for creating new Zotero.Translate instances<br/> 29 * <br/> 30 * New code should use Zotero.Translate.Web, Zotero.Translate.Import, Zotero.Translate.Export, or 31 * Zotero.Translate.Search 32 */ 33 Zotero.Translate = function(type) { 34 Zotero.debug("Translate: WARNING: new Zotero.Translate() is deprecated; please don't use this if you don't have to"); 35 // hack 36 var translate = Zotero.Translate.newInstance(type); 37 for(var i in translate) { 38 this[i] = translate[i]; 39 } 40 this.constructor = translate.constructor; 41 this.__proto__ = translate.__proto__; 42 } 43 44 /** 45 * Create a new translator by a string type 46 */ 47 Zotero.Translate.newInstance = function(type) { 48 return new Zotero.Translate[type.substr(0, 1).toUpperCase()+type.substr(1).toLowerCase()]; 49 } 50 51 /** 52 * Namespace for Zotero sandboxes 53 * @namespace 54 */ 55 Zotero.Translate.Sandbox = { 56 /** 57 * Combines a sandbox with the base sandbox 58 */ 59 "_inheritFromBase":function(sandboxToMerge) { 60 var newSandbox = {}; 61 62 for(var method in Zotero.Translate.Sandbox.Base) { 63 newSandbox[method] = Zotero.Translate.Sandbox.Base[method]; 64 } 65 66 for(var method in sandboxToMerge) { 67 newSandbox[method] = sandboxToMerge[method]; 68 } 69 70 return newSandbox; 71 }, 72 73 /** 74 * Base sandbox. These methods are available to all translators. 75 * @namespace 76 */ 77 "Base": { 78 /** 79 * Called as {@link Zotero.Item#complete} from translators to save items to the database. 80 * @param {Zotero.Translate} translate 81 * @param {SandboxItem} An item created using the Zotero.Item class from the sandbox 82 */ 83 _itemDone: function (translate, item) { 84 // https://github.com/zotero/translators/issues/1353 85 var asyncTranslator = !(translate instanceof Zotero.Translate.Web) 86 && translate.translator[0].configOptions 87 && translate.translator[0].configOptions.async; 88 89 var run = async function (async) { 90 Zotero.debug("Translate: Saving item"); 91 92 // warn if itemDone called after translation completed 93 if(translate._complete) { 94 Zotero.debug("Translate: WARNING: Zotero.Item#complete() called after Zotero.done(); please fix your code", 2); 95 } 96 97 const allowedObjects = [ 98 "complete", 99 "attachments", 100 "creators", 101 "tags", 102 "notes", 103 "relations", 104 // Is this still needed? 105 "seeAlso" 106 ]; 107 108 // Create a new object here, so that we strip the "complete" property 109 var newItem = {}; 110 var oldItem = item; 111 for(var i in item) { 112 var val = item[i]; 113 if(i === "complete" || (!val && val !== 0)) continue; 114 115 var type = typeof val; 116 var isObject = type === "object" || type === "xml" || type === "function", 117 shouldBeObject = allowedObjects.indexOf(i) !== -1; 118 if(isObject && !shouldBeObject) { 119 // Convert things that shouldn't be objects to objects 120 translate._debug("Translate: WARNING: typeof "+i+" is "+type+"; converting to string"); 121 newItem[i] = val.toString(); 122 } else if(shouldBeObject && !isObject) { 123 translate._debug("Translate: WARNING: typeof "+i+" is "+type+"; converting to array"); 124 newItem[i] = [val]; 125 } else if(type === "string") { 126 // trim strings 127 newItem[i] = val.trim(); 128 } else { 129 newItem[i] = val; 130 } 131 } 132 item = newItem; 133 134 // Clean empty creators 135 if (item.creators) { 136 for (var i=0; i<item.creators.length; i++) { 137 var creator = item.creators[i]; 138 if (!creator.firstName && !creator.lastName) { 139 item.creators.splice(i, 1); 140 i--; 141 } 142 } 143 } 144 145 // If we're not in a child translator, canonicalize tags 146 if (!translate._parentTranslator) { 147 if(item.tags) item.tags = translate._cleanTags(item.tags); 148 } 149 150 // if we're not supposed to save the item or we're in a child translator, 151 // just return the item array 152 if(translate._libraryID === false || translate._parentTranslator) { 153 translate.newItems.push(item); 154 if(translate._parentTranslator && Zotero.isFx && !Zotero.isBookmarklet) { 155 // Copy object so it is accessible to parent translator 156 item = translate._sandboxManager.copyObject(item); 157 item.complete = oldItem.complete; 158 } 159 return translate._runHandler("itemDone", item, item); 160 } 161 162 // We use this within the connector to keep track of items as they are saved 163 if(!item.id) item.id = Zotero.Utilities.randomString(); 164 165 if(item.attachments) { 166 var attachments = item.attachments; 167 for(var j=0; j<attachments.length; j++) { 168 var attachment = attachments[j]; 169 170 // Don't save documents as documents in connector, since we can't pass them around 171 if(Zotero.isConnector && attachment.document) { 172 attachment.url = attachment.document.documentURI || attachment.document.URL; 173 attachment.mimeType = "text/html"; 174 delete attachment.document; 175 } 176 177 // If we're not in a child translator, canonicalize tags 178 if (!translate._parentTranslator) { 179 if(attachment.tags !== undefined) attachment.tags = translate._cleanTags(attachment.tags); 180 } 181 } 182 } 183 184 if(item.notes) { 185 var notes = item.notes; 186 for(var j=0; j<notes.length; j++) { 187 var note = notes[j]; 188 if(!note) { 189 notes.splice(j--, 1); 190 } else if(typeof(note) != "object") { 191 // Convert to object 192 notes[j] = {"note":note.toString()} 193 } 194 // If we're not in a child translator, canonicalize tags 195 if (!translate._parentTranslator) { 196 if(note.tags !== undefined) note.tags = translate._cleanTags(note.tags); 197 } 198 } 199 } 200 201 if (item.version) { 202 translate._debug("Translate: item.version is deprecated; set item.versionNumber instead"); 203 item.versionNumber = item.version; 204 } 205 206 if (item.accessDate) { 207 if (Zotero.Date.isSQLDateTime(item.accessDate)) { 208 translate._debug("Translate: Passing accessDate as SQL is deprecated; pass an ISO 8601 date instead"); 209 item.accessDate = Zotero.Date.sqlToISO8601(item.accessDate); 210 } 211 } 212 213 // Fire itemSaving event 214 translate._runHandler("itemSaving", item); 215 translate._savingItems++; 216 217 // For synchronous import (when Promise isn't available in the sandbox or the do* 218 // function doesn't use it) and web translators, queue saves 219 if (!async || !asyncTranslator) { 220 Zotero.debug("Translate: Saving via queue"); 221 translate.saveQueue.push(item); 222 } 223 // For async import, save items immediately 224 else { 225 Zotero.debug("Translate: Saving now"); 226 translate.incrementAsyncProcesses("Zotero.Translate#_saveItems()"); 227 return translate._saveItems([item]) 228 .then(() => translate.decrementAsyncProcesses("Zotero.Translate#_saveItems()")); 229 } 230 }; 231 232 if (!translate._sandboxManager.sandbox.Promise) { 233 Zotero.debug("Translate: Promise not available in sandbox in _itemDone()"); 234 run(); 235 return; 236 } 237 238 return new translate._sandboxManager.sandbox.Promise(function (resolve, reject) { 239 try { 240 run(true).then( 241 resolve, 242 function (e) { 243 // Fix wrapping error from sandbox when error is thrown from _saveItems() 244 if (Zotero.isFx) { 245 reject(translate._sandboxManager.copyObject(e)); 246 } 247 else { 248 reject(e); 249 } 250 } 251 ); 252 } 253 catch (e) { 254 reject(e); 255 } 256 }); 257 }, 258 259 /** 260 * Gets translator options that were defined in displayOptions in translator header 261 * 262 * @param {Zotero.Translate} translate 263 * @param {String} option Option to be retrieved 264 */ 265 "getOption":function(translate, option) { 266 if(typeof option !== "string") { 267 throw(new Error("getOption: option must be a string")); 268 return; 269 } 270 271 return translate._displayOptions[option]; 272 }, 273 274 /** 275 * Gets a hidden preference that can be defined by hiddenPrefs in translator header 276 * 277 * @param {Zotero.Translate} translate 278 * @param {String} pref Prefernce to be retrieved 279 */ 280 "getHiddenPref":function(translate, pref) { 281 if(typeof(pref) != "string") { 282 throw(new Error("getPref: preference must be a string")); 283 } 284 285 var hp = translate._translatorInfo.hiddenPrefs || {}; 286 287 var value; 288 try { 289 value = Zotero.Prefs.get('translators.' + pref); 290 } catch(e) {} 291 292 return (value !== undefined ? value : hp[pref]); 293 }, 294 295 /** 296 * For loading other translators and accessing their methods 297 * 298 * @param {Zotero.Translate} translate 299 * @param {String} type Translator type ("web", "import", "export", or "search") 300 * @returns {Object} A safeTranslator object, which operates mostly like Zotero.Translate 301 */ 302 "loadTranslator":function(translate, type) { 303 const setDefaultHandlers = function(translate, translation) { 304 if(type !== "export" 305 && (!translation._handlers['itemDone'] || !translation._handlers['itemDone'].length)) { 306 translation.setHandler("itemDone", function(obj, item) { 307 translate.Sandbox._itemDone(translate, item); 308 }); 309 } 310 if(!translation._handlers['selectItems'] || !translation._handlers['selectItems'].length) { 311 translation.setHandler("selectItems", translate._handlers["selectItems"]); 312 } 313 } 314 315 if(typeof type !== "string") { 316 throw(new Error("loadTranslator: type must be a string")); 317 return; 318 } 319 320 Zotero.debug("Translate: Creating translate instance of type "+type+" in sandbox"); 321 var translation = Zotero.Translate.newInstance(type); 322 translation._parentTranslator = translate; 323 324 if(translation instanceof Zotero.Translate.Export && !(translation instanceof Zotero.Translate.Export)) { 325 throw(new Error("Only export translators may call other export translators")); 326 } 327 328 /** 329 * @class Wrapper for {@link Zotero.Translate} for safely calling another translator 330 * from inside an existing translator 331 * @inner 332 */ 333 var safeTranslator = {}; 334 safeTranslator.__exposedProps__ = { 335 "setSearch":"r", 336 "setDocument":"r", 337 "setHandler":"r", 338 "setString":"r", 339 "setTranslator":"r", 340 "getTranslators":"r", 341 "translate":"r", 342 "getTranslatorObject":"r" 343 }; 344 safeTranslator.setSearch = function(arg) { 345 if(!Zotero.isBookmarklet) arg = JSON.parse(JSON.stringify(arg)); 346 return translation.setSearch(arg); 347 }; 348 safeTranslator.setDocument = function(arg) { 349 if (Zotero.isFx && !Zotero.isBookmarklet) { 350 return translation.setDocument( 351 Zotero.Translate.DOMWrapper.wrap(arg, arg.SpecialPowers_wrapperOverrides) 352 ); 353 } else { 354 return translation.setDocument(arg); 355 } 356 }; 357 var errorHandlerSet = false; 358 safeTranslator.setHandler = function(arg1, arg2) { 359 if(arg1 === "error") errorHandlerSet = true; 360 translation.setHandler(arg1, 361 function(obj, item) { 362 try { 363 item = item.wrappedJSObject ? item.wrappedJSObject : item; 364 if(arg1 == "itemDone") { 365 item.complete = translate._sandboxZotero.Item.prototype.complete; 366 } else if(arg1 == "translators" && Zotero.isFx && !Zotero.isBookmarklet) { 367 var translators = new translate._sandboxManager.sandbox.Array(); 368 translators = translators.wrappedJSObject || translators; 369 for (var i=0; i<item.length; i++) { 370 translators.push(item[i]); 371 } 372 item = translators; 373 } 374 arg2(obj, item); 375 } catch(e) { 376 translate.complete(false, e); 377 } 378 } 379 ); 380 }; 381 safeTranslator.setString = function(arg) { translation.setString(arg) }; 382 safeTranslator.setTranslator = function(arg) { 383 var success = translation.setTranslator(arg); 384 if(!success) { 385 throw new Error("Translator "+translate.translator[0].translatorID+" attempted to call invalid translatorID "+arg); 386 } 387 }; 388 389 var translatorsHandlerSet = false; 390 safeTranslator.getTranslators = function() { 391 if(!translation._handlers["translators"] || !translation._handlers["translators"].length) { 392 throw new Error('Translator must register a "translators" handler to '+ 393 'call getTranslators() in this translation environment.'); 394 } 395 if(!translatorsHandlerSet) { 396 translation.setHandler("translators", function() { 397 translate.decrementAsyncProcesses("safeTranslator#getTranslators()"); 398 }); 399 } 400 translate.incrementAsyncProcesses("safeTranslator#getTranslators()"); 401 return translation.getTranslators(); 402 }; 403 404 var doneHandlerSet = false; 405 safeTranslator.translate = function() { 406 translate.incrementAsyncProcesses("safeTranslator#translate()"); 407 setDefaultHandlers(translate, translation); 408 if(!doneHandlerSet) { 409 doneHandlerSet = true; 410 translation.setHandler("done", function() { translate.decrementAsyncProcesses("safeTranslator#translate()") }); 411 } 412 if(!errorHandlerSet) { 413 errorHandlerSet = true; 414 translation.setHandler("error", function(obj, error) { translate.complete(false, error) }); 415 } 416 translation.translate(false); 417 }; 418 419 safeTranslator.getTranslatorObject = function(callback) { 420 if(callback) { 421 translate.incrementAsyncProcesses("safeTranslator#getTranslatorObject()"); 422 } else { 423 throw new Error("Translator must pass a callback to getTranslatorObject() to "+ 424 "operate in this translation environment."); 425 } 426 427 var translator = translation.translator[0]; 428 translator = typeof translator === "object" ? translator : Zotero.Translators.get(translator); 429 // Zotero.Translators.get returns a value in the client and a promise in connectors 430 // so we normalize the value to a promise here 431 Zotero.Promise.resolve(translator) 432 .then(function(translator) { 433 return translation._loadTranslator(translator) 434 }) 435 .then(function() { 436 if(Zotero.isFx && !Zotero.isBookmarklet) { 437 // do same origin check 438 var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"] 439 .getService(Components.interfaces.nsIScriptSecurityManager); 440 var ioService = Components.classes["@mozilla.org/network/io-service;1"] 441 .getService(Components.interfaces.nsIIOService); 442 443 var outerSandboxURI = ioService.newURI(typeof translate._sandboxLocation === "object" ? 444 translate._sandboxLocation.location : translate._sandboxLocation, null, null); 445 var innerSandboxURI = ioService.newURI(typeof translation._sandboxLocation === "object" ? 446 translation._sandboxLocation.location : translation._sandboxLocation, null, null); 447 448 try { 449 secMan.checkSameOriginURI(outerSandboxURI, innerSandboxURI, false); 450 } catch(e) { 451 throw new Error("getTranslatorObject() may not be called from web or search "+ 452 "translators to web or search translators from different origins."); 453 return; 454 } 455 } 456 457 return translation._prepareTranslation(); 458 }) 459 .then(function () { 460 setDefaultHandlers(translate, translation); 461 var sandbox = translation._sandboxManager.sandbox; 462 if(!Zotero.Utilities.isEmpty(sandbox.exports)) { 463 sandbox.exports.Zotero = sandbox.Zotero; 464 sandbox = sandbox.exports; 465 } else { 466 translate._debug("COMPAT WARNING: "+translation.translator[0].label+" does "+ 467 "not export any properties. Only detect"+translation._entryFunctionSuffix+ 468 " and do"+translation._entryFunctionSuffix+" will be available in "+ 469 "connectors."); 470 } 471 472 callback(sandbox); 473 translate.decrementAsyncProcesses("safeTranslator#getTranslatorObject()"); 474 }).catch(function(e) { 475 translate.complete(false, e); 476 return; 477 }); 478 }; 479 480 if (Zotero.isFx) { 481 for(var i in safeTranslator) { 482 if (typeof(safeTranslator[i]) === "function") { 483 safeTranslator[i] = translate._sandboxManager._makeContentForwarder(function(func) { 484 return function() { 485 func.apply(safeTranslator, this.args.wrappedJSObject || this.args); 486 } 487 }(safeTranslator[i])); 488 } 489 } 490 } 491 492 return safeTranslator; 493 }, 494 495 /** 496 * Enables asynchronous detection or translation 497 * @param {Zotero.Translate} translate 498 * @deprecated 499 */ 500 "wait":function(translate) {}, 501 502 /** 503 * Sets the return value for detection 504 * 505 * @param {Zotero.Translate} translate 506 */ 507 "done":function(translate, returnValue) { 508 if(translate._currentState === "detect") { 509 translate._returnValue = returnValue; 510 } 511 }, 512 513 /** 514 * Proxy for translator _debug function 515 * 516 * @param {Zotero.Translate} translate 517 * @param {String} string String to write to console 518 * @param {String} [level] Level to log as (1 to 5) 519 */ 520 "debug":function(translate, string, level) { 521 translate._debug(string, level); 522 } 523 }, 524 525 /** 526 * Web functions exposed to sandbox 527 * @namespace 528 */ 529 "Web":{ 530 /** 531 * Lets user pick which items s/he wants to put in his/her library 532 * @param {Zotero.Translate} translate 533 * @param {Object} items An set of id => name pairs in object format 534 */ 535 "selectItems":function(translate, items, callback) { 536 function transferObject(obj) { 537 return Zotero.isFx && !Zotero.isBookmarklet ? translate._sandboxManager.copyObject(obj) : obj; 538 } 539 540 if(Zotero.Utilities.isEmpty(items)) { 541 throw new Error("Translator called select items with no items"); 542 } 543 544 // Some translators pass an array rather than an object to Zotero.selectItems. 545 // This will break messaging outside of Firefox, so we need to fix it. 546 if(Object.prototype.toString.call(items) === "[object Array]") { 547 translate._debug("WARNING: Zotero.selectItems should be called with an object, not an array"); 548 var itemsObj = {}; 549 for(var i in items) itemsObj[i] = items[i]; 550 items = itemsObj; 551 } 552 553 if(translate._selectedItems) { 554 // if we have a set of selected items for this translation, use them 555 return transferObject(translate._selectedItems); 556 } else if(translate._handlers.select) { 557 // whether the translator supports asynchronous selectItems 558 var haveAsyncCallback = !!callback; 559 // whether the handler operates asynchronously 560 var haveAsyncHandler = false; 561 var returnedItems = null; 562 563 var callbackExecuted = false; 564 if(haveAsyncCallback) { 565 // if this translator provides an async callback for selectItems, rig things 566 // up to pop off the async process 567 var newCallback = function(selectedItems) { 568 callbackExecuted = true; 569 callback(transferObject(selectedItems)); 570 if(haveAsyncHandler) translate.decrementAsyncProcesses("Zotero.selectItems()"); 571 }; 572 } else { 573 // if this translator doesn't provide an async callback for selectItems, set things 574 // up so that we can wait to see if the select handler returns synchronously. If it 575 // doesn't, we will need to restart translation. 576 var newCallback = function(selectedItems) { 577 callbackExecuted = true; 578 if(haveAsyncHandler) { 579 translate.translate({ 580 libraryID: translate._libraryID, 581 saveAttachments: translate._saveAttachments, 582 selectedItems 583 }); 584 } else { 585 returnedItems = transferObject(selectedItems); 586 } 587 }; 588 } 589 590 if(Zotero.isFx && !Zotero.isBookmarklet) { 591 items = Components.utils.cloneInto(items, {}); 592 } 593 594 var returnValue = translate._runHandler("select", items, newCallback); 595 if(returnValue !== undefined) { 596 // handler may have returned a value, which makes callback unnecessary 597 Zotero.debug("WARNING: Returning items from a select handler is deprecated. "+ 598 "Please pass items as to the callback provided as the third argument to "+ 599 "the handler."); 600 601 returnedItems = transferObject(returnValue); 602 haveAsyncHandler = false; 603 } else { 604 // if we don't have returnedItems set already, the handler is asynchronous 605 haveAsyncHandler = !callbackExecuted; 606 } 607 608 if(haveAsyncCallback) { 609 if(haveAsyncHandler) { 610 // we are running asynchronously, so increment async processes 611 translate.incrementAsyncProcesses("Zotero.selectItems()"); 612 } else if(!callbackExecuted) { 613 // callback didn't get called from handler, so call it here 614 callback(returnedItems); 615 } 616 return false; 617 } else { 618 translate._debug("COMPAT WARNING: No callback was provided for "+ 619 "Zotero.selectItems(). When executed outside of Firefox, a selectItems() call "+ 620 "will require this translator to be called multiple times.", 1); 621 622 if(haveAsyncHandler) { 623 // The select handler is asynchronous, but this translator doesn't support 624 // asynchronous select. We return false to abort translation in this 625 // instance, and we will restart it later when the selectItems call is 626 // complete. 627 translate._aborted = true; 628 return false; 629 } else { 630 return returnedItems; 631 } 632 } 633 } else { // no handler defined; assume they want all of them 634 if(callback) callback(items); 635 return items; 636 } 637 }, 638 639 /** 640 * Overloads {@link Zotero.Translate.Sandbox.Base._itemDone} to ensure that no standalone 641 * items are saved, that an item type is specified, and to add a libraryCatalog and 642 * shortTitle if relevant. 643 * @param {Zotero.Translate} translate 644 * @param {SandboxItem} An item created using the Zotero.Item class from the sandbox 645 */ 646 "_itemDone":function(translate, item) { 647 // Only apply checks if there is no parent translator 648 if(!translate._parentTranslator) { 649 if(!item.itemType) { 650 item.itemType = "webpage"; 651 translate._debug("WARNING: No item type specified"); 652 } 653 654 if(item.type == "attachment" || item.type == "note") { 655 Zotero.debug("Translate: Discarding standalone "+item.type+" in non-import translator", 2); 656 return; 657 } 658 659 // store library catalog if this item was captured from a website, and 660 // libraryCatalog is truly undefined (not false or "") 661 if(item.repository !== undefined) { 662 Zotero.debug("Translate: 'repository' field is now 'libraryCatalog'; please fix your code", 2); 663 item.libraryCatalog = item.repository; 664 delete item.repository; 665 } 666 667 // automatically set library catalog 668 if(item.libraryCatalog === undefined && item.itemType != "webpage") { 669 item.libraryCatalog = translate.translator[0].label; 670 } 671 672 // automatically set access date if URL is set 673 if(item.url && typeof item.accessDate == 'undefined') { 674 item.accessDate = Zotero.Date.dateToISO(new Date()); 675 } 676 677 //consider type-specific "title" alternatives 678 var altTitle = Zotero.ItemFields.getName(Zotero.ItemFields.getFieldIDFromTypeAndBase(item.itemType, 'title')); 679 if(altTitle && item[altTitle]) item.title = item[altTitle]; 680 681 if(!item.title) { 682 translate.complete(false, new Error("No title specified for item")); 683 return; 684 } 685 686 // create short title 687 if(item.shortTitle === undefined && Zotero.Utilities.fieldIsValidForType("shortTitle", item.itemType)) { 688 // only set if changes have been made 689 var setShortTitle = false; 690 var title = item.title; 691 692 // shorten to before first colon 693 var index = title.indexOf(":"); 694 if(index !== -1) { 695 title = title.substr(0, index); 696 setShortTitle = true; 697 } 698 // shorten to after first question mark 699 index = title.indexOf("?"); 700 if(index !== -1) { 701 index++; 702 if(index != title.length) { 703 title = title.substr(0, index); 704 setShortTitle = true; 705 } 706 } 707 708 if(setShortTitle) item.shortTitle = title; 709 } 710 711 /* Clean up ISBNs 712 * Allow multiple ISBNs, but... 713 * (1) validate all ISBNs 714 * (2) convert all ISBNs to ISBN-13 715 * (3) remove any duplicates 716 * (4) separate them with space 717 */ 718 if (item.ISBN) { 719 // Match ISBNs with groups separated by various dashes or even spaces 720 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, 721 validISBNs = [], 722 isbn; 723 while (isbn = isbnRe.exec(item.ISBN)) { 724 var validISBN = Zotero.Utilities.cleanISBN(isbn[0]); 725 if (!validISBN) { 726 // Back up and move up one character 727 isbnRe.lastIndex = isbn.index + 1; 728 continue; 729 } 730 731 var isbn13 = Zotero.Utilities.toISBN13(validISBN); 732 if (validISBNs.indexOf(isbn13) == -1) validISBNs.push(isbn13); 733 } 734 item.ISBN = validISBNs.join(' '); 735 } 736 737 // refuse to save very long tags 738 if(item.tags) { 739 for(var i=0; i<item.tags.length; i++) { 740 var tag = item.tags[i], 741 tagString = typeof tag === "string" ? tag : 742 typeof tag === "object" ? (tag.tag || tag.name) : null; 743 if(tagString && tagString.length > 255) { 744 translate._debug("WARNING: Skipping unsynchable tag "+JSON.stringify(tagString)); 745 item.tags.splice(i--, 1); 746 } 747 } 748 } 749 750 for(var i=0; i<item.attachments.length; i++) { 751 var attachment = item.attachments[i]; 752 753 // Web translators are not allowed to use attachment.path 754 if (attachment.path) { 755 if (!attachment.url) attachment.url = attachment.path; 756 delete attachment.path; 757 } 758 759 if(attachment.url) { 760 // Remap attachment (but not link) URLs 761 // TODO: provide both proxied and un-proxied URLs (also for documents) 762 // because whether the attachment is attached as link or file 763 // depends on Zotero preferences as well. 764 attachment.url = translate.resolveURL(attachment.url, attachment.snapshot === false); 765 } 766 } 767 } 768 769 // call super 770 Zotero.Translate.Sandbox.Base._itemDone(translate, item); 771 }, 772 773 /** 774 * Tells Zotero to monitor changes to the DOM and re-trigger detectWeb 775 * Can only be set during the detectWeb call 776 * @param {DOMNode} target Document node to monitor for changes 777 * @param {MutationObserverInit} [config] specifies which DOM mutations should be reported 778 */ 779 "monitorDOMChanges":function(translate, target, config) { 780 if(translate._currentState != "detect") { 781 Zotero.debug("Translate: monitorDOMChanges can only be called during the 'detect' stage"); 782 return; 783 } 784 785 var window = translate.document.defaultView 786 var mutationObserver = window && ( window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver ); 787 if(!mutationObserver) { 788 Zotero.debug("Translate: This browser does not support mutation observers."); 789 return; 790 } 791 792 var translator = translate._potentialTranslators[0]; 793 if(!translate._registeredDOMObservers[translator.translatorID]) 794 translate._registeredDOMObservers[translator.translatorID] = []; 795 var obs = translate._registeredDOMObservers[translator.translatorID]; 796 797 //do not re-register observer by the same translator for the same node 798 if(obs.indexOf(target) != -1) { 799 Zotero.debug("Translate: Already monitoring this node"); 800 return; 801 } 802 803 obs.push(target); 804 805 var observer = new mutationObserver(function(mutations, observer) { 806 obs.splice(obs.indexOf(target),1); 807 observer.disconnect(); 808 809 Zotero.debug("Translate: Page modified."); 810 //we don't really care what got updated 811 var doc = mutations[0].target.ownerDocument; 812 translate._runHandler("pageModified", doc); 813 }); 814 815 observer.observe(target, config || {childList: true, subtree: true}); 816 Zotero.debug("Translate: Mutation observer registered on <" + target.nodeName + "> node"); 817 } 818 }, 819 820 /** 821 * Import functions exposed to sandbox 822 * @namespace 823 */ 824 "Import":{ 825 /** 826 * Saves a collection to the DB 827 * Called as {@link Zotero.Collection#complete} from the sandbox 828 * @param {Zotero.Translate} translate 829 * @param {SandboxCollection} collection 830 */ 831 "_collectionDone":function(translate, collection) { 832 translate.newCollections.push(collection); 833 if(translate._libraryID == false) { 834 translate._runHandler("collectionDone", collection); 835 } 836 }, 837 838 /** 839 * Sets the value of the progress indicator associated with export as a percentage 840 * @param {Zotero.Translate} translate 841 * @param {Number} value 842 */ 843 "setProgress":function(translate, value) { 844 if(typeof value !== "number") { 845 translate._progress = null; 846 } else { 847 translate._progress = value; 848 } 849 } 850 }, 851 852 /** 853 * Export functions exposed to sandbox 854 * @namespace 855 */ 856 "Export":{ 857 /** 858 * Retrieves the next item to be exported 859 * @param {Zotero.Translate} translate 860 * @return {SandboxItem} 861 */ 862 "nextItem":function(translate) { 863 var item = translate._itemGetter.nextItem(); 864 865 if(translate._displayOptions.hasOwnProperty("exportTags") && !translate._displayOptions["exportTags"]) { 866 item.tags = []; 867 } 868 869 translate._runHandler("itemDone", item); 870 871 return item; 872 }, 873 874 /** 875 * Retrieves the next collection to be exported 876 * @param {Zotero.Translate} translate 877 * @return {SandboxCollection} 878 */ 879 "nextCollection":function(translate) { 880 if(!translate._translatorInfo.configOptions || !translate._translatorInfo.configOptions.getCollections) { 881 throw(new Error("getCollections configure option not set; cannot retrieve collection")); 882 } 883 884 return translate._itemGetter.nextCollection(); 885 }, 886 887 /** 888 * @borrows Zotero.Translate.Sandbox.Import.setProgress as this.setProgress 889 */ 890 "setProgress":function(translate, value) { 891 Zotero.Translate.Sandbox.Import.setProgress(translate, value); 892 } 893 }, 894 895 /** 896 * Search functions exposed to sandbox 897 * @namespace 898 */ 899 "Search":{ 900 /** 901 * @borrows Zotero.Translate.Sandbox.Web._itemDone as this._itemDone 902 */ 903 "_itemDone":function(translate, item) { 904 // Always set library catalog, even if we have a parent translator 905 if(item.libraryCatalog === undefined) { 906 item.libraryCatalog = translate.translator[0].label; 907 } 908 909 Zotero.Translate.Sandbox.Web._itemDone(translate, item); 910 } 911 } 912 } 913 914 /** 915 * @class Base class for all translation types 916 * 917 * @property {String} type The type of translator. This is deprecated; use instanceof instead. 918 * @property {Zotero.Translator[]} translator The translator currently in use. Usually, only the 919 * first entry of the Zotero.Translator array is populated; subsequent entries represent 920 * translators to be used if the first fails. 921 * @property {String} path The path or URI string of the target 922 * @property {String} newItems Items created when translate() was called 923 * @property {String} newCollections Collections created when translate() was called 924 * @property {Number} runningAsyncProcesses The number of async processes that are running. These 925 * need to terminate before Zotero.done() is called. 926 */ 927 Zotero.Translate.Base = function() {} 928 Zotero.Translate.Base.prototype = { 929 /** 930 * Initializes a Zotero.Translate instance 931 */ 932 "init":function() { 933 this._handlers = []; 934 this._currentState = null; 935 this._translatorInfo = null; 936 this.document = null; 937 this.location = null; 938 }, 939 940 /** 941 * Sets the location to operate upon 942 * 943 * @param {String|nsIFile} location The URL to which the sandbox should be bound or path to local file 944 */ 945 "setLocation":function(location) { 946 this.location = location; 947 if(typeof this.location == "object") { // if a file 948 this.path = location.path; 949 } else { // if a url 950 this.path = location; 951 } 952 }, 953 954 /** 955 * Sets the translator to be used for import/export 956 * 957 * @param {Zotero.Translator|string} Translator object or ID 958 */ 959 "setTranslator":function(translator) { 960 if(!translator) { 961 throw new Error("No translator specified"); 962 } 963 964 this.translator = null; 965 966 if(typeof(translator) == "object") { // passed an object and not an ID 967 if(translator.translatorID) { 968 this.translator = [translator]; 969 } else { 970 throw(new Error("No translatorID specified")); 971 } 972 } else { 973 this.translator = [translator]; 974 } 975 976 return !!this.translator; 977 }, 978 979 /** 980 * Registers a handler function to be called when translation is complete 981 * 982 * @param {String} type Type of handler to register. Legal values are: 983 * select 984 * valid: web 985 * called: when the user needs to select from a list of available items 986 * passed: an associative array in the form id => text 987 * returns: a numerically indexed array of ids, as extracted from the passed 988 * string 989 * itemDone 990 * valid: import, web, search 991 * called: when an item has been processed; may be called asynchronously 992 * passed: an item object (see Zotero.Item) 993 * returns: N/A 994 * collectionDone 995 * valid: import 996 * called: when a collection has been processed, after all items have been 997 * added; may be called asynchronously 998 * passed: a collection object (see Zotero.Collection) 999 * returns: N/A 1000 * done 1001 * valid: all 1002 * called: when all processing is finished 1003 * passed: true if successful, false if an error occurred 1004 * returns: N/A 1005 * debug 1006 * valid: all 1007 * called: when Zotero.debug() is called 1008 * passed: string debug message 1009 * returns: true if message should be logged to the console, false if not 1010 * error 1011 * valid: all 1012 * called: when a fatal error occurs 1013 * passed: error object (or string) 1014 * returns: N/A 1015 * translators 1016 * valid: all 1017 * called: when a translator search initiated with Zotero.Translate.getTranslators() is 1018 * complete 1019 * passed: an array of appropriate translators 1020 * returns: N/A 1021 * pageModified 1022 * valid: web 1023 * called: when a web page has been modified 1024 * passed: the document object for the modified page 1025 * returns: N/A 1026 * @param {Function} handler Callback function. All handlers will be passed the current 1027 * translate instance as the first argument. The second argument is dependent on the handler. 1028 */ 1029 "setHandler":function(type, handler) { 1030 if(!this._handlers[type]) { 1031 this._handlers[type] = new Array(); 1032 } 1033 this._handlers[type].push(handler); 1034 }, 1035 1036 /** 1037 * Clears all handlers for a given function 1038 * @param {String} type See {@link Zotero.Translate.Base#setHandler} for valid values 1039 */ 1040 "clearHandlers":function(type) { 1041 this._handlers[type] = new Array(); 1042 }, 1043 1044 /** 1045 * Clears a single handler for a given function 1046 * @param {String} type See {@link Zotero.Translate.Base#setHandler} for valid values 1047 * @param {Function} handler Callback function to remove 1048 */ 1049 "removeHandler":function(type, handler) { 1050 var handlerIndex = this._handlers[type].indexOf(handler); 1051 if(handlerIndex !== -1) this._handlers[type].splice(handlerIndex, 1); 1052 }, 1053 1054 /** 1055 * Indicates that a new async process is running 1056 */ 1057 "incrementAsyncProcesses":function(f) { 1058 this._runningAsyncProcesses++; 1059 if(this._parentTranslator) { 1060 this._parentTranslator.incrementAsyncProcesses(f+" from child translator"); 1061 } else { 1062 //Zotero.debug("Translate: Incremented asynchronous processes to "+this._runningAsyncProcesses+" for "+f, 4); 1063 //Zotero.debug((new Error()).stack); 1064 } 1065 }, 1066 1067 /** 1068 * Indicates that a new async process is finished 1069 */ 1070 "decrementAsyncProcesses":function(f, by) { 1071 this._runningAsyncProcesses -= (by ? by : 1); 1072 if(!this._parentTranslator) { 1073 //Zotero.debug("Translate: Decremented asynchronous processes to "+this._runningAsyncProcesses+" for "+f, 4); 1074 //Zotero.debug((new Error()).stack); 1075 } 1076 if(this._runningAsyncProcesses === 0) { 1077 this.complete(); 1078 } 1079 if(this._parentTranslator) this._parentTranslator.decrementAsyncProcesses(f+" from child translator", by); 1080 }, 1081 1082 /** 1083 * Clears all handlers for a given function 1084 * @param {String} type See {@link Zotero.Translate.Base#setHandler} for valid values 1085 * @param {Any} argument Argument to be passed to handler 1086 */ 1087 "_runHandler":function(type) { 1088 var returnValue = undefined; 1089 if(this._handlers[type]) { 1090 // compile list of arguments 1091 if(this._parentTranslator) { 1092 // if there is a parent translator, make sure we don't pass the Zotero.Translate 1093 // object, since it could open a security hole 1094 var args = [null]; 1095 } else { 1096 var args = [this]; 1097 } 1098 for(var i=1; i<arguments.length; i++) { 1099 args.push(arguments[i]); 1100 } 1101 1102 var handlers = this._handlers[type].slice(); 1103 for(var i=0, n=handlers.length; i<n; i++) { 1104 if (type != 'debug') { 1105 Zotero.debug(`Translate: Running handler ${i} for ${type}`, 5); 1106 } 1107 try { 1108 returnValue = handlers[i].apply(null, args); 1109 } catch(e) { 1110 if(this._parentTranslator) { 1111 // throw handler errors if they occur when a translator is 1112 // called from another translator, so that the 1113 // "Could Not Translate" dialog will appear if necessary 1114 throw(e); 1115 } else { 1116 // otherwise, fail silently, so as not to interfere with 1117 // interface cleanup 1118 Zotero.debug("Translate: "+e+' in handler '+i+' for '+type, 5); 1119 Zotero.logError(e); 1120 } 1121 } 1122 } 1123 } 1124 return returnValue; 1125 }, 1126 1127 /** 1128 * Gets all applicable translators of a given type 1129 * 1130 * For import, you should call this after setLocation; otherwise, you'll just get a list of all 1131 * import filters, not filters equipped to handle a specific file 1132 * 1133 * @param {Boolean} [getAllTranslators] Whether all applicable translators should be returned, 1134 * rather than just the first available. 1135 * @param {Boolean} [checkSetTranslator] If true, the appropriate detect function is run on the 1136 * set document/text/etc. using the translator set by setTranslator. 1137 * getAllTranslators parameter is meaningless in this context. 1138 * @return {Promise} Promise for an array of {@link Zotero.Translator} objects 1139 */ 1140 getTranslators: Zotero.Promise.method(function (getAllTranslators, checkSetTranslator) { 1141 var potentialTranslators; 1142 1143 // do not allow simultaneous instances of getTranslators 1144 if(this._currentState === "detect") throw new Error("getTranslators: detection is already running"); 1145 this._currentState = "detect"; 1146 this._getAllTranslators = getAllTranslators; 1147 this._potentialTranslators = []; 1148 this._foundTranslators = []; 1149 1150 if(checkSetTranslator) { 1151 // setTranslator must be called beforehand if checkSetTranslator is set 1152 if( !this.translator || !this.translator[0] ) { 1153 return Zotero.Promise.reject(new Error("getTranslators: translator must be set via setTranslator before calling" + 1154 " getTranslators with the checkSetTranslator flag")); 1155 } 1156 var promises = new Array(); 1157 var t; 1158 for(var i=0, n=this.translator.length; i<n; i++) { 1159 if(typeof(this.translator[i]) == 'string') { 1160 t = Zotero.Translators.get(this.translator[i]); 1161 if(!t) Zotero.debug("getTranslators: could not retrieve translator '" + this.translator[i] + "'"); 1162 } else { 1163 t = this.translator[i]; 1164 } 1165 /**TODO: check that the translator is of appropriate type?*/ 1166 if(t) promises.push(t); 1167 } 1168 if(!promises.length) return Zotero.Promise.reject(new Error("getTranslators: no valid translators were set")); 1169 potentialTranslators = Zotero.Promise.all(promises); 1170 } else { 1171 potentialTranslators = this._getTranslatorsGetPotentialTranslators(); 1172 } 1173 1174 // if detection returns immediately, return found translators 1175 return potentialTranslators.then(function(result) { 1176 var allPotentialTranslators = result[0]; 1177 var proxies = result[1]; 1178 1179 // this gets passed out by Zotero.Translators.getWebTranslatorsForLocation() because it is 1180 // specific for each translator, but we want to avoid making a copy of a translator whenever 1181 // possible. 1182 this._proxies = proxies ? [] : null; 1183 this._waitingForRPC = false; 1184 1185 for(var i=0, n=allPotentialTranslators.length; i<n; i++) { 1186 var translator = allPotentialTranslators[i]; 1187 if(translator.runMode === Zotero.Translator.RUN_MODE_IN_BROWSER) { 1188 this._potentialTranslators.push(translator); 1189 if (proxies) { 1190 this._proxies.push(proxies[i]); 1191 } 1192 } else if (this instanceof Zotero.Translate.Web && Zotero.Connector) { 1193 this._waitingForRPC = true; 1194 } 1195 } 1196 1197 // Attach handler for translators, so that we can return a 1198 // promise that provides them. 1199 // TODO make this._detect() return a promise 1200 var deferred = Zotero.Promise.defer(); 1201 var translatorsHandler = function(obj, translators) { 1202 this.removeHandler("translators", translatorsHandler); 1203 deferred.resolve(translators); 1204 }.bind(this); 1205 this.setHandler("translators", translatorsHandler); 1206 this._detect(); 1207 1208 if(this._waitingForRPC) { 1209 // Try detect in Zotero Standalone. If this fails, it fails; we shouldn't 1210 // get hung up about it. 1211 Zotero.Connector.callMethod( 1212 "detect", 1213 { 1214 uri: this.location.toString(), 1215 cookie: this.document.cookie, 1216 html: this.document.documentElement.innerHTML 1217 }).catch(() => false).then(function (rpcTranslators) { 1218 this._waitingForRPC = false; 1219 1220 // if there are translators, add them to the list of found translators 1221 if (rpcTranslators) { 1222 for(var i=0, n=rpcTranslators.length; i<n; i++) { 1223 rpcTranslators[i] = new Zotero.Translator(rpcTranslators[i]); 1224 rpcTranslators[i].runMode = Zotero.Translator.RUN_MODE_ZOTERO_STANDALONE; 1225 rpcTranslators[i].proxy = rpcTranslators[i].proxy ? new Zotero.Proxy(rpcTranslators[i].proxy) : null; 1226 } 1227 this._foundTranslators = this._foundTranslators.concat(rpcTranslators); 1228 } 1229 1230 // call _detectTranslatorsCollected to return detected translators 1231 if (this._currentState === null) { 1232 this._detectTranslatorsCollected(); 1233 } 1234 }.bind(this)); 1235 } 1236 1237 return deferred.promise; 1238 }.bind(this)) 1239 .catch(function(e) { 1240 Zotero.logError(e); 1241 this.complete(false, e); 1242 }.bind(this)); 1243 }), 1244 1245 /** 1246 * Get all potential translators (without running detect) 1247 * @return {Promise} Promise for an array of {@link Zotero.Translator} objects 1248 */ 1249 "_getTranslatorsGetPotentialTranslators":function() { 1250 return Zotero.Translators.getAllForType(this.type). 1251 then(function(translators) { return [translators] }); 1252 }, 1253 1254 /** 1255 * Begins the actual translation. At present, this returns immediately for import/export 1256 * translators, but new code should use {@link Zotero.Translate.Base#setHandler} to register a 1257 * "done" handler to determine when execution of web/search translators is complete. 1258 * 1259 * @param {Integer|FALSE} [libraryID] Library in which to save items, 1260 * or NULL for default library; 1261 * if FALSE, don't save items 1262 * @param {Boolean} [saveAttachments=true] Exclude attachments (e.g., snapshots) on import 1263 * @returns {Promise} Promise resolved with saved items 1264 * when translation complete 1265 */ 1266 translate: Zotero.Promise.method(function (options = {}, ...args) { // initialize properties specific to each translation 1267 if (typeof options == 'number') { 1268 Zotero.debug("Translate: translate() now takes an object -- update your code", 2); 1269 options = { 1270 libraryID: options, 1271 saveAttachments: args[0], 1272 selectedItems: args[1] 1273 }; 1274 } 1275 1276 var me = this; 1277 var deferred = Zotero.Promise.defer() 1278 1279 if(!this.translator || !this.translator.length) { 1280 Zotero.debug("Translate: translate called without specifying a translator. Running detection first."); 1281 this.setHandler('translators', function(me, translators) { 1282 if(!translators.length) { 1283 me.complete(false, "Could not find an appropriate translator"); 1284 } else { 1285 me.setTranslator(translators); 1286 deferred.resolve(Zotero.Translate.Base.prototype.translate.call(me, options)); 1287 } 1288 }); 1289 this.getTranslators(); 1290 return deferred.promise; 1291 } 1292 1293 this._currentState = "translate"; 1294 1295 this._sessionID = options.sessionID; 1296 this._libraryID = options.libraryID; 1297 if (options.collections && !Array.isArray(options.collections)) { 1298 throw new Error("'collections' must be an array"); 1299 } 1300 this._collections = options.collections; 1301 this._saveAttachments = options.saveAttachments === undefined || options.saveAttachments; 1302 this._forceTagType = options.forceTagType; 1303 this._saveOptions = options.saveOptions; 1304 1305 this._savingAttachments = []; 1306 this._savingItems = 0; 1307 this._waitingForSave = false; 1308 1309 // Attach handlers for promise 1310 var me = this; 1311 var doneHandler = function (obj, returnValue) { 1312 if (returnValue) deferred.resolve(me.newItems); 1313 me.removeHandler("done", doneHandler); 1314 me.removeHandler("error", errorHandler); 1315 }; 1316 var errorHandler = function (obj, error) { 1317 deferred.reject(error); 1318 me.removeHandler("done", doneHandler); 1319 me.removeHandler("error", errorHandler); 1320 }; 1321 this.setHandler("done", doneHandler); 1322 this.setHandler("error", errorHandler); 1323 1324 // need to get translator first 1325 if (typeof this.translator[0] !== "object") { 1326 this.translator[0] = Zotero.Translators.get(this.translator[0]); 1327 } 1328 1329 // Zotero.Translators.get() returns a promise in the connectors, but we don't expect it to 1330 // otherwise 1331 if (!Zotero.isConnector && this.translator[0].then) { 1332 throw new Error("Translator should not be a promise in non-connector mode"); 1333 } 1334 1335 if (this.noWait) { 1336 var loadPromise = this._loadTranslator(this.translator[0]); 1337 if (!loadPromise.isResolved()) { 1338 return Zotero.Promise.reject(new Error("Load promise is not resolved in noWait mode")); 1339 } 1340 this._translateTranslatorLoaded(); 1341 } 1342 else if (this.translator[0].then) { 1343 Zotero.Promise.resolve(this.translator[0]) 1344 .then(function (translator) { 1345 this.translator[0] = translator; 1346 this._loadTranslator(translator) 1347 .then(() => this._translateTranslatorLoaded()) 1348 .catch(e => deferred.reject(e)); 1349 }.bind(this)); 1350 } 1351 else { 1352 this._loadTranslator(this.translator[0]) 1353 .then(() => this._translateTranslatorLoaded()) 1354 .catch(e => deferred.reject(e)); 1355 } 1356 1357 return deferred.promise; 1358 }), 1359 1360 /** 1361 * Called when translator has been retrieved and loaded 1362 */ 1363 "_translateTranslatorLoaded": Zotero.Promise.method(function() { 1364 // set display options to default if they don't exist 1365 if(!this._displayOptions) this._displayOptions = this._translatorInfo.displayOptions || {}; 1366 1367 var loadPromise = this._prepareTranslation(); 1368 if (this.noWait) { 1369 if (!loadPromise.isResolved()) { 1370 throw new Error("Load promise is not resolved in noWait mode"); 1371 } 1372 rest.apply(this, arguments); 1373 } else { 1374 return loadPromise.then(() => rest.apply(this, arguments)) 1375 } 1376 1377 function rest() { 1378 Zotero.debug("Translate: Beginning translation with " + this.translator[0].label); 1379 1380 this.incrementAsyncProcesses("Zotero.Translate#translate()"); 1381 1382 // translate 1383 try { 1384 let maybePromise = Function.prototype.apply.call( 1385 this._sandboxManager.sandbox["do" + this._entryFunctionSuffix], 1386 null, 1387 this._getParameters() 1388 ); 1389 // doImport can return a promise to allow for incremental saves (via promise-returning 1390 // item.complete() calls) 1391 if (maybePromise) { 1392 maybePromise 1393 .then(() => this.decrementAsyncProcesses("Zotero.Translate#translate()")) 1394 return; 1395 } 1396 } catch (e) { 1397 this.complete(false, e); 1398 return false; 1399 } 1400 1401 this.decrementAsyncProcesses("Zotero.Translate#translate()"); 1402 } 1403 }), 1404 1405 /** 1406 * Return the progress of the import operation, or null if progress cannot be determined 1407 */ 1408 "getProgress":function() { return null }, 1409 1410 /** 1411 * Translate a URL to a form that goes through the appropriate proxy, or 1412 * convert a relative URL to an absolute one 1413 * 1414 * @param {String} url 1415 * @param {Boolean} dontUseProxy If true, don't convert URLs to variants 1416 * that use the proxy 1417 * @type String 1418 * @private 1419 */ 1420 "resolveURL":function(url, dontUseProxy) { 1421 Zotero.debug("Translate: resolving URL " + url); 1422 1423 const hostPortRe = /^([A-Z][-A-Z0-9+.]*):\/\/[^\/]+/i; 1424 const allowedSchemes = ['http', 'https', 'ftp']; 1425 1426 var m = url.match(hostPortRe), 1427 resolved; 1428 if (!m) { 1429 // Convert relative URLs to absolute 1430 if(Zotero.isFx && this.location) { 1431 resolved = Components.classes["@mozilla.org/network/io-service;1"]. 1432 getService(Components.interfaces.nsIIOService). 1433 newURI(this.location, "", null).resolve(url); 1434 } else if(Zotero.isNode && this.location) { 1435 resolved = require('url').resolve(this.location, url); 1436 } else if (this.document) { 1437 var a = this.document.createElement('a'); 1438 a.href = url; 1439 resolved = a.href; 1440 } else if (url.indexOf('//') == 0) { 1441 // Protocol-relative URL with no associated web page 1442 // Use HTTP by default 1443 resolved = 'http:' + url; 1444 } else { 1445 throw new Error('Cannot resolve relative URL without an associated web page: ' + url); 1446 } 1447 } else if (allowedSchemes.indexOf(m[1].toLowerCase()) == -1) { 1448 Zotero.debug("Translate: unsupported scheme " + m[1]); 1449 return url; 1450 } else { 1451 resolved = url; 1452 } 1453 1454 Zotero.debug("Translate: resolved to " + resolved); 1455 1456 // convert proxy to proper if applicable 1457 if(!dontUseProxy && this.translator && this.translator[0] 1458 && this._proxy) { 1459 var proxiedURL = this._proxy.toProxy(resolved); 1460 if (proxiedURL != resolved) { 1461 Zotero.debug("Translate: proxified to " + proxiedURL); 1462 } 1463 resolved = proxiedURL; 1464 } 1465 1466 /*var m = hostPortRe.exec(resolved); 1467 if(!m) { 1468 throw new Error("Invalid URL supplied for HTTP request: "+url); 1469 } else if(this._translate.document && this._translate.document.location) { 1470 var loc = this._translate.document.location; 1471 if(this._translate._currentState !== "translate" && loc 1472 && (m[1].toLowerCase() !== loc.protocol.toLowerCase() 1473 || m[2].toLowerCase() !== loc.host.toLowerCase())) { 1474 throw new Error("Attempt to access "+m[1]+"//"+m[2]+" from "+loc.protocol+"//"+loc.host 1475 +" blocked: Cross-site requests are only allowed during translation"); 1476 } 1477 }*/ 1478 1479 return resolved; 1480 }, 1481 1482 /** 1483 * Executed on translator completion, either automatically from a synchronous scraper or as 1484 * done() from an asynchronous scraper. Finishes things up and calls callback function(s). 1485 * @param {Boolean|String} returnValue An item type or a boolean true or false 1486 * @param {String|Exception} [error] An error that occurred during translation. 1487 * @returm {String|NULL} The exception serialized to a string, or null if translation 1488 * completed successfully. 1489 */ 1490 "complete":function(returnValue, error) { 1491 // allow translation to be aborted for re-running after selecting items 1492 if(this._aborted) return; 1493 1494 // Make sure this isn't called twice 1495 if(this._currentState === null) { 1496 if(!returnValue) { 1497 Zotero.debug("Translate: WARNING: Zotero.done() called after translator completion with error"); 1498 Zotero.debug(error); 1499 } else { 1500 var e = new Error(); 1501 Zotero.debug("Translate: WARNING: Zotero.done() called after translation completion. This should never happen. Please examine the stack below."); 1502 Zotero.debug(e.stack); 1503 } 1504 return; 1505 } 1506 1507 // reset async processes and propagate them to parent 1508 if(this._parentTranslator && this._runningAsyncProcesses) { 1509 this._parentTranslator.decrementAsyncProcesses("Zotero.Translate#complete", this._runningAsyncProcesses); 1510 } 1511 this._runningAsyncProcesses = 0; 1512 1513 if(!returnValue && this._returnValue) returnValue = this._returnValue; 1514 1515 var errorString = null; 1516 if(!returnValue && error) errorString = this._generateErrorString(error); 1517 if(this._currentState === "detect") { 1518 if(this._potentialTranslators.length) { 1519 var lastTranslator = this._potentialTranslators.shift(); 1520 var lastProxy = this._proxies ? this._proxies.shift() : null; 1521 1522 if (returnValue) { 1523 var dupeTranslator = {proxy: lastProxy ? new Zotero.Proxy(lastProxy) : null}; 1524 1525 for (var i in lastTranslator) dupeTranslator[i] = lastTranslator[i]; 1526 if (Zotero.isBookmarklet && returnValue === "server") { 1527 // In the bookmarklet, the return value from detectWeb can be "server" to 1528 // indicate the translator should be run on the Zotero server 1529 dupeTranslator.runMode = Zotero.Translator.RUN_MODE_ZOTERO_SERVER; 1530 } else { 1531 // Usually the return value from detectWeb will be either an item type or 1532 // the string "multiple" 1533 dupeTranslator.itemType = returnValue; 1534 } 1535 1536 this._foundTranslators.push(dupeTranslator); 1537 } else if(error) { 1538 this._debug("Detect using "+lastTranslator.label+" failed: \n"+errorString, 2); 1539 } 1540 } 1541 1542 if(this._potentialTranslators.length && (this._getAllTranslators || !returnValue)) { 1543 // more translators to try; proceed to next translator 1544 this._detect(); 1545 } else { 1546 this._currentState = null; 1547 if(!this._waitingForRPC) this._detectTranslatorsCollected(); 1548 } 1549 } else { 1550 // unset return value is equivalent to true 1551 if(returnValue === undefined) returnValue = true; 1552 1553 if(returnValue) { 1554 if(this.saveQueue.length) { 1555 this._waitingForSave = true; 1556 this._saveItems(this.saveQueue) 1557 .catch(e => this._runHandler("error", e)) 1558 .then(() => this.saveQueue = []); 1559 return; 1560 } 1561 this._debug("Translation successful"); 1562 } else { 1563 if(error) { 1564 // report error to console 1565 Zotero.logError(error); 1566 1567 // report error to debug log 1568 this._debug("Translation using "+(this.translator && this.translator[0] && this.translator[0].label ? this.translator[0].label : "no translator")+" failed: \n"+errorString, 2); 1569 } 1570 1571 this._runHandler("error", error); 1572 } 1573 1574 this._currentState = null; 1575 1576 // call handlers 1577 this._runHandler("itemsDone", returnValue); 1578 if(returnValue) { 1579 this._checkIfDone(); 1580 } else { 1581 this._runHandler("done", returnValue); 1582 } 1583 } 1584 1585 return errorString; 1586 }, 1587 1588 /** 1589 * Canonicalize an array of tags such that they are all objects with the tag stored in the 1590 * "tag" property and a type (if specified) is stored in the "type" property 1591 * @returns {Object[]} Array of new tag objects 1592 */ 1593 "_cleanTags":function(tags) { 1594 var newTags = []; 1595 if(!tags) return newTags; 1596 for(var i=0; i<tags.length; i++) { 1597 var tag = tags[i]; 1598 if(!tag) continue; 1599 if(typeof(tag) == "object") { 1600 var tagString = tag.tag || tag.name; 1601 if(tagString) { 1602 var newTag = {"tag":tagString}; 1603 if(tag.type) newTag.type = tag.type; 1604 newTags.push(newTag); 1605 } 1606 } else { 1607 newTags.push({"tag":tag.toString()}); 1608 } 1609 } 1610 return newTags; 1611 }, 1612 1613 /** 1614 * Saves items to the database, taking care to defer attachmentProgress notifications 1615 * until after save 1616 */ 1617 _saveItems: Zotero.Promise.method(function (items) { 1618 var itemDoneEventsDispatched = false; 1619 var deferredProgress = []; 1620 var attachmentsWithProgress = []; 1621 1622 function attachmentCallback(attachment, progress, error) { 1623 var attachmentIndex = this._savingAttachments.indexOf(attachment); 1624 if(progress === false || progress === 100) { 1625 if(attachmentIndex !== -1) { 1626 this._savingAttachments.splice(attachmentIndex, 1); 1627 } 1628 } else if(attachmentIndex === -1) { 1629 this._savingAttachments.push(attachment); 1630 } 1631 1632 if(itemDoneEventsDispatched) { 1633 // itemDone event has already fired, so we can fire attachmentProgress 1634 // notifications 1635 this._runHandler("attachmentProgress", attachment, progress, error); 1636 this._checkIfDone(); 1637 } else { 1638 // Defer until after we fire the itemDone event 1639 deferredProgress.push([attachment, progress, error]); 1640 attachmentsWithProgress.push(attachment); 1641 } 1642 } 1643 1644 return this._itemSaver.saveItems(items.slice(), attachmentCallback.bind(this)) 1645 .then(function(newItems) { 1646 // Remove attachments not being saved from item.attachments 1647 for(var i=0; i<items.length; i++) { 1648 var item = items[i]; 1649 for(var j=0; j<item.attachments.length; j++) { 1650 if(attachmentsWithProgress.indexOf(item.attachments[j]) === -1) { 1651 item.attachments.splice(j--, 1); 1652 } 1653 } 1654 } 1655 1656 // Trigger itemDone events, waiting for them if they return promises 1657 var maybePromises = []; 1658 for(var i=0, nItems = items.length; i<nItems; i++) { 1659 maybePromises.push(this._runHandler("itemDone", newItems[i], items[i])); 1660 } 1661 return Zotero.Promise.all(maybePromises).then(() => newItems); 1662 }.bind(this)) 1663 .then(function (newItems) { 1664 // Specify that itemDone event was dispatched, so that we don't defer 1665 // attachmentProgress notifications anymore 1666 itemDoneEventsDispatched = true; 1667 1668 // Run deferred attachmentProgress notifications 1669 for(var i=0; i<deferredProgress.length; i++) { 1670 this._runHandler("attachmentProgress", deferredProgress[i][0], 1671 deferredProgress[i][1], deferredProgress[i][2]); 1672 } 1673 1674 this._savingItems -= items.length; 1675 this.newItems = this.newItems.concat(newItems); 1676 this._checkIfDone(); 1677 }.bind(this)) 1678 .catch((e) => { 1679 this._savingItems -= items.length; 1680 this.complete(false, e); 1681 throw e; 1682 }); 1683 }), 1684 1685 /** 1686 * Checks if saving done, and if so, fires done event 1687 */ 1688 "_checkIfDone":function() { 1689 if(!this._savingItems && !this._savingAttachments.length && (!this._currentState || this._waitingForSave)) { 1690 if(this.newCollections && this._itemSaver.saveCollections) { 1691 var me = this; 1692 this._itemSaver.saveCollections(this.newCollections) 1693 .then(function (newCollections) { 1694 me.newCollections = newCollections; 1695 me._runHandler("done", true); 1696 }) 1697 .catch(function (err) { 1698 me._runHandler("error", err); 1699 me._runHandler("done", false); 1700 }); 1701 } else { 1702 this._runHandler("done", true); 1703 } 1704 } 1705 }, 1706 1707 /** 1708 * Begins running detect code for a translator, first loading it 1709 */ 1710 "_detect":function() { 1711 // there won't be any translators if we need an RPC call 1712 if(!this._potentialTranslators.length) { 1713 this.complete(true); 1714 return; 1715 } 1716 1717 let lab = this._potentialTranslators[0].label; 1718 this._loadTranslator(this._potentialTranslators[0]) 1719 .then(function() { 1720 return this._detectTranslatorLoaded(); 1721 }.bind(this)) 1722 .catch(function (e) { 1723 this.complete(false, e); 1724 }.bind(this)); 1725 }, 1726 1727 /** 1728 * Runs detect code for a translator 1729 */ 1730 "_detectTranslatorLoaded":function() { 1731 this._prepareDetection(); 1732 1733 this.incrementAsyncProcesses("Zotero.Translate#getTranslators"); 1734 1735 try { 1736 var returnValue = Function.prototype.apply.call(this._sandboxManager.sandbox["detect"+this._entryFunctionSuffix], null, this._getParameters()); 1737 } catch(e) { 1738 this.complete(false, e); 1739 return; 1740 } 1741 1742 if(returnValue !== undefined) this._returnValue = returnValue; 1743 this.decrementAsyncProcesses("Zotero.Translate#getTranslators"); 1744 }, 1745 1746 /** 1747 * Called when all translators have been collected for detection 1748 */ 1749 "_detectTranslatorsCollected":function() { 1750 Zotero.debug("Translate: All translator detect calls and RPC calls complete:"); 1751 this._foundTranslators.sort(function(a, b) { return a.priority-b.priority }); 1752 if (this._foundTranslators.length) { 1753 this._foundTranslators.forEach(function(t) { 1754 Zotero.debug("\t" + t.label + ": " + t.priority); 1755 }); 1756 } else { 1757 Zotero.debug("\tNo suitable translators found"); 1758 } 1759 this._runHandler("translators", this._foundTranslators); 1760 }, 1761 1762 /** 1763 * Loads the translator into its sandbox 1764 * @param {Zotero.Translator} translator 1765 * @return {Promise<Boolean>} Whether the translator could be successfully loaded 1766 */ 1767 "_loadTranslator": Zotero.Promise.method(function (translator) { 1768 var sandboxLocation = this._getSandboxLocation(); 1769 if(!this._sandboxLocation || sandboxLocation !== this._sandboxLocation) { 1770 this._sandboxLocation = sandboxLocation; 1771 this._generateSandbox(); 1772 } 1773 1774 this._currentTranslator = translator; 1775 1776 // Pass on the proxy of the parent translate 1777 if (this._parentTranslator) { 1778 this._proxy = this._parentTranslator._proxy; 1779 } else { 1780 this._proxy = translator.proxy; 1781 } 1782 this._runningAsyncProcesses = 0; 1783 this._returnValue = undefined; 1784 this._aborted = false; 1785 this.saveQueue = []; 1786 1787 var parse = function(code) { 1788 Zotero.debug("Translate: Parsing code for " + translator.label + " " 1789 + "(" + translator.translatorID + ", " + translator.lastUpdated + ")", 4); 1790 this._sandboxManager.eval( 1791 "var exports = {}, ZOTERO_TRANSLATOR_INFO = " + code, 1792 [ 1793 "detect" + this._entryFunctionSuffix, 1794 "do" + this._entryFunctionSuffix, 1795 "exports", 1796 "ZOTERO_TRANSLATOR_INFO" 1797 ], 1798 (translator.file ? translator.file.path : translator.label) 1799 ); 1800 this._translatorInfo = this._sandboxManager.sandbox.ZOTERO_TRANSLATOR_INFO; 1801 }.bind(this); 1802 1803 if (this.noWait) { 1804 try { 1805 let codePromise = translator.getCode(); 1806 if (!codePromise.isResolved()) { 1807 throw new Error("Code promise is not resolved in noWait mode"); 1808 } 1809 parse(codePromise.value()); 1810 } 1811 catch (e) { 1812 this.complete(false, e); 1813 } 1814 } 1815 else { 1816 return translator.getCode() 1817 .then(parse) 1818 .catch(function(e) { 1819 this.complete(false, e); 1820 }.bind(this)); 1821 } 1822 }), 1823 1824 /** 1825 * Generates a sandbox for scraping/scraper detection 1826 */ 1827 "_generateSandbox":function() { 1828 Zotero.debug("Translate: Binding sandbox to "+(typeof this._sandboxLocation == "object" ? this._sandboxLocation.document.location : this._sandboxLocation), 4); 1829 if (this._parentTranslator && this._parentTranslator._sandboxManager.newChild) { 1830 this._sandboxManager = this._parentTranslator._sandboxManager.newChild(); 1831 } else { 1832 this._sandboxManager = new Zotero.Translate.SandboxManager(this._sandboxLocation); 1833 } 1834 const createArrays = "['creators', 'notes', 'tags', 'seeAlso', 'attachments']"; 1835 var src = ""; 1836 if (Zotero.isFx && !Zotero.isBookmarklet) { 1837 src = "var Zotero = {};"; 1838 } 1839 src += "Zotero.Item = function (itemType) {"+ 1840 "var createArrays = "+createArrays+";"+ 1841 "this.itemType = itemType;"+ 1842 "for(var i=0, n=createArrays.length; i<n; i++) {"+ 1843 "this[createArrays[i]] = [];"+ 1844 "}"+ 1845 "};"; 1846 1847 if(this instanceof Zotero.Translate.Export || this instanceof Zotero.Translate.Import) { 1848 src += "Zotero.Collection = function () {};"+ 1849 "Zotero.Collection.prototype.complete = function() { return Zotero._collectionDone(this); };"; 1850 } 1851 1852 src += "Zotero.Item.prototype.complete = function() { return Zotero._itemDone(this); }"; 1853 1854 this._sandboxManager.eval(src); 1855 this._sandboxManager.importObject(this.Sandbox, this); 1856 this._sandboxManager.importObject({"Utilities":new Zotero.Utilities.Translate(this)}); 1857 1858 this._sandboxZotero = this._sandboxManager.sandbox.Zotero; 1859 1860 if(Zotero.isFx) { 1861 if(this._sandboxZotero.wrappedJSObject) this._sandboxZotero = this._sandboxZotero.wrappedJSObject; 1862 } 1863 this._sandboxZotero.Utilities.HTTP = this._sandboxZotero.Utilities; 1864 1865 this._sandboxZotero.isBookmarklet = Zotero.isBookmarklet || false; 1866 this._sandboxZotero.isConnector = Zotero.isConnector || false; 1867 this._sandboxZotero.isServer = Zotero.isServer || false; 1868 this._sandboxZotero.parentTranslator = this._parentTranslator 1869 && this._parentTranslator._currentTranslator ? 1870 this._parentTranslator._currentTranslator.translatorID : null; 1871 1872 // create shortcuts 1873 this._sandboxManager.sandbox.Z = this._sandboxZotero; 1874 this._sandboxManager.sandbox.ZU = this._sandboxZotero.Utilities; 1875 this._transferItem = this._sandboxZotero._transferItem; 1876 1877 // Add web helper functions 1878 if (this.type == 'web') { 1879 this._sandboxManager.sandbox.attr = this._attr.bind(this); 1880 this._sandboxManager.sandbox.text = this._text.bind(this); 1881 } 1882 }, 1883 1884 /** 1885 * Helper function to extract HTML attribute text 1886 */ 1887 _attr: function (selector, attr, index) { 1888 if (typeof arguments[0] == 'string') { 1889 var docOrElem = this.document; 1890 } 1891 // Document or element passed as first argument 1892 else { 1893 // TODO: Warn if Document rather than Element is passed once we drop 4.0 translator 1894 // support 1895 [docOrElem, selector, attr, index] = arguments; 1896 } 1897 var elem = index 1898 ? docOrElem.querySelectorAll(selector).item(index) 1899 : docOrElem.querySelector(selector); 1900 return elem ? elem.getAttribute(attr) : null; 1901 }, 1902 1903 /** 1904 * Helper function to extract HTML element text 1905 */ 1906 _text: function (selector, index) { 1907 if (typeof arguments[0] == 'string') { 1908 var docOrElem = this.document; 1909 } 1910 // Document or element passed as first argument 1911 else { 1912 // TODO: Warn if Document rather than Element is passed once we drop 4.0 translator 1913 // support 1914 [docOrElem, selector, index] = arguments; 1915 } 1916 var elem = index 1917 ? docOrElem.querySelectorAll(selector).item(index) 1918 : docOrElem.querySelector(selector); 1919 return elem ? elem.textContent : null; 1920 }, 1921 1922 /** 1923 * Logs a debugging message 1924 * @param {String} string Debug string to log 1925 * @param {Integer} level Log level (1-5, higher numbers are higher priority) 1926 */ 1927 "_debug":function(string, level) { 1928 if(level !== undefined && typeof level !== "number") { 1929 Zotero.debug("debug: level must be an integer"); 1930 return; 1931 } 1932 1933 // if handler does not return anything explicitly false, show debug 1934 // message in console 1935 if(this._runHandler("debug", string) !== false) { 1936 if(typeof string == "string") string = "Translate: "+string; 1937 Zotero.debug(string, level); 1938 } 1939 }, 1940 /** 1941 * Generates a string from an exception 1942 * @param {String|Exception} error 1943 */ 1944 _generateErrorString: function (error) { 1945 var errorString = error; 1946 if (error.stack && error) { 1947 errorString += "\n\n" + error.stack; 1948 } 1949 if (this.path) { 1950 errorString += `\nurl => ${this.path}`; 1951 } 1952 if (Zotero.Prefs.get("downloadAssociatedFiles")) { 1953 errorString += "\ndownloadAssociatedFiles => true"; 1954 } 1955 if (Zotero.Prefs.get("automaticSnapshots")) { 1956 errorString += "\nautomaticSnapshots => true"; 1957 } 1958 return errorString; 1959 }, 1960 1961 /** 1962 * Determines the location where the sandbox should be bound 1963 * @return {String|document} The location to which to bind the sandbox 1964 */ 1965 "_getSandboxLocation":function() { 1966 return (this._parentTranslator ? this._parentTranslator._sandboxLocation : "http://www.example.com/"); 1967 }, 1968 1969 /** 1970 * Gets parameters to be passed to detect* and do* functions 1971 * @return {Array} A list of parameters 1972 */ 1973 "_getParameters":function() { return []; }, 1974 1975 /** 1976 * No-op for preparing detection 1977 */ 1978 "_prepareDetection":function() {}, 1979 1980 /** 1981 * No-op for preparing translation 1982 */ 1983 "_prepareTranslation": function () { return Zotero.Promise.resolve(); } 1984 } 1985 1986 /** 1987 * @class Web translation 1988 * 1989 * @property {Document} document The document object to be used for web scraping (set with setDocument) 1990 * @property {Zotero.CookieSandbox} cookieSandbox A CookieSandbox to manage cookies for 1991 * this Translate instance. 1992 */ 1993 Zotero.Translate.Web = function() { 1994 this._registeredDOMObservers = {} 1995 this.init(); 1996 } 1997 Zotero.Translate.Web.prototype = new Zotero.Translate.Base(); 1998 Zotero.Translate.Web.prototype.type = "web"; 1999 Zotero.Translate.Web.prototype._entryFunctionSuffix = "Web"; 2000 Zotero.Translate.Web.prototype.Sandbox = Zotero.Translate.Sandbox._inheritFromBase(Zotero.Translate.Sandbox.Web); 2001 2002 /** 2003 * Sets the browser to be used for web translation 2004 * @param {Document} doc An HTML document 2005 */ 2006 Zotero.Translate.Web.prototype.setDocument = function(doc) { 2007 this.document = doc; 2008 try { 2009 this.rootDocument = doc.defaultView.top.document; 2010 } catch (e) { 2011 // Cross-origin frames won't be able to access top.document and will throw an error 2012 } 2013 if (!this.rootDocument) { 2014 this.rootDocument = doc; 2015 } 2016 this.setLocation(doc.location.href, this.rootDocument.location.href); 2017 } 2018 2019 /** 2020 * Sets a Zotero.CookieSandbox to handle cookie management for XHRs initiated from this 2021 * translate instance 2022 * 2023 * @param {Zotero.CookieSandbox} cookieSandbox 2024 */ 2025 Zotero.Translate.Web.prototype.setCookieSandbox = function(cookieSandbox) { 2026 this.cookieSandbox = cookieSandbox; 2027 } 2028 2029 /** 2030 * Sets the location to operate upon 2031 * 2032 * @param {String} location The URL of the page to translate 2033 * @param {String} rootLocation The URL of the root page, within which `location` is embedded 2034 */ 2035 Zotero.Translate.Web.prototype.setLocation = function(location, rootLocation) { 2036 this.location = location; 2037 this.rootLocation = rootLocation || location; 2038 this.path = this.location; 2039 } 2040 2041 /** 2042 * Get potential web translators 2043 */ 2044 Zotero.Translate.Web.prototype._getTranslatorsGetPotentialTranslators = function() { 2045 return Zotero.Translators.getWebTranslatorsForLocation(this.location, this.rootLocation); 2046 } 2047 2048 /** 2049 * Bind sandbox to document being translated 2050 */ 2051 Zotero.Translate.Web.prototype._getSandboxLocation = function() { 2052 if(this._parentTranslator) { 2053 return this._parentTranslator._sandboxLocation; 2054 } else if(this.document.defaultView 2055 && (this.document.defaultView.toString().indexOf("Window") !== -1 2056 || this.document.defaultView.toString().indexOf("XrayWrapper") !== -1)) { 2057 return this.document.defaultView; 2058 } else { 2059 return this.document.location.toString(); 2060 } 2061 } 2062 2063 /** 2064 * Pass document and location to detect* and do* functions 2065 */ 2066 Zotero.Translate.Web.prototype._getParameters = function() { 2067 if (Zotero.Translate.DOMWrapper && Zotero.Translate.DOMWrapper.isWrapped(this.document)) { 2068 return [ 2069 this._sandboxManager.wrap( 2070 Zotero.Translate.DOMWrapper.unwrap(this.document), 2071 null, 2072 this.document.SpecialPowers_wrapperOverrides 2073 ), 2074 this.location 2075 ]; 2076 } else { 2077 return [this.document, this.location]; 2078 } 2079 }; 2080 2081 /** 2082 * Prepare translation 2083 */ 2084 Zotero.Translate.Web.prototype._prepareTranslation = Zotero.Promise.method(function () { 2085 this._itemSaver = new Zotero.Translate.ItemSaver({ 2086 libraryID: this._libraryID, 2087 collections: this._collections, 2088 attachmentMode: Zotero.Translate.ItemSaver[(this._saveAttachments ? "ATTACHMENT_MODE_DOWNLOAD" : "ATTACHMENT_MODE_IGNORE")], 2089 forceTagType: 1, 2090 sessionID: this._sessionID, 2091 cookieSandbox: this._cookieSandbox, 2092 proxy: this._proxy, 2093 baseURI: this.location 2094 }); 2095 this.newItems = []; 2096 }); 2097 2098 /** 2099 * Overload translate to set selectedItems 2100 */ 2101 Zotero.Translate.Web.prototype.translate = function (options = {}, ...args) { 2102 if (typeof options == 'number' || options === false) { 2103 Zotero.debug("Translate: translate() now takes an object -- update your code", 2); 2104 options = { 2105 libraryID: options, 2106 saveAttachments: args[0], 2107 selectedItems: args[1] 2108 }; 2109 } 2110 this._selectedItems = options.selectedItems; 2111 return Zotero.Translate.Base.prototype.translate.call(this, options); 2112 } 2113 2114 /** 2115 * Overload _translateTranslatorLoaded to send an RPC call if necessary 2116 */ 2117 Zotero.Translate.Web.prototype._translateTranslatorLoaded = function() { 2118 var runMode = this.translator[0].runMode; 2119 if(runMode === Zotero.Translator.RUN_MODE_IN_BROWSER || this._parentTranslator) { 2120 Zotero.Translate.Base.prototype._translateTranslatorLoaded.apply(this); 2121 } else if(runMode === Zotero.Translator.RUN_MODE_ZOTERO_STANDALONE || 2122 (runMode === Zotero.Translator.RUN_MODE_ZOTERO_SERVER && Zotero.Connector.isOnline)) { 2123 var me = this; 2124 Zotero.Connector.callMethod("savePage", { 2125 uri: this.location.toString(), 2126 translatorID: (typeof this.translator[0] === "object" 2127 ? this.translator[0].translatorID : this.translator[0]), 2128 cookie: this.document.cookie, 2129 proxy: this._proxy ? this._proxy.toJSON() : null, 2130 html: this.document.documentElement.innerHTML 2131 }).then(obj => me._translateRPCComplete(obj)); 2132 } else if(runMode === Zotero.Translator.RUN_MODE_ZOTERO_SERVER) { 2133 var me = this; 2134 Zotero.API.createItem({"url":this.document.location.href.toString()}, 2135 function(statusCode, response) { 2136 me._translateServerComplete(statusCode, response); 2137 }); 2138 } 2139 } 2140 2141 /** 2142 * Called when an call to Zotero Standalone for translation completes 2143 */ 2144 Zotero.Translate.Web.prototype._translateRPCComplete = function(obj, failureCode) { 2145 if(!obj) this.complete(false, failureCode); 2146 2147 if(obj.selectItems) { 2148 // if we have to select items, call the selectItems handler and do it 2149 var me = this; 2150 this._runHandler("select", obj.selectItems, 2151 function(selectedItems) { 2152 Zotero.Connector.callMethod("selectItems", 2153 {"instanceID":obj.instanceID, "selectedItems":selectedItems}) 2154 .then((obj) => me._translateRPCComplete(obj)) 2155 } 2156 ); 2157 } else { 2158 // if we don't have to select items, continue 2159 for(var i=0, n=obj.items.length; i<n; i++) { 2160 this._runHandler("itemDone", null, obj.items[i]); 2161 } 2162 this.newItems = obj.items; 2163 this.complete(true); 2164 } 2165 } 2166 2167 /** 2168 * Called when an call to the Zotero Translator Server for translation completes 2169 */ 2170 Zotero.Translate.Web.prototype._translateServerComplete = function(statusCode, response) { 2171 if(statusCode === 300) { 2172 // Multiple Choices 2173 try { 2174 response = JSON.parse(response); 2175 } catch(e) { 2176 Zotero.logError(e); 2177 this.complete(false, "Invalid JSON response received from server"); 2178 return; 2179 } 2180 var me = this; 2181 this._runHandler("select", response, 2182 function(selectedItems) { 2183 Zotero.API.createItem({ 2184 "url":me.document.location.href.toString(), 2185 "items":selectedItems 2186 }, 2187 function(statusCode, response) { 2188 me._translateServerComplete(statusCode, response); 2189 }); 2190 } 2191 ); 2192 } else if(statusCode === 201) { 2193 // Created 2194 try { 2195 response = (new DOMParser()).parseFromString(response, "application/xml"); 2196 } catch(e) { 2197 Zotero.logError(e); 2198 this.complete(false, "Invalid XML response received from server"); 2199 return; 2200 } 2201 2202 // Extract items from ATOM/JSON response 2203 var items = [], contents; 2204 if("getElementsByTagNameNS" in response) { 2205 contents = response.getElementsByTagNameNS("http://www.w3.org/2005/Atom", "content"); 2206 } else { // IE... 2207 contents = response.getElementsByTagName("content"); 2208 } 2209 for(var i=0, n=contents.length; i<n; i++) { 2210 var content = contents[i]; 2211 if("getAttributeNS" in content) { 2212 if(content.getAttributeNS("http://zotero.org/ns/api", "type") != "json") continue; 2213 } else if(content.getAttribute("zapi:type") != "json") { // IE... 2214 continue; 2215 } 2216 2217 try { 2218 var item = JSON.parse("textContent" in content ? 2219 content.textContent : content.text); 2220 } catch(e) { 2221 Zotero.logError(e); 2222 this.complete(false, "Invalid JSON response received from server"); 2223 return; 2224 } 2225 2226 if(!("attachments" in item)) item.attachments = []; 2227 this._runHandler("itemDone", null, item); 2228 items.push(item); 2229 } 2230 this.newItems = items; 2231 this.complete(true); 2232 } else { 2233 this.complete(false, response); 2234 } 2235 } 2236 2237 /** 2238 * Overload complete to report translation failure 2239 */ 2240 Zotero.Translate.Web.prototype.complete = async function(returnValue, error) { 2241 // call super 2242 var oldState = this._currentState; 2243 var errorString = Zotero.Translate.Base.prototype.complete.apply(this, [returnValue, error]); 2244 2245 var promise; 2246 if (Zotero.Prefs.getAsync) { 2247 promise = Zotero.Prefs.getAsync('reportTranslationFailure'); 2248 } else { 2249 promise = Zotero.Promise.resolve(Zotero.Prefs.get("reportTranslationFailure")); 2250 } 2251 var reportTranslationFailure = await promise; 2252 // Report translation failure if we failed 2253 if(oldState == "translate" && errorString && !this._parentTranslator && this.translator.length 2254 && this.translator[0].inRepository && reportTranslationFailure) { 2255 // Don't report failure if in private browsing mode 2256 if (Zotero.isConnector && await Zotero.Connector_Browser.isIncognito()) { 2257 return 2258 } 2259 2260 var translator = this.translator[0]; 2261 var info = await Zotero.getSystemInfo(); 2262 2263 var postBody = "id=" + encodeURIComponent(translator.translatorID) + 2264 "&lastUpdated=" + encodeURIComponent(translator.lastUpdated) + 2265 "&diagnostic=" + encodeURIComponent(info) + 2266 "&errorData=" + encodeURIComponent(errorString); 2267 return Zotero.HTTP.doPost(ZOTERO_CONFIG.REPOSITORY_URL + "report", postBody); 2268 } 2269 } 2270 2271 /** 2272 * @class Import translation 2273 */ 2274 Zotero.Translate.Import = function() { 2275 this.init(); 2276 } 2277 Zotero.Translate.Import.prototype = new Zotero.Translate.Base(); 2278 Zotero.Translate.Import.prototype.type = "import"; 2279 Zotero.Translate.Import.prototype._entryFunctionSuffix = "Import"; 2280 Zotero.Translate.Import.prototype._io = false; 2281 2282 Zotero.Translate.Import.prototype.Sandbox = Zotero.Translate.Sandbox._inheritFromBase(Zotero.Translate.Sandbox.Import); 2283 2284 /** 2285 * Sets string for translation and initializes string IO 2286 */ 2287 Zotero.Translate.Import.prototype.setString = function(string) { 2288 this._string = string; 2289 this._io = false; 2290 } 2291 2292 /** 2293 * Overload {@link Zotero.Translate.Base#complete} to close file 2294 */ 2295 Zotero.Translate.Import.prototype.complete = function(returnValue, error) { 2296 if(this._io) { 2297 this._progress = null; 2298 this._io.close(false); 2299 } 2300 2301 // call super 2302 Zotero.Translate.Base.prototype.complete.apply(this, [returnValue, error]); 2303 } 2304 2305 /** 2306 * Get all potential import translators, ordering translators with the right file extension first 2307 */ 2308 Zotero.Translate.Import.prototype._getTranslatorsGetPotentialTranslators = function() { 2309 return (this.location ? 2310 Zotero.Translators.getImportTranslatorsForLocation(this.location) : 2311 Zotero.Translators.getAllForType(this.type)). 2312 then(function(translators) { return [translators] });; 2313 } 2314 2315 /** 2316 * Overload {@link Zotero.Translate.Base#getTranslators} to return all translators immediately only 2317 * if no string or location is set 2318 */ 2319 Zotero.Translate.Import.prototype.getTranslators = function() { 2320 if(!this._string && !this.location) { 2321 if(this._currentState === "detect") throw new Error("getTranslators: detection is already running"); 2322 this._currentState = "detect"; 2323 var me = this; 2324 return Zotero.Translators.getAllForType(this.type). 2325 then(function(translators) { 2326 me._potentialTranslators = []; 2327 me._foundTranslators = translators; 2328 me.complete(true); 2329 return me._foundTranslators; 2330 }); 2331 } else { 2332 return Zotero.Translate.Base.prototype.getTranslators.call(this); 2333 } 2334 } 2335 2336 /** 2337 * Overload {@link Zotero.Translate.Base#_loadTranslator} to prepare translator IO 2338 */ 2339 Zotero.Translate.Import.prototype._loadTranslator = function(translator) { 2340 return Zotero.Translate.Base.prototype._loadTranslator.call(this, translator) 2341 .then(function() { 2342 return this._loadTranslatorPrepareIO(translator); 2343 }.bind(this)); 2344 } 2345 2346 /** 2347 * Prepare translator IO 2348 */ 2349 Zotero.Translate.Import.prototype._loadTranslatorPrepareIO = Zotero.Promise.method(function (translator) { 2350 var configOptions = this._translatorInfo.configOptions; 2351 var dataMode = configOptions ? configOptions["dataMode"] : ""; 2352 2353 if(!this._io) { 2354 if(Zotero.Translate.IO.Read && this.location && this.location instanceof Components.interfaces.nsIFile) { 2355 this._io = new Zotero.Translate.IO.Read(this.location, this._sandboxManager); 2356 } else { 2357 this._io = new Zotero.Translate.IO.String(this._string, this.path ? this.path : "", this._sandboxManager); 2358 } 2359 } 2360 2361 this._io.init(dataMode); 2362 this._sandboxManager.importObject(this._io); 2363 }); 2364 2365 /** 2366 * Prepare translation 2367 */ 2368 Zotero.Translate.Import.prototype._prepareTranslation = Zotero.Promise.method(function () { 2369 this._progress = undefined; 2370 2371 var baseURI = null; 2372 if(this.location) { 2373 try { 2374 baseURI = Components.classes["@mozilla.org/network/io-service;1"]. 2375 getService(Components.interfaces.nsIIOService).newFileURI(this.location); 2376 } catch(e) {} 2377 } 2378 2379 this._itemSaver = new Zotero.Translate.ItemSaver({ 2380 libraryID: this._libraryID, 2381 collections: this._collections, 2382 forceTagType: this._forceTagType, 2383 attachmentMode: Zotero.Translate.ItemSaver[(this._saveAttachments ? "ATTACHMENT_MODE_FILE" : "ATTACHMENT_MODE_IGNORE")], 2384 baseURI, 2385 saveOptions: Object.assign( 2386 { 2387 skipSelect: true 2388 }, 2389 this._saveOptions || {} 2390 ) 2391 }); 2392 this.newItems = []; 2393 this.newCollections = []; 2394 }); 2395 2396 /** 2397 * Return the progress of the import operation, or null if progress cannot be determined 2398 */ 2399 Zotero.Translate.Import.prototype.getProgress = function() { 2400 if(this._progress !== undefined) return this._progress; 2401 if(Zotero.Translate.IO.rdfDataModes.indexOf(this._mode) !== -1 || this._mode === "xml/e4x" || this._mode == "xml/dom" || !this._io) { 2402 return null; 2403 } 2404 return this._io.bytesRead/this._io.contentLength*100; 2405 }; 2406 2407 2408 /** 2409 * @class Export translation 2410 */ 2411 Zotero.Translate.Export = function() { 2412 this.init(); 2413 } 2414 Zotero.Translate.Export.prototype = new Zotero.Translate.Base(); 2415 Zotero.Translate.Export.prototype.type = "export"; 2416 Zotero.Translate.Export.prototype._entryFunctionSuffix = "Export"; 2417 Zotero.Translate.Export.prototype.Sandbox = Zotero.Translate.Sandbox._inheritFromBase(Zotero.Translate.Sandbox.Export); 2418 2419 /** 2420 * Sets the items to be exported 2421 * @param {Zotero.Item[]} items 2422 */ 2423 Zotero.Translate.Export.prototype.setItems = function(items) { 2424 this._export = {type: 'items', items: items}; 2425 } 2426 2427 /** 2428 * Sets the group to be exported (overrides setItems/setCollection) 2429 * @param {Zotero.Group[]} group 2430 */ 2431 Zotero.Translate.Export.prototype.setLibraryID = function(libraryID) { 2432 this._export = {type: 'library', id: libraryID}; 2433 } 2434 2435 /** 2436 * Sets the collection to be exported (overrides setItems/setGroup) 2437 * @param {Zotero.Collection[]} collection 2438 */ 2439 Zotero.Translate.Export.prototype.setCollection = function(collection) { 2440 this._export = {type: 'collection', collection: collection}; 2441 } 2442 2443 /** 2444 * Sets the translator to be used for export 2445 * 2446 * @param {Zotero.Translator|string} Translator object or ID. If this contains a displayOptions 2447 * attribute, setDisplayOptions is automatically called with the specified value. 2448 */ 2449 Zotero.Translate.Export.prototype.setTranslator = function(translator) { 2450 if(typeof translator == "object" && translator.displayOptions) { 2451 this._displayOptions = translator.displayOptions; 2452 } 2453 return Zotero.Translate.Base.prototype.setTranslator.apply(this, [translator]); 2454 } 2455 2456 /** 2457 * Sets translator display options. you can also pass a translator (not ID) to 2458 * setTranslator that includes a displayOptions argument 2459 */ 2460 Zotero.Translate.Export.prototype.setDisplayOptions = function(displayOptions) { 2461 this._displayOptions = displayOptions; 2462 } 2463 2464 /** 2465 * Overload {@link Zotero.Translate.Base#complete} to close file and set complete 2466 */ 2467 Zotero.Translate.Export.prototype.complete = function(returnValue, error) { 2468 if(this._io) { 2469 this._progress = null; 2470 this._io.close(true); 2471 if(this._io instanceof Zotero.Translate.IO.String) { 2472 this.string = this._io.string; 2473 } 2474 } 2475 2476 // call super 2477 Zotero.Translate.Base.prototype.complete.apply(this, [returnValue, error]); 2478 } 2479 2480 /** 2481 * Overload {@link Zotero.Translate.Base#getTranslators} to return all translators immediately 2482 */ 2483 Zotero.Translate.Export.prototype.getTranslators = function() { 2484 if(this._currentState === "detect") { 2485 return Zotero.Promise.reject(new Error("getTranslators: detection is already running")); 2486 } 2487 var me = this; 2488 return Zotero.Translators.getAllForType(this.type).then(function(translators) { 2489 me._currentState = "detect"; 2490 me._foundTranslators = translators; 2491 me._potentialTranslators = []; 2492 me.complete(true); 2493 return me._foundTranslators; 2494 }); 2495 } 2496 2497 /** 2498 * Does the actual export, after code has been loaded and parsed 2499 */ 2500 Zotero.Translate.Export.prototype._prepareTranslation = Zotero.Promise.method(function () { 2501 this._progress = undefined; 2502 2503 // initialize ItemGetter 2504 this._itemGetter = new Zotero.Translate.ItemGetter(); 2505 2506 // Toggle legacy mode for translators pre-4.0.27 2507 this._itemGetter.legacy = Services.vc.compare('4.0.27', this._translatorInfo.minVersion) > 0; 2508 2509 var configOptions = this._translatorInfo.configOptions || {}, 2510 getCollections = configOptions.getCollections || false; 2511 var loadPromise = Zotero.Promise.resolve(); 2512 switch (this._export.type) { 2513 case 'collection': 2514 this._itemGetter.setCollection(this._export.collection, getCollections); 2515 break; 2516 case 'items': 2517 this._itemGetter.setItems(this._export.items); 2518 break; 2519 case 'library': 2520 loadPromise = this._itemGetter.setAll(this._export.id, getCollections); 2521 break; 2522 default: 2523 throw new Error('No export set up'); 2524 break; 2525 } 2526 delete this._export; 2527 2528 if (this.noWait) { 2529 if (!loadPromise.isResolved()) { 2530 throw new Error("Load promise is not resolved in noWait mode"); 2531 } 2532 rest.apply(this, arguments); 2533 } else { 2534 return loadPromise.then(() => rest.apply(this, arguments)) 2535 } 2536 2537 function rest() { 2538 // export file data, if requested 2539 if(this._displayOptions["exportFileData"]) { 2540 this.location = this._itemGetter.exportFiles(this.location, this.translator[0].target); 2541 } 2542 2543 // initialize IO 2544 // this is currently hackish since we pass null callbacks to the init function (they have 2545 // callbacks to be consistent with import, but they are synchronous, so we ignore them) 2546 if(!this.location) { 2547 this._io = new Zotero.Translate.IO.String(null, this.path ? this.path : "", this._sandboxManager); 2548 this._io.init(configOptions["dataMode"], function() {}); 2549 } else if(!Zotero.Translate.IO.Write) { 2550 throw new Error("Writing to files is not supported in this build of Zotero."); 2551 } else { 2552 this._io = new Zotero.Translate.IO.Write(this.location); 2553 this._io.init(configOptions["dataMode"], 2554 this._displayOptions["exportCharset"] ? this._displayOptions["exportCharset"] : null, 2555 function() {}); 2556 } 2557 2558 this._sandboxManager.importObject(this._io); 2559 } 2560 }); 2561 2562 /** 2563 * Overload Zotero.Translate.Base#translate to make sure that 2564 * Zotero.Translate.Export#translate is not called without setting a 2565 * translator first. Doesn't make sense to run detection for export. 2566 */ 2567 Zotero.Translate.Export.prototype.translate = function() { 2568 if(!this.translator || !this.translator.length) { 2569 this.complete(false, new Error("Export translation initiated without setting a translator")); 2570 } else { 2571 return Zotero.Translate.Base.prototype.translate.apply(this, arguments); 2572 } 2573 }; 2574 2575 /** 2576 * Return the progress of the import operation, or null if progress cannot be determined 2577 */ 2578 Zotero.Translate.Export.prototype.getProgress = function() { 2579 if(this._progress !== undefined) return this._progress; 2580 if(!this._itemGetter) { 2581 return null; 2582 } 2583 return (1-this._itemGetter.numItemsRemaining/this._itemGetter.numItems)*100; 2584 }; 2585 2586 /** 2587 * @class Search translation 2588 * @property {Array[]} search Item (in {@link Zotero.Item#serialize} format) to extrapolate data 2589 * (set with setSearch) 2590 */ 2591 Zotero.Translate.Search = function() { 2592 this.init(); 2593 }; 2594 Zotero.Translate.Search.prototype = new Zotero.Translate.Base(); 2595 Zotero.Translate.Search.prototype.type = "search"; 2596 Zotero.Translate.Search.prototype._entryFunctionSuffix = "Search"; 2597 Zotero.Translate.Search.prototype.Sandbox = Zotero.Translate.Sandbox._inheritFromBase(Zotero.Translate.Sandbox.Search); 2598 Zotero.Translate.Search.prototype.ERROR_NO_RESULTS = "No items returned from any translator"; 2599 2600 /** 2601 * @borrows Zotero.Translate.Web#setCookieSandbox 2602 */ 2603 Zotero.Translate.Search.prototype.setCookieSandbox = Zotero.Translate.Web.prototype.setCookieSandbox; 2604 2605 /** 2606 * Sets the item to be used for searching 2607 * @param {Object} item An item, with as many fields as desired, in the format returned by 2608 * {@link Zotero.Item#serialize} 2609 */ 2610 Zotero.Translate.Search.prototype.setSearch = function(search) { 2611 this.search = search; 2612 } 2613 2614 /** 2615 * Set an identifier to use for searching 2616 * 2617 * @param {Object} identifier - An object with 'DOI', 'ISBN', or 'PMID' 2618 */ 2619 Zotero.Translate.Search.prototype.setIdentifier = function (identifier) { 2620 var search; 2621 if (identifier.DOI) { 2622 search = { 2623 itemType: "journalArticle", 2624 DOI: identifier.DOI 2625 }; 2626 } 2627 else if (identifier.ISBN) { 2628 search = { 2629 itemType: "book", 2630 ISBN: identifier.ISBN 2631 }; 2632 } 2633 else if (identifier.PMID) { 2634 search = { 2635 itemType: "journalArticle", 2636 contextObject: "rft_id=info:pmid/" + identifier.PMID 2637 }; 2638 } 2639 else if (identifier.arXiv) { 2640 search = { 2641 itemType: "journalArticle", 2642 arXiv: identifier.arXiv 2643 }; 2644 } 2645 else { 2646 throw new Error("Unrecognized identifier"); 2647 } 2648 this.setSearch(search); 2649 } 2650 2651 /** 2652 * Overloads {@link Zotero.Translate.Base#getTranslators} to always return all potential translators 2653 */ 2654 Zotero.Translate.Search.prototype.getTranslators = function() { 2655 return Zotero.Translate.Base.prototype.getTranslators.call(this, true); 2656 } 2657 2658 /** 2659 * Sets the translator or translators to be used for search 2660 * 2661 * @param {Zotero.Translator|string} Translator object or ID 2662 */ 2663 Zotero.Translate.Search.prototype.setTranslator = function(translator) { 2664 // Accept an array of translators 2665 if (Array.isArray(translator)) { 2666 this.translator = translator; 2667 return true; 2668 } 2669 return Zotero.Translate.Base.prototype.setTranslator.apply(this, [translator]); 2670 } 2671 2672 /** 2673 * Overload Zotero.Translate.Base#complete to move onto the next translator if 2674 * translation fails 2675 */ 2676 Zotero.Translate.Search.prototype.complete = function(returnValue, error) { 2677 if(this._currentState == "translate" 2678 && (!this.newItems || !this.newItems.length) 2679 && !this._savingItems 2680 //length is 0 only when translate was called without translators 2681 && this.translator.length) { 2682 Zotero.debug("Translate: Could not find a result using " + this.translator[0].label 2683 + (this.translator.length > 1 ? " -- trying next translator" : ""), 3); 2684 if(error) Zotero.debug(this._generateErrorString(error), 3); 2685 if(this.translator.length > 1) { 2686 this.translator.shift(); 2687 this.translate({ 2688 libraryID: this._libraryID, 2689 saveAttachments: this._saveAttachments, 2690 collections: this._collections 2691 }); 2692 return; 2693 } else { 2694 Zotero.debug("No more translators to try"); 2695 error = this.ERROR_NO_RESULTS; 2696 returnValue = false; 2697 } 2698 } 2699 2700 // call super 2701 Zotero.Translate.Base.prototype.complete.apply(this, [returnValue, error]); 2702 } 2703 2704 /** 2705 * Pass search item to detect* and do* functions 2706 */ 2707 Zotero.Translate.Search.prototype._getParameters = function() { 2708 if(Zotero.isFx) { 2709 return [this._sandboxManager.copyObject(this.search)]; 2710 } 2711 return [this.search]; 2712 }; 2713 2714 /** 2715 * Extract sandbox location from translator target 2716 */ 2717 Zotero.Translate.Search.prototype._getSandboxLocation = function() { 2718 // generate sandbox for search by extracting domain from translator target 2719 if(this.translator && this.translator[0] && this.translator[0].target) { 2720 // so that web translators work too 2721 const searchSandboxRe = /^http:\/\/[\w.]+\//; 2722 var tempURL = this.translator[0].target.replace(/\\/g, "").replace(/\^/g, ""); 2723 var m = searchSandboxRe.exec(tempURL); 2724 if(m) return m[0]; 2725 } 2726 return Zotero.Translate.Base.prototype._getSandboxLocation.call(this); 2727 } 2728 2729 Zotero.Translate.Search.prototype._prepareTranslation = Zotero.Translate.Web.prototype._prepareTranslation; 2730 2731 /** 2732 * IO-related functions 2733 * @namespace 2734 */ 2735 Zotero.Translate.IO = { 2736 /** 2737 * Parses XML using DOMParser 2738 */ 2739 "parseDOMXML":function(input, charset, size) { 2740 try { 2741 var dp = new DOMParser(); 2742 } catch(e) { 2743 try { 2744 var dp = Components.classes["@mozilla.org/xmlextras/domparser;1"] 2745 .createInstance(Components.interfaces.nsIDOMParser); 2746 } catch(e) { 2747 throw new Error("DOMParser not supported"); 2748 } 2749 } 2750 2751 if(typeof input == "string") { 2752 var nodes = dp.parseFromString(input, "text/xml"); 2753 } else { 2754 var nodes = dp.parseFromStream(input, charset, size, "text/xml"); 2755 } 2756 2757 if(nodes.getElementsByTagName("parsererror").length) { 2758 throw "DOMParser error: loading data into data store failed"; 2759 } 2760 2761 if("normalize" in nodes) nodes.normalize(); 2762 2763 return nodes; 2764 }, 2765 2766 /** 2767 * Names of RDF data modes 2768 */ 2769 "rdfDataModes":["rdf", "rdf/xml", "rdf/n3"] 2770 }; 2771 2772 /******* String support *******/ 2773 2774 /** 2775 * @class Translate backend for translating from a string 2776 */ 2777 Zotero.Translate.IO.String = function(string, uri, sandboxManager) { 2778 if(string && typeof string === "string") { 2779 this.string = string; 2780 } else { 2781 this.string = ""; 2782 } 2783 this.contentLength = this.string.length; 2784 this.bytesRead = 0; 2785 this._uri = uri; 2786 this._sandboxManager = sandboxManager; 2787 } 2788 2789 Zotero.Translate.IO.String.prototype = { 2790 "__exposedProps__":{ 2791 "RDF":"r", 2792 "read":"r", 2793 "write":"r", 2794 "setCharacterSet":"r", 2795 "getXML":"r" 2796 }, 2797 2798 "_initRDF": function () { 2799 Zotero.debug("Translate: Initializing RDF data store"); 2800 this._dataStore = new Zotero.RDF.AJAW.IndexedFormula(); 2801 this.RDF = new Zotero.Translate.IO._RDFSandbox(this._dataStore); 2802 2803 if(this.contentLength) { 2804 try { 2805 var xml = Zotero.Translate.IO.parseDOMXML(this.string); 2806 } catch(e) { 2807 this._xmlInvalid = true; 2808 throw e; 2809 } 2810 var parser = new Zotero.RDF.AJAW.RDFParser(this._dataStore); 2811 parser.parse(xml, this._uri); 2812 } 2813 }, 2814 2815 "setCharacterSet":function(charset) {}, 2816 2817 "read":function(bytes) { 2818 // if we are reading in RDF data mode and no string is set, serialize current RDF to the 2819 // string 2820 if(Zotero.Translate.IO.rdfDataModes.indexOf(this._mode) !== -1 && this.string === "") { 2821 this.string = this.RDF.serialize(); 2822 } 2823 2824 // return false if string has been read 2825 if(this.bytesRead >= this.contentLength) { 2826 return false; 2827 } 2828 2829 if(bytes !== undefined) { 2830 if(this.bytesRead >= this.contentLength) return false; 2831 var oldPointer = this.bytesRead; 2832 this.bytesRead += bytes; 2833 return this.string.substr(oldPointer, bytes); 2834 } else { 2835 // bytes not specified; read a line 2836 var oldPointer = this.bytesRead; 2837 var lfIndex = this.string.indexOf("\n", this.bytesRead); 2838 2839 if(lfIndex !== -1) { 2840 // in case we have a CRLF 2841 this.bytesRead = lfIndex+1; 2842 if(this.contentLength > lfIndex && this.string.substr(lfIndex-1, 1) === "\r") { 2843 lfIndex--; 2844 } 2845 return this.string.substr(oldPointer, lfIndex-oldPointer); 2846 } 2847 2848 if(!this._noCR) { 2849 var crIndex = this.string.indexOf("\r", this.bytesRead); 2850 if(crIndex === -1) { 2851 this._noCR = true; 2852 } else { 2853 this.bytesRead = crIndex+1; 2854 return this.string.substr(oldPointer, crIndex-oldPointer-1); 2855 } 2856 } 2857 2858 this.bytesRead = this.contentLength; 2859 return this.string.substr(oldPointer); 2860 } 2861 }, 2862 2863 "write":function(data) { 2864 this.string += data; 2865 this.contentLength = this.string.length; 2866 }, 2867 2868 "getXML":function() { 2869 try { 2870 var xml = Zotero.Translate.IO.parseDOMXML(this.string); 2871 } catch(e) { 2872 this._xmlInvalid = true; 2873 throw e; 2874 } 2875 return (Zotero.isFx && !Zotero.isBookmarklet ? this._sandboxManager.wrap(xml) : xml); 2876 }, 2877 2878 init: function (newMode) { 2879 this.bytesRead = 0; 2880 this._noCR = undefined; 2881 2882 this._mode = newMode; 2883 if(newMode === "xml/e4x") { 2884 throw new Error("E4X is not supported"); 2885 } else if(newMode && (Zotero.Translate.IO.rdfDataModes.indexOf(newMode) !== -1 2886 || newMode.substr(0, 3) === "xml/dom") && this._xmlInvalid) { 2887 throw new Error("XML known invalid"); 2888 } else if(Zotero.Translate.IO.rdfDataModes.indexOf(this._mode) !== -1) { 2889 this._initRDF(); 2890 } 2891 }, 2892 2893 "close":function(serialize) { 2894 // if we are writing in RDF data mode and no string is set, serialize current RDF to the 2895 // string 2896 if(serialize && Zotero.Translate.IO.rdfDataModes.indexOf(this._mode) !== -1 && this.string === "") { 2897 this.string = this.RDF.serialize(); 2898 } 2899 } 2900 } 2901 2902 /****** RDF DATA MODE ******/ 2903 2904 /** 2905 * @class An API for handling RDF from the sandbox. This is exposed to translators as Zotero.RDF. 2906 * 2907 * @property {Zotero.RDF.AJAW.IndexedFormula} _dataStore 2908 * @property {Integer[]} _containerCounts 2909 * @param {Zotero.RDF.AJAW.IndexedFormula} dataStore 2910 */ 2911 Zotero.Translate.IO._RDFSandbox = function(dataStore) { 2912 this._dataStore = dataStore; 2913 } 2914 2915 Zotero.Translate.IO._RDFSandbox.prototype = { 2916 "_containerCounts":[], 2917 "__exposedProps__":{ 2918 "addStatement":"r", 2919 "newResource":"r", 2920 "newContainer":"r", 2921 "addContainerElement":"r", 2922 "getContainerElements":"r", 2923 "addNamespace":"r", 2924 "getAllResources":"r", 2925 "getResourceURI":"r", 2926 "getArcsIn":"r", 2927 "getArcsOut":"r", 2928 "getSources":"r", 2929 "getTargets":"r", 2930 "getStatementsMatching":"r", 2931 "serialize":"r" 2932 }, 2933 2934 /** 2935 * Gets a resource as a Zotero.RDF.AJAW.Symbol, rather than a string 2936 * @param {String|Zotero.RDF.AJAW.Symbol} about 2937 * @return {Zotero.RDF.AJAW.Symbol} 2938 */ 2939 "_getResource":function(about) { 2940 return (typeof about == "object" ? about : new Zotero.RDF.AJAW.Symbol(about)); 2941 }, 2942 2943 /** 2944 * Runs a callback to initialize this RDF store 2945 */ 2946 "_init":function() { 2947 if(this._prepFunction) { 2948 this._dataStore = this._prepFunction(); 2949 delete this._prepFunction; 2950 } 2951 }, 2952 2953 /** 2954 * Serializes the current RDF to a string 2955 */ 2956 "serialize":function(dataMode) { 2957 var serializer = Zotero.RDF.AJAW.Serializer(this._dataStore); 2958 2959 for(var prefix in this._dataStore.namespaces) { 2960 serializer.suggestPrefix(prefix, this._dataStore.namespaces[prefix]); 2961 } 2962 2963 // serialize in appropriate format 2964 if(dataMode == "rdf/n3") { 2965 return serializer.statementsToN3(this._dataStore.statements); 2966 } 2967 2968 return serializer.statementsToXML(this._dataStore.statements); 2969 }, 2970 2971 /** 2972 * Adds an RDF triple 2973 * @param {String|Zotero.RDF.AJAW.Symbol} about 2974 * @param {String|Zotero.RDF.AJAW.Symbol} relation 2975 * @param {String|Zotero.RDF.AJAW.Symbol} value 2976 * @param {Boolean} literal Whether value should be treated as a literal (true) or a resource 2977 * (false) 2978 */ 2979 "addStatement":function(about, relation, value, literal) { 2980 if(about === null || about === undefined) { 2981 throw new Error("about must be defined in Zotero.RDF.addStatement"); 2982 } 2983 if(relation === null || relation === undefined) { 2984 throw new Error("relation must be defined in Zotero.RDF.addStatement"); 2985 } 2986 if(value === null || value === undefined) { 2987 throw new Error("value must be defined in Zotero.RDF.addStatement"); 2988 } 2989 2990 if(literal) { 2991 // zap chars that Mozilla will mangle 2992 value = value.toString().replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, ''); 2993 } else { 2994 value = this._getResource(value); 2995 } 2996 2997 this._dataStore.add(this._getResource(about), this._getResource(relation), value); 2998 }, 2999 3000 /** 3001 * Creates a new anonymous resource 3002 * @return {Zotero.RDF.AJAW.Symbol} 3003 */ 3004 "newResource":function() { 3005 return new Zotero.RDF.AJAW.BlankNode(); 3006 }, 3007 3008 /** 3009 * Creates a new container resource 3010 * @param {String} type The type of the container ("bag", "seq", or "alt") 3011 * @param {String|Zotero.RDF.AJAW.Symbol} about The URI of the resource 3012 * @return {Zotero.Translate.RDF.prototype.newContainer 3013 */ 3014 "newContainer":function(type, about) { 3015 const rdf = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; 3016 const containerTypes = {"bag":"Bag", "seq":"Seq", "alt":"Alt"}; 3017 3018 type = type.toLowerCase(); 3019 if(!containerTypes[type]) { 3020 throw new Error("Invalid container type in Zotero.RDF.newContainer"); 3021 } 3022 3023 var about = this._getResource(about); 3024 this.addStatement(about, rdf+"type", rdf+containerTypes[type], false); 3025 this._containerCounts[about.toNT()] = 1; 3026 3027 return about; 3028 }, 3029 3030 /** 3031 * Adds a new element to a container 3032 * @param {String|Zotero.RDF.AJAW.Symbol} about The container 3033 * @param {String|Zotero.RDF.AJAW.Symbol} element The element to add to the container 3034 * @param {Boolean} literal Whether element should be treated as a literal (true) or a resource 3035 * (false) 3036 */ 3037 "addContainerElement":function(about, element, literal) { 3038 const rdf = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; 3039 3040 var about = this._getResource(about); 3041 this._dataStore.add(about, new Zotero.RDF.AJAW.Symbol(rdf+"_"+(this._containerCounts[about.toNT()]++)), element, literal); 3042 }, 3043 3044 /** 3045 * Gets all elements within a container 3046 * @param {String|Zotero.RDF.AJAW.Symbol} about The container 3047 * @return {Zotero.RDF.AJAW.Symbol[]} 3048 */ 3049 "getContainerElements":function(about) { 3050 const liPrefix = "http://www.w3.org/1999/02/22-rdf-syntax-ns#_"; 3051 3052 var about = this._getResource(about); 3053 var statements = this._dataStore.statementsMatching(about); 3054 var containerElements = []; 3055 3056 // loop over arcs out looking for list items 3057 for(var i=0; i<statements.length; i++) { 3058 var statement = statements[i]; 3059 if(statement.predicate.uri.substr(0, liPrefix.length) == liPrefix) { 3060 var number = statement.predicate.uri.substr(liPrefix.length); 3061 3062 // make sure these are actually numeric list items 3063 var intNumber = parseInt(number); 3064 if(number == intNumber.toString()) { 3065 // add to element array 3066 containerElements[intNumber-1] = (statement.object.termType == "literal" ? statement.object.toString() : statement.object); 3067 } 3068 } 3069 } 3070 3071 return containerElements; 3072 }, 3073 3074 /** 3075 * Adds a namespace for a specific URI 3076 * @param {String} prefix Namespace prefix 3077 * @param {String} uri Namespace URI 3078 */ 3079 "addNamespace":function(prefix, uri) { 3080 this._dataStore.setPrefixForURI(prefix, uri); 3081 }, 3082 3083 /** 3084 * Gets the URI a specific resource 3085 * @param {String|Zotero.RDF.AJAW.Symbol} resource 3086 * @return {String} 3087 */ 3088 "getResourceURI":function(resource) { 3089 if(typeof(resource) == "string") return resource; 3090 3091 const rdf = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; 3092 var values = this.getTargets(resource, rdf + 'value'); 3093 if(values && values.length) return this.getResourceURI(values[0]); 3094 3095 if(resource.uri) return resource.uri; 3096 if(resource.toNT == undefined) throw new Error("Zotero.RDF: getResourceURI called on invalid resource"); 3097 return resource.toNT(); 3098 }, 3099 3100 /** 3101 * Gets all resources in the RDF data store 3102 * @return {Zotero.RDF.AJAW.Symbol[]} 3103 */ 3104 "getAllResources":function() { 3105 var returnArray = []; 3106 for(var i in this._dataStore.subjectIndex) { 3107 returnArray.push(this._dataStore.subjectIndex[i][0].subject); 3108 } 3109 return returnArray; 3110 }, 3111 3112 /** 3113 * Gets all arcs (predicates) into a resource 3114 * @return {Zotero.RDF.AJAW.Symbol[]} 3115 * @deprecated Since 2.1. Use {@link Zotero.Translate.IO["rdf"]._RDFBase#getStatementsMatching} 3116 */ 3117 "getArcsIn":function(resource) { 3118 var statements = this._dataStore.objectIndex[this._dataStore.canon(this._getResource(resource))]; 3119 if(!statements) return false; 3120 3121 var returnArray = []; 3122 for(var i=0; i<statements.length; i++) { 3123 returnArray.push(statements[i].predicate.uri); 3124 } 3125 return returnArray; 3126 }, 3127 3128 /** 3129 * Gets all arcs (predicates) out of a resource 3130 * @return {Zotero.RDF.AJAW.Symbol[]} 3131 * @deprecated Since 2.1. Use {@link Zotero.Translate.IO["rdf"]._RDFBase#getStatementsMatching} 3132 */ 3133 "getArcsOut":function(resource) { 3134 var statements = this._dataStore.subjectIndex[this._dataStore.canon(this._getResource(resource))]; 3135 if(!statements) return false; 3136 3137 var returnArray = []; 3138 for(var i=0; i<statements.length; i++) { 3139 returnArray.push(statements[i].predicate.uri); 3140 } 3141 return returnArray; 3142 }, 3143 3144 /** 3145 * Gets all subjects whose predicates point to a resource 3146 * @param {String|Zotero.RDF.AJAW.Symbol} resource Subject that predicates should point to 3147 * @param {String|Zotero.RDF.AJAW.Symbol} property Predicate 3148 * @return {Zotero.RDF.AJAW.Symbol[]} 3149 * @deprecated Since 2.1. Use {@link Zotero.Translate.IO["rdf"]._RDFBase#getStatementsMatching} 3150 */ 3151 "getSources":function(resource, property) { 3152 var statements = this._dataStore.statementsMatching(undefined, this._getResource(property), this._getResource(resource)); 3153 if(!statements.length) return false; 3154 3155 var returnArray = []; 3156 for(var i=0; i<statements.length; i++) { 3157 returnArray.push(statements[i].subject); 3158 } 3159 return returnArray; 3160 }, 3161 3162 /** 3163 * Gets all objects of a given subject with a given predicate 3164 * @param {String|Zotero.RDF.AJAW.Symbol} resource Subject 3165 * @param {String|Zotero.RDF.AJAW.Symbol} property Predicate 3166 * @return {Zotero.RDF.AJAW.Symbol[]} 3167 * @deprecated Since 2.1. Use {@link Zotero.Translate.IO["rdf"]._RDFBase#getStatementsMatching} 3168 */ 3169 "getTargets":function(resource, property) { 3170 var statements = this._dataStore.statementsMatching(this._getResource(resource), this._getResource(property)); 3171 if(!statements.length) return false; 3172 3173 var returnArray = []; 3174 for(var i=0; i<statements.length; i++) { 3175 returnArray.push(statements[i].object.termType == "literal" ? statements[i].object.toString() : statements[i].object); 3176 } 3177 return returnArray; 3178 }, 3179 3180 /** 3181 * Gets statements matching a certain pattern 3182 * 3183 * @param {String|Zotero.RDF.AJAW.Symbol} subj Subject 3184 * @param {String|Zotero.RDF.AJAW.Symbol} predicate Predicate 3185 * @param {String|Zotero.RDF.AJAW.Symbol} obj Object 3186 * @param {Boolean} objLiteral Whether the object is a literal (as 3187 * opposed to a URI) 3188 * @param {Boolean} justOne Whether to stop when a single result is 3189 * retrieved 3190 */ 3191 "getStatementsMatching":function(subj, pred, obj, objLiteral, justOne) { 3192 var statements = this._dataStore.statementsMatching( 3193 (subj ? this._getResource(subj) : undefined), 3194 (pred ? this._getResource(pred) : undefined), 3195 (obj ? (objLiteral ? objLiteral : this._getResource(obj)) : undefined), 3196 undefined, justOne); 3197 if(!statements.length) return false; 3198 3199 3200 var returnArray = []; 3201 for(var i=0; i<statements.length; i++) { 3202 returnArray.push([statements[i].subject, statements[i].predicate, (statements[i].object.termType == "literal" ? statements[i].object.toString() : statements[i].object)]); 3203 } 3204 return returnArray; 3205 } 3206 };