utilities_internal.js (51929B)
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 24 Utilities based in part on code taken from Piggy Bank 2.1.1 (BSD-licensed) 25 26 ***** END LICENSE BLOCK ***** 27 */ 28 29 /** 30 * @class Utility functions not made available to translators 31 */ 32 Zotero.Utilities.Internal = { 33 SNAPSHOT_SAVE_TIMEOUT: 30000, 34 35 /** 36 * Run a function on chunks of a given size of an array's elements. 37 * 38 * @param {Array} arr 39 * @param {Integer} chunkSize 40 * @param {Function} func - A promise-returning function 41 * @return {Array} The return values from the successive runs 42 */ 43 "forEachChunkAsync": Zotero.Promise.coroutine(function* (arr, chunkSize, func) { 44 var retValues = []; 45 var tmpArray = arr.concat(); 46 var num = arr.length; 47 var done = 0; 48 49 do { 50 var chunk = tmpArray.splice(0, chunkSize); 51 done += chunk.length; 52 retValues.push(yield func(chunk)); 53 } 54 while (done < num); 55 56 return retValues; 57 }), 58 59 60 /** 61 * Copy a text string to the clipboard 62 */ 63 "copyTextToClipboard":function(str) { 64 Components.classes["@mozilla.org/widget/clipboardhelper;1"] 65 .getService(Components.interfaces.nsIClipboardHelper) 66 .copyString(str); 67 }, 68 69 70 /* 71 * Adapted from http://developer.mozilla.org/en/docs/nsICryptoHash 72 * 73 * @param {String|nsIFile} strOrFile 74 * @param {Boolean} [base64=false] Return as base-64-encoded string rather than hex string 75 * @return {String} 76 */ 77 "md5":function(strOrFile, base64) { 78 if (typeof strOrFile == 'string') { 79 var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]. 80 createInstance(Components.interfaces.nsIScriptableUnicodeConverter); 81 converter.charset = "UTF-8"; 82 var result = {}; 83 var data = converter.convertToByteArray(strOrFile, result); 84 var ch = Components.classes["@mozilla.org/security/hash;1"] 85 .createInstance(Components.interfaces.nsICryptoHash); 86 ch.init(ch.MD5); 87 ch.update(data, data.length); 88 } 89 else if (strOrFile instanceof Components.interfaces.nsIFile) { 90 if (!strOrFile.exists()) { 91 return false; 92 } 93 94 // Otherwise throws (NS_ERROR_NOT_AVAILABLE) [nsICryptoHash.updateFromStream] 95 if (!strOrFile.fileSize) { 96 // MD5 for empty string 97 return "d41d8cd98f00b204e9800998ecf8427e"; 98 } 99 100 var istream = Components.classes["@mozilla.org/network/file-input-stream;1"] 101 .createInstance(Components.interfaces.nsIFileInputStream); 102 // open for reading 103 istream.init(strOrFile, 0x01, 0o444, 0); 104 var ch = Components.classes["@mozilla.org/security/hash;1"] 105 .createInstance(Components.interfaces.nsICryptoHash); 106 // we want to use the MD5 algorithm 107 ch.init(ch.MD5); 108 // this tells updateFromStream to read the entire file 109 const PR_UINT32_MAX = 0xffffffff; 110 ch.updateFromStream(istream, PR_UINT32_MAX); 111 } 112 113 // pass false here to get binary data back 114 var hash = ch.finish(base64); 115 116 if (istream) { 117 istream.close(); 118 } 119 120 if (base64) { 121 return hash; 122 } 123 124 // return the two-digit hexadecimal code for a byte 125 function toHexString(charCode) { 126 return ("0" + charCode.toString(16)).slice(-2); 127 } 128 129 // convert the binary hash data to a hex string. 130 var hexStr = ""; 131 for (let i = 0; i < hash.length; i++) { 132 hexStr += toHexString(hash.charCodeAt(i)); 133 } 134 return hexStr; 135 }, 136 137 138 /** 139 * @param {OS.File|nsIFile|String} file File or file path 140 * @param {Boolean} [base64=FALSE] Return as base-64-encoded string 141 * rather than hex string 142 */ 143 md5Async: async function (file, base64) { 144 const CHUNK_SIZE = 16384; 145 146 function toHexString(charCode) { 147 return ("0" + charCode.toString(16)).slice(-2); 148 } 149 150 var ch = Components.classes["@mozilla.org/security/hash;1"] 151 .createInstance(Components.interfaces.nsICryptoHash); 152 ch.init(ch.MD5); 153 154 // Recursively read chunks of the file and return a promise for the hash 155 let readChunk = async function (file) { 156 try { 157 let data = await file.read(CHUNK_SIZE); 158 ch.update(data, data.length); 159 if (data.length == CHUNK_SIZE) { 160 return readChunk(file); 161 } 162 163 let hash = ch.finish(base64); 164 // Base64 165 if (base64) { 166 return hash; 167 } 168 // Hex string 169 let hexStr = ""; 170 for (let i = 0; i < hash.length; i++) { 171 hexStr += toHexString(hash.charCodeAt(i)); 172 } 173 return hexStr; 174 } 175 catch (e) { 176 try { 177 ch.finish(false); 178 } 179 catch (e) { 180 Zotero.logError(e); 181 } 182 throw e; 183 } 184 }; 185 186 if (file instanceof OS.File) { 187 return readChunk(file); 188 } 189 190 var path = (file instanceof Components.interfaces.nsIFile) ? file.path : file; 191 var hash; 192 try { 193 var osFile = await OS.File.open(path); 194 hash = await readChunk(osFile); 195 } 196 finally { 197 if (osFile) { 198 await osFile.close(); 199 } 200 } 201 return hash; 202 }, 203 204 205 gzip: Zotero.Promise.coroutine(function* (data) { 206 var deferred = Zotero.Promise.defer(); 207 208 // Get input stream from POST data 209 var unicodeConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"] 210 .createInstance(Components.interfaces.nsIScriptableUnicodeConverter); 211 unicodeConverter.charset = "UTF-8"; 212 var is = unicodeConverter.convertToInputStream(data); 213 214 // Initialize stream converter 215 var converter = Components.classes["@mozilla.org/streamconv;1?from=uncompressed&to=gzip"] 216 .createInstance(Components.interfaces.nsIStreamConverter); 217 converter.asyncConvertData( 218 "uncompressed", 219 "gzip", 220 { 221 binaryInputStream: null, 222 size: 0, 223 data: '', 224 225 onStartRequest: function (request, context) {}, 226 227 onStopRequest: function (request, context, status) { 228 this.binaryInputStream.close(); 229 delete this.binaryInputStream; 230 231 deferred.resolve(this.data); 232 }, 233 234 onDataAvailable: function (request, context, inputStream, offset, count) { 235 this.size += count; 236 237 this.binaryInputStream = Components.classes["@mozilla.org/binaryinputstream;1"] 238 .createInstance(Components.interfaces.nsIBinaryInputStream) 239 this.binaryInputStream.setInputStream(inputStream); 240 this.data += this.binaryInputStream.readBytes(this.binaryInputStream.available()); 241 }, 242 243 QueryInterface: function (iid) { 244 if (iid.equals(Components.interfaces.nsISupports) 245 || iid.equals(Components.interfaces.nsIStreamListener)) { 246 return this; 247 } 248 throw Components.results.NS_ERROR_NO_INTERFACE; 249 } 250 }, 251 null 252 ); 253 254 // Send input stream to stream converter 255 var pump = Components.classes["@mozilla.org/network/input-stream-pump;1"] 256 .createInstance(Components.interfaces.nsIInputStreamPump); 257 pump.init(is, -1, -1, 0, 0, true); 258 pump.asyncRead(converter, null); 259 260 return deferred.promise; 261 }), 262 263 264 gunzip: Zotero.Promise.coroutine(function* (data) { 265 var deferred = Zotero.Promise.defer(); 266 267 Components.utils.import("resource://gre/modules/NetUtil.jsm"); 268 269 var is = Components.classes["@mozilla.org/io/string-input-stream;1"] 270 .createInstance(Ci.nsIStringInputStream); 271 is.setData(data, data.length); 272 273 var bis = Components.classes["@mozilla.org/binaryinputstream;1"] 274 .createInstance(Components.interfaces.nsIBinaryInputStream); 275 bis.setInputStream(is); 276 277 // Initialize stream converter 278 var converter = Components.classes["@mozilla.org/streamconv;1?from=gzip&to=uncompressed"] 279 .createInstance(Components.interfaces.nsIStreamConverter); 280 converter.asyncConvertData( 281 "gzip", 282 "uncompressed", 283 { 284 data: '', 285 286 onStartRequest: function (request, context) {}, 287 288 onStopRequest: function (request, context, status) { 289 deferred.resolve(this.data); 290 }, 291 292 onDataAvailable: function (request, context, inputStream, offset, count) { 293 this.data += NetUtil.readInputStreamToString( 294 inputStream, 295 inputStream.available(), 296 { 297 charset: 'UTF-8', 298 replacement: 65533 299 } 300 ) 301 }, 302 303 QueryInterface: function (iid) { 304 if (iid.equals(Components.interfaces.nsISupports) 305 || iid.equals(Components.interfaces.nsIStreamListener)) { 306 return this; 307 } 308 throw Components.results.NS_ERROR_NO_INTERFACE; 309 } 310 }, 311 null 312 ); 313 314 // Send input stream to stream converter 315 var pump = Components.classes["@mozilla.org/network/input-stream-pump;1"] 316 .createInstance(Components.interfaces.nsIInputStreamPump); 317 pump.init(bis, -1, -1, 0, 0, true); 318 pump.asyncRead(converter, null); 319 320 return deferred.promise; 321 }), 322 323 324 /** 325 * Unicode normalization 326 */ 327 "normalize":function(str) { 328 var normalizer = Components.classes["@mozilla.org/intl/unicodenormalizer;1"] 329 .getService(Components.interfaces.nsIUnicodeNormalizer); 330 var obj = {}; 331 str = normalizer.NormalizeUnicodeNFC(str, obj); 332 return obj.value; 333 }, 334 335 336 /** 337 * Return the byte length of a UTF-8 string 338 * 339 * http://stackoverflow.com/a/23329386 340 */ 341 byteLength: function (str) { 342 var s = str.length; 343 for (var i=str.length-1; i>=0; i--) { 344 var code = str.charCodeAt(i); 345 if (code > 0x7f && code <= 0x7ff) s++; 346 else if (code > 0x7ff && code <= 0xffff) s+=2; 347 if (code >= 0xDC00 && code <= 0xDFFF) i--; //trail surrogate 348 } 349 return s; 350 }, 351 352 /** 353 * Display a prompt from an error with custom buttons and a callback 354 */ 355 "errorPrompt":function(title, e) { 356 var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 357 .getService(Components.interfaces.nsIPromptService); 358 var message, buttonText, buttonCallback; 359 360 if (e.dialogButtonText !== undefined) { 361 buttonText = e.dialogButtonText; 362 buttonCallback = e.dialogButtonCallback; 363 } 364 if (e.message) { 365 message = e.message; 366 } 367 else { 368 message = e; 369 } 370 371 if (typeof buttonText == 'undefined') { 372 buttonText = Zotero.getString('errorReport.reportError'); 373 buttonCallback = function () { 374 win.ZoteroPane.reportErrors(); 375 } 376 } 377 // If secondary button is explicitly null, just use an alert 378 else if (buttonText === null) { 379 ps.alert(null, title, message); 380 return; 381 } 382 383 var buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_OK 384 + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING; 385 var index = ps.confirmEx( 386 null, 387 title, 388 message, 389 buttonFlags, 390 "", 391 buttonText, 392 "", null, {} 393 ); 394 395 if (index == 1) { 396 setTimeout(function () { buttonCallback(); }, 1); 397 } 398 }, 399 400 401 /** 402 * saveURI wrapper function 403 * @param {nsIWebBrowserPersist} nsIWebBrowserPersist 404 * @param {nsIURI} uri URL 405 * @param {nsIFile|string path} target file 406 * @param {Object} [headers] 407 */ 408 saveURI: function (wbp, uri, target, headers) { 409 // Handle gzip encoding 410 wbp.persistFlags |= wbp.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION; 411 // If not explicitly using cache, skip it 412 if (!(wbp.persistFlags & wbp.PERSIST_FLAGS_FROM_CACHE)) { 413 wbp.persistFlags |= wbp.PERSIST_FLAGS_BYPASS_CACHE; 414 } 415 416 if (typeof uri == 'string') { 417 uri = Services.io.newURI(uri, null, null); 418 } 419 420 target = Zotero.File.pathToFile(target); 421 422 if (headers) { 423 headers = Object.keys(headers).map(x => x + ": " + headers[x]).join("\r\n") + "\r\n"; 424 } 425 426 wbp.saveURI(uri, null, null, null, null, headers, target, null); 427 }, 428 429 430 saveDocument: function (document, destFile) { 431 const nsIWBP = Components.interfaces.nsIWebBrowserPersist; 432 let wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"] 433 .createInstance(nsIWBP); 434 wbp.persistFlags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES 435 | nsIWBP.PERSIST_FLAGS_FORCE_ALLOW_COOKIES 436 | nsIWBP.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION 437 | nsIWBP.PERSIST_FLAGS_FROM_CACHE 438 | nsIWBP.PERSIST_FLAGS_CLEANUP_ON_FAILURE 439 // Mostly ads 440 | nsIWBP.PERSIST_FLAGS_IGNORE_IFRAMES 441 | nsIWBP.PERSIST_FLAGS_IGNORE_REDIRECTED_DATA; 442 443 let encodingFlags = 0; 444 let filesFolder = null; 445 if (document.contentType == "text/plain") { 446 encodingFlags |= nsIWBP.ENCODE_FLAGS_FORMATTED; 447 encodingFlags |= nsIWBP.ENCODE_FLAGS_ABSOLUTE_LINKS; 448 encodingFlags |= nsIWBP.ENCODE_FLAGS_NOFRAMES_CONTENT; 449 } 450 else { 451 encodingFlags |= nsIWBP.ENCODE_FLAGS_ENCODE_BASIC_ENTITIES; 452 453 // Save auxiliary files to the same folder 454 filesFolder = OS.Path.dirname(destFile); 455 } 456 const wrapColumn = 80; 457 458 var deferred = Zotero.Promise.defer(); 459 var listener = new Zotero.WebProgressFinishListener(function () { 460 deferred.resolve(); 461 }); 462 wbp.progressListener = listener; 463 464 wbp.saveDocument( 465 document, 466 Zotero.File.pathToFile(destFile), 467 Zotero.File.pathToFile(filesFolder), 468 null, 469 encodingFlags, 470 wrapColumn 471 ); 472 473 // Cancel save after timeout has passed, so we return an error to the connector and don't stay 474 // saving forever 475 var timeoutID = setTimeout(function () { 476 if (deferred.promise.isPending()) { 477 Zotero.debug("Stopping save for " + document.location.href, 2); 478 //Zotero.debug(listener.getRequest()); 479 deferred.reject("Snapshot save timeout on " + document.location.href); 480 wbp.cancelSave(); 481 } 482 }, this.SNAPSHOT_SAVE_TIMEOUT); 483 deferred.promise.then(() => clearTimeout(timeoutID)); 484 485 return deferred.promise; 486 }, 487 488 489 /** 490 * Launch a process 491 * @param {nsIFile|String} cmd Path to command to launch 492 * @param {String[]} args Arguments given 493 * @return {Promise} Promise resolved to true if command succeeds, or an error otherwise 494 */ 495 "exec": Zotero.Promise.method(function (cmd, args) { 496 if (typeof cmd == 'string') { 497 Components.utils.import("resource://gre/modules/FileUtils.jsm"); 498 cmd = new FileUtils.File(cmd); 499 } 500 501 if(!cmd.isExecutable()) { 502 throw new Error(cmd.path + " is not an executable"); 503 } 504 505 var proc = Components.classes["@mozilla.org/process/util;1"]. 506 createInstance(Components.interfaces.nsIProcess); 507 proc.init(cmd); 508 509 Zotero.debug("Running " + cmd.path + " " + args.map(arg => "'" + arg + "'").join(" ")); 510 511 var deferred = Zotero.Promise.defer(); 512 proc.runwAsync(args, args.length, {"observe":function(subject, topic) { 513 if(topic !== "process-finished") { 514 deferred.reject(new Error(cmd.path+" failed")); 515 } else if(proc.exitValue != 0) { 516 deferred.reject(new Error(cmd.path+" returned exit status "+proc.exitValue)); 517 } else { 518 deferred.resolve(true); 519 } 520 }}); 521 522 return deferred.promise; 523 }), 524 525 /** 526 * Get string data from the clipboard 527 * @param {String[]} mimeType MIME type of data to get 528 * @return {String|null} Clipboard data, or null if none was available 529 */ 530 "getClipboard":function(mimeType) { 531 var clip = Services.clipboard; 532 if (!clip.hasDataMatchingFlavors([mimeType], 1, clip.kGlobalClipboard)) { 533 return null; 534 } 535 var trans = Components.classes["@mozilla.org/widget/transferable;1"] 536 .createInstance(Components.interfaces.nsITransferable); 537 trans.addDataFlavor(mimeType); 538 clip.getData(trans, clip.kGlobalClipboard); 539 var str = {}; 540 try { 541 trans.getTransferData(mimeType, str, {}); 542 str = str.value.QueryInterface(Components.interfaces.nsISupportsString).data; 543 } 544 catch (e) { 545 return null; 546 } 547 return str; 548 }, 549 550 /** 551 * Determine if one Window is a descendant of another Window 552 * @param {DOMWindow} suspected child window 553 * @param {DOMWindow} suspected parent window 554 * @return {boolean} 555 */ 556 "isIframeOf":function isIframeOf(childWindow, parentWindow) { 557 while(childWindow.parent !== childWindow) { 558 childWindow = childWindow.parent; 559 if(childWindow === parentWindow) return true; 560 } 561 }, 562 563 564 /** 565 * Returns a DOMDocument object not attached to any window 566 */ 567 "getDOMDocument": function() { 568 return Components.classes["@mozilla.org/xmlextras/domparser;1"] 569 .createInstance(Components.interfaces.nsIDOMParser) 570 .parseFromString("<!DOCTYPE html><html></html>", "text/html"); 571 }, 572 573 574 /** 575 * Update HTML links within XUL 576 * 577 * @param {HTMLElement} elem - HTML element to modify 578 * @param {Object} [options] - Properties: 579 * .linkEvent - An object to pass to ZoteroPane.loadURI() to 580 * simulate modifier keys for link clicks. For example, to 581 * force links to open in new windows, pass with 582 * .shiftKey = true. If not provided, the actual event will 583 * be used instead. 584 */ 585 updateHTMLInXUL: function (elem, options) { 586 options = options || {}; 587 var links = elem.getElementsByTagName('a'); 588 for (let i = 0; i < links.length; i++) { 589 let a = links[i]; 590 let href = a.getAttribute('href'); 591 a.setAttribute('tooltiptext', href); 592 a.onclick = function (event) { 593 try { 594 let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] 595 .getService(Components.interfaces.nsIWindowMediator); 596 let win = wm.getMostRecentWindow("navigator:browser"); 597 win.ZoteroPane_Local.loadURI(href, options.linkEvent || event) 598 } 599 catch (e) { 600 Zotero.logError(e); 601 } 602 return false; 603 }; 604 } 605 }, 606 607 608 /** 609 * A generator that yields promises that delay for the given intervals 610 * 611 * @param {Array<Integer>} intervals - An array of intervals in milliseconds 612 * @param {Integer} [maxTime] - Total time to wait in milliseconds, after which the delaying 613 * promise will return false. Before maxTime has elapsed, or if 614 * maxTime isn't specified, the promises will yield true. 615 */ 616 "delayGenerator": function* (intervals, maxTime) { 617 var delay; 618 var totalTime = 0; 619 var last = false; 620 while (true) { 621 let interval = intervals.shift(); 622 if (interval) { 623 delay = interval; 624 } 625 626 if (maxTime && (totalTime + delay) > maxTime) { 627 yield Zotero.Promise.resolve(false); 628 } 629 630 totalTime += delay; 631 632 Zotero.debug("Delaying " + delay + " ms"); 633 yield Zotero.Promise.delay(delay).return(true); 634 } 635 }, 636 637 638 /** 639 * Return an input stream that will be filled asynchronously with strings yielded from a 640 * generator. If the generator yields a promise, the promise is waited for, but its value 641 * is not added to the input stream. 642 * 643 * @param {GeneratorFunction|Generator} gen - Promise-returning generator function or 644 * generator 645 * @return {nsIAsyncInputStream} 646 */ 647 getAsyncInputStream: function (gen, onError) { 648 // Initialize generator if necessary 649 var g = gen.next ? gen : gen(); 650 var seq = 0; 651 652 const PR_UINT32_MAX = Math.pow(2, 32) - 1; 653 var pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); 654 pipe.init(true, true, 0, PR_UINT32_MAX, null); 655 656 var os = Components.classes["@mozilla.org/intl/converter-output-stream;1"] 657 .createInstance(Components.interfaces.nsIConverterOutputStream); 658 os.init(pipe.outputStream, 'utf-8', 0, 0x0000); 659 660 661 function onOutputStreamReady(aos) { 662 let currentSeq = seq++; 663 664 var maybePromise = processNextValue(); 665 // If generator returns a promise, wait for it 666 if (maybePromise.then) { 667 maybePromise.then(() => onOutputStreamReady(aos)); 668 } 669 // If more data, tell stream we're ready 670 else if (maybePromise) { 671 aos.asyncWait({ onOutputStreamReady }, 0, 0, Zotero.mainThread); 672 } 673 // Otherwise close the stream 674 else { 675 aos.close(); 676 } 677 }; 678 679 function processNextValue(lastVal) { 680 try { 681 var result = g.next(lastVal); 682 if (result.done) { 683 Zotero.debug("No more data to write"); 684 return false; 685 } 686 if (result.value.then) { 687 return result.value.then(val => processNextValue(val)); 688 } 689 if (typeof result.value != 'string') { 690 throw new Error("Data is not a string or promise (" + result.value + ")"); 691 } 692 os.writeString(result.value); 693 return true; 694 } 695 catch (e) { 696 Zotero.logError(e); 697 if (onError) { 698 try { 699 os.writeString(onError(e)); 700 } 701 catch (e) { 702 Zotero.logError(e); 703 } 704 } 705 os.close(); 706 return false; 707 } 708 } 709 710 pipe.outputStream.asyncWait({ onOutputStreamReady }, 0, 0, Zotero.mainThread); 711 return pipe.inputStream; 712 }, 713 714 715 /** 716 * Converts Zotero.Item to a format expected by translators 717 * This is mostly the Zotero web API item JSON format, but with an attachments 718 * and notes arrays and optional compatibility mappings for older translators. 719 * 720 * @param {Zotero.Item} zoteroItem 721 * @param {Boolean} legacy Add mappings for legacy (pre-4.0.27) translators 722 * @return {Object} 723 */ 724 itemToExportFormat: function (zoteroItem, legacy, skipChildItems) { 725 function addCompatibilityMappings(item, zoteroItem) { 726 item.uniqueFields = {}; 727 728 // Meaningless local item ID, but some older export translators depend on it 729 item.itemID = zoteroItem.id; 730 item.key = zoteroItem.key; // CSV translator exports this 731 732 // "version" is expected to be a field for "computerProgram", which is now 733 // called "versionNumber" 734 delete item.version; 735 if (item.versionNumber) { 736 item.version = item.uniqueFields.version = item.versionNumber; 737 delete item.versionNumber; 738 } 739 740 // SQL instead of ISO-8601 741 item.dateAdded = zoteroItem.dateAdded; 742 item.dateModified = zoteroItem.dateModified; 743 if (item.accessDate) { 744 item.accessDate = zoteroItem.getField('accessDate'); 745 } 746 747 // Map base fields 748 for (let field in item) { 749 let id = Zotero.ItemFields.getID(field); 750 if (!id || !Zotero.ItemFields.isValidForType(id, zoteroItem.itemTypeID)) { 751 continue; 752 } 753 754 let baseField = Zotero.ItemFields.getName( 755 Zotero.ItemFields.getBaseIDFromTypeAndField(item.itemType, field) 756 ); 757 758 if (!baseField || baseField == field) { 759 item.uniqueFields[field] = item[field]; 760 } else { 761 item[baseField] = item[field]; 762 item.uniqueFields[baseField] = item[field]; 763 } 764 } 765 766 // Add various fields for compatibility with translators pre-4.0.27 767 item.itemID = zoteroItem.id; 768 item.libraryID = zoteroItem.libraryID == 1 ? null : zoteroItem.libraryID; 769 770 // Creators 771 if (item.creators) { 772 for (let i=0; i<item.creators.length; i++) { 773 let creator = item.creators[i]; 774 775 if (creator.name) { 776 creator.fieldMode = 1; 777 creator.lastName = creator.name; 778 delete creator.name; 779 } 780 781 // Old format used to supply creatorID (the database ID), but no 782 // translator ever used it 783 } 784 } 785 786 if (!zoteroItem.isRegularItem()) { 787 item.sourceItemKey = item.parentItem; 788 } 789 790 // Tags 791 for (let i=0; i<item.tags.length; i++) { 792 if (!item.tags[i].type) { 793 item.tags[i].type = 0; 794 } 795 // No translator ever used "primary", "fields", or "linkedItems" objects 796 } 797 798 // "related" was never used (array of itemIDs) 799 800 // seeAlso was always present, but it was always an empty array. 801 // Zotero RDF translator pretended to use it 802 item.seeAlso = []; 803 804 if (zoteroItem.isAttachment()) { 805 item.linkMode = item.uniqueFields.linkMode = zoteroItem.attachmentLinkMode; 806 item.mimeType = item.uniqueFields.mimeType = item.contentType; 807 } 808 809 if (item.note) { 810 item.uniqueFields.note = item.note; 811 } 812 813 return item; 814 } 815 816 var item = zoteroItem.toJSON(); 817 818 item.uri = Zotero.URI.getItemURI(zoteroItem); 819 delete item.key; 820 821 if (!skipChildItems && !zoteroItem.isAttachment() && !zoteroItem.isNote()) { 822 // Include attachments 823 item.attachments = []; 824 let attachments = zoteroItem.getAttachments(); 825 for (let i=0; i<attachments.length; i++) { 826 let zoteroAttachment = Zotero.Items.get(attachments[i]), 827 attachment = zoteroAttachment.toJSON(); 828 if (legacy) addCompatibilityMappings(attachment, zoteroAttachment); 829 830 item.attachments.push(attachment); 831 } 832 833 // Include notes 834 item.notes = []; 835 let notes = zoteroItem.getNotes(); 836 for (let i=0; i<notes.length; i++) { 837 let zoteroNote = Zotero.Items.get(notes[i]), 838 note = zoteroNote.toJSON(); 839 if (legacy) addCompatibilityMappings(note, zoteroNote); 840 841 item.notes.push(note); 842 } 843 } 844 845 if (legacy) addCompatibilityMappings(item, zoteroItem); 846 847 return item; 848 }, 849 850 851 extractIdentifiers: function (text) { 852 var identifiers = []; 853 var foundIDs = new Set(); // keep track of identifiers to avoid duplicates 854 855 // First look for DOIs 856 var ids = text.split(/[\s\u00A0]+/); // whitespace + non-breaking space 857 var doi; 858 for (let id of ids) { 859 if ((doi = Zotero.Utilities.cleanDOI(id)) && !foundIDs.has(doi)) { 860 identifiers.push({ 861 DOI: doi 862 }); 863 foundIDs.add(doi); 864 } 865 } 866 867 // Then try ISBNs 868 if (!identifiers.length) { 869 // First try replacing dashes 870 let ids = text.replace(/[\u002D\u00AD\u2010-\u2015\u2212]+/g, "") // hyphens and dashes 871 .toUpperCase(); 872 let ISBN_RE = /(?:\D|^)(97[89]\d{10}|\d{9}[\dX])(?!\d)/g; 873 let isbn; 874 while (isbn = ISBN_RE.exec(ids)) { 875 isbn = Zotero.Utilities.cleanISBN(isbn[1]); 876 if (isbn && !foundIDs.has(isbn)) { 877 identifiers.push({ 878 ISBN: isbn 879 }); 880 foundIDs.add(isbn); 881 } 882 } 883 884 // Next try spaces 885 if (!identifiers.length) { 886 ids = ids.replace(/[ \u00A0]+/g, ""); // space + non-breaking space 887 while (isbn = ISBN_RE.exec(ids)) { 888 isbn = Zotero.Utilities.cleanISBN(isbn[1]); 889 if(isbn && !foundIDs.has(isbn)) { 890 identifiers.push({ 891 ISBN: isbn 892 }); 893 foundIDs.add(isbn); 894 } 895 } 896 } 897 } 898 899 // Next try arXiv 900 if (!identifiers.length) { 901 // arXiv identifiers are extracted without version number 902 // i.e. 0706.0044v1 is extracted as 0706.0044, 903 // because arXiv OAI API doesn't allow to access individual versions 904 let arXiv_RE = /((?:[^A-Za-z]|^)([\-A-Za-z\.]+\/\d{7})(?:(v[0-9]+)|)(?!\d))|((?:\D|^)(\d{4}\.\d{4,5})(?:(v[0-9]+)|)(?!\d))/g; 905 let m; 906 while ((m = arXiv_RE.exec(text))) { 907 let arXiv = m[2] || m[5]; 908 if (arXiv && !foundIDs.has(arXiv)) { 909 identifiers.push({arXiv: arXiv}); 910 foundIDs.add(arXiv); 911 } 912 } 913 } 914 915 // Finally try for PMID 916 if (!identifiers.length) { 917 // PMID; right now, the longest PMIDs are 8 digits, so it doesn't seem like we'll 918 // need to discriminate for a fairly long time 919 let PMID_RE = /(^|\s|,|:)(\d{1,9})(?=\s|,|$)/g; 920 let pmid; 921 while ((pmid = PMID_RE.exec(text)) && !foundIDs.has(pmid)) { 922 identifiers.push({ 923 PMID: pmid[2] 924 }); 925 foundIDs.add(pmid); 926 } 927 } 928 929 return identifiers; 930 }, 931 932 933 /** 934 * Hyphenate an ISBN based on the registrant table available from 935 * https://www.isbn-international.org/range_file_generation 936 * See isbn.js 937 * 938 * @param {String} isbn ISBN-10 or ISBN-13 939 * @param {Boolean} dontValidate Do not attempt to validate check digit 940 * @return {String} Hyphenated ISBN or empty string if invalid ISBN is supplied 941 */ 942 "hyphenateISBN": function(isbn, dontValidate) { 943 isbn = Zotero.Utilities.cleanISBN(isbn, dontValidate); 944 if (!isbn) return ''; 945 946 var ranges = Zotero.ISBN.ranges, 947 parts = [], 948 uccPref, 949 i = 0; 950 if (isbn.length == 10) { 951 uccPref = '978'; 952 } else { 953 uccPref = isbn.substr(0,3); 954 if (!ranges[uccPref]) return ''; // Probably invalid ISBN, but the checksum is OK 955 parts.push(uccPref); 956 i = 3; // Skip ahead 957 } 958 959 var group = '', 960 found = false; 961 while (i < isbn.length-3 /* check digit, publication, registrant */) { 962 group += isbn.charAt(i); 963 if (ranges[uccPref][group]) { 964 parts.push(group); 965 found = true; 966 break; 967 } 968 i++; 969 } 970 971 if (!found) return ''; // Did not find a valid group 972 973 // Array of registrant ranges that are valid for a group 974 // Array always contains an even number of values (as string) 975 // From left to right, the values are paired so that the first indicates a 976 // lower bound of the range and the right indicates an upper bound 977 // The ranges are sorted by increasing number of characters 978 var regRanges = ranges[uccPref][group]; 979 980 var registrant = ''; 981 found = false; 982 i++; // Previous loop 'break'ed early 983 while (!found && i < isbn.length-2 /* check digit, publication */) { 984 registrant += isbn.charAt(i); 985 986 for(let j=0; j < regRanges.length && registrant.length >= regRanges[j].length; j+=2) { 987 if(registrant.length == regRanges[j].length 988 && registrant >= regRanges[j] && registrant <= regRanges[j+1] // Falls within the range 989 ) { 990 parts.push(registrant); 991 found = true; 992 break; 993 } 994 } 995 996 i++; 997 } 998 999 if (!found) return ''; // Outside of valid range, but maybe we need to update our data 1000 1001 parts.push(isbn.substring(i,isbn.length-1)); // Publication is the remainder up to last digit 1002 parts.push(isbn.charAt(isbn.length-1)); // Check digit 1003 1004 return parts.join('-'); 1005 }, 1006 1007 1008 buildLibraryMenu: function (menulist, libraries, selectedLibraryID) { 1009 var menupopup = menulist.firstChild; 1010 while (menupopup.hasChildNodes()) { 1011 menupopup.removeChild(menupopup.firstChild); 1012 } 1013 var selectedIndex = 0; 1014 var i = 0; 1015 for (let library of libraries) { 1016 let menuitem = menulist.ownerDocument.createElement('menuitem'); 1017 menuitem.value = library.libraryID; 1018 menuitem.setAttribute('label', library.name); 1019 menupopup.appendChild(menuitem); 1020 if (library.libraryID == selectedLibraryID) { 1021 selectedIndex = i; 1022 } 1023 i++; 1024 } 1025 1026 menulist.appendChild(menupopup); 1027 menulist.selectedIndex = selectedIndex; 1028 }, 1029 1030 1031 buildLibraryMenuHTML: function (select, libraries, selectedLibraryID) { 1032 var namespaceURI = 'http://www.w3.org/1999/xhtml'; 1033 while (select.hasChildNodes()) { 1034 select.removeChild(select.firstChild); 1035 } 1036 var selectedIndex = 0; 1037 var i = 0; 1038 for (let library of libraries) { 1039 let option = select.ownerDocument.createElementNS(namespaceURI, 'option'); 1040 option.setAttribute('value', library.libraryID); 1041 option.setAttribute('data-editable', library.editable ? 'true' : 'false'); 1042 option.setAttribute('data-filesEditable', library.filesEditable ? 'true' : 'false'); 1043 option.textContent = library.name; 1044 select.appendChild(option); 1045 if (library.libraryID == selectedLibraryID) { 1046 option.setAttribute('selected', 'selected'); 1047 } 1048 i++; 1049 } 1050 }, 1051 1052 1053 /** 1054 * Create a libraryOrCollection DOM tree to place in <menupopup> element. 1055 * If has no children, returns a <menuitem> element, otherwise <menu>. 1056 * 1057 * @param {Library/Collection} libraryOrCollection 1058 * @param {Node<menupopup>} elem parent element 1059 * @param {function} clickAction function to execute on clicking the menuitem. 1060 * Receives the event and libraryOrCollection for given item. 1061 * 1062 * @return {Node<menuitem>/Node<menu>} appended node 1063 */ 1064 createMenuForTarget: function(libraryOrCollection, elem, currentTarget, clickAction) { 1065 var doc = elem.ownerDocument; 1066 function _createMenuitem(label, value, icon, command) { 1067 let menuitem = doc.createElement('menuitem'); 1068 menuitem.setAttribute("label", label); 1069 menuitem.setAttribute("type", "checkbox"); 1070 if (value == currentTarget) { 1071 menuitem.setAttribute("checked", "true"); 1072 } 1073 menuitem.setAttribute("value", value); 1074 menuitem.setAttribute("image", icon); 1075 menuitem.addEventListener('command', command); 1076 menuitem.classList.add('menuitem-iconic'); 1077 return menuitem 1078 } 1079 1080 function _createMenu(label, value, icon, command) { 1081 let menu = doc.createElement('menu'); 1082 menu.setAttribute("label", label); 1083 menu.setAttribute("value", value); 1084 menu.setAttribute("image", icon); 1085 // Allow click on menu itself to select a target 1086 menu.addEventListener('click', command); 1087 menu.classList.add('menu-iconic'); 1088 let menupopup = doc.createElement('menupopup'); 1089 menu.appendChild(menupopup); 1090 return menu; 1091 } 1092 1093 var imageSrc = libraryOrCollection.treeViewImage; 1094 1095 // Create menuitem for library or collection itself, to be placed either directly in the 1096 // containing menu or as the top item in a submenu 1097 var menuitem = _createMenuitem( 1098 libraryOrCollection.name, 1099 libraryOrCollection.treeViewID, 1100 imageSrc, 1101 function (event) { 1102 clickAction(event, libraryOrCollection); 1103 } 1104 ); 1105 1106 var collections; 1107 if (libraryOrCollection.objectType == 'collection') { 1108 collections = Zotero.Collections.getByParent(libraryOrCollection.id); 1109 } else { 1110 collections = Zotero.Collections.getByLibrary(libraryOrCollection.id); 1111 } 1112 1113 // If no subcollections, place menuitem for target directly in containing men 1114 if (collections.length == 0) { 1115 elem.appendChild(menuitem); 1116 return menuitem 1117 } 1118 1119 // Otherwise create a submenu for the target's subcollections 1120 var menu = _createMenu( 1121 libraryOrCollection.name, 1122 libraryOrCollection.treeViewID, 1123 imageSrc, 1124 function (event) { 1125 clickAction(event, libraryOrCollection); 1126 } 1127 ); 1128 var menupopup = menu.firstChild; 1129 menupopup.appendChild(menuitem); 1130 menupopup.appendChild(doc.createElement('menuseparator')); 1131 for (let collection of collections) { 1132 let collectionMenu = this.createMenuForTarget( 1133 collection, elem, currentTarget, clickAction 1134 ); 1135 menupopup.appendChild(collectionMenu); 1136 } 1137 elem.appendChild(menu); 1138 return menu; 1139 }, 1140 1141 1142 // TODO: Move somewhere better 1143 getVirtualCollectionState: function (type) { 1144 switch (type) { 1145 case 'duplicates': 1146 var prefKey = 'duplicateLibraries'; 1147 break; 1148 1149 case 'unfiled': 1150 var prefKey = 'unfiledLibraries'; 1151 break; 1152 1153 default: 1154 throw new Error("Invalid virtual collection type '" + type + "'"); 1155 } 1156 var libraries; 1157 try { 1158 libraries = JSON.parse(Zotero.Prefs.get(prefKey) || '{}'); 1159 if (typeof libraries != 'object') { 1160 throw true; 1161 } 1162 } 1163 // Ignore old/incorrect formats 1164 catch (e) { 1165 Zotero.Prefs.clear(prefKey); 1166 libraries = {}; 1167 } 1168 1169 return libraries; 1170 }, 1171 1172 1173 getVirtualCollectionStateForLibrary: function (libraryID, type) { 1174 return this.getVirtualCollectionState(type)[libraryID] !== false; 1175 }, 1176 1177 1178 setVirtualCollectionStateForLibrary: function (libraryID, type, show) { 1179 switch (type) { 1180 case 'duplicates': 1181 var prefKey = 'duplicateLibraries'; 1182 break; 1183 1184 case 'unfiled': 1185 var prefKey = 'unfiledLibraries'; 1186 break; 1187 1188 default: 1189 throw new Error("Invalid virtual collection type '" + type + "'"); 1190 } 1191 1192 var libraries = this.getVirtualCollectionState(type); 1193 1194 // Update current library 1195 libraries[libraryID] = !!show; 1196 // Remove libraries that don't exist or that are set to true 1197 for (let id of Object.keys(libraries).filter(id => libraries[id] || !Zotero.Libraries.exists(id))) { 1198 delete libraries[id]; 1199 } 1200 Zotero.Prefs.set(prefKey, JSON.stringify(libraries)); 1201 }, 1202 1203 1204 openPreferences: function (paneID, options = {}) { 1205 if (typeof options == 'string') { 1206 Zotero.debug("ZoteroPane.openPreferences() now takes an 'options' object -- update your code", 2); 1207 options = { 1208 action: options 1209 }; 1210 } 1211 1212 var io = { 1213 pane: paneID, 1214 tab: options.tab, 1215 tabIndex: options.tabIndex, 1216 action: options.action 1217 }; 1218 1219 var win = null; 1220 // If window is already open and no special action, just focus it 1221 if (!options.action) { 1222 var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] 1223 .getService(Components.interfaces.nsIWindowMediator); 1224 var enumerator = wm.getEnumerator("zotero:pref"); 1225 if (enumerator.hasMoreElements()) { 1226 var win = enumerator.getNext(); 1227 win.focus(); 1228 if (paneID) { 1229 var pane = win.document.getElementsByAttribute('id', paneID)[0]; 1230 pane.parentElement.showPane(pane); 1231 1232 // TODO: tab/action 1233 } 1234 } 1235 } 1236 if (!win) { 1237 let args = [ 1238 'chrome://zotero/content/preferences/preferences.xul', 1239 'zotero-prefs', 1240 'chrome,titlebar,toolbar,centerscreen,' 1241 + Zotero.Prefs.get('browser.preferences.instantApply', true) ? 'dialog=no' : 'modal', 1242 io 1243 ]; 1244 1245 let win = Services.wm.getMostRecentWindow("navigator:browser"); 1246 if (win) { 1247 win.openDialog(...args); 1248 } 1249 else { 1250 // nsIWindowWatcher needs a wrappedJSObject 1251 args[args.length - 1].wrappedJSObject = args[args.length - 1]; 1252 Services.ww.openWindow(null, ...args); 1253 } 1254 } 1255 1256 return win; 1257 }, 1258 1259 1260 filterStack: function (stack) { 1261 return stack.split(/\n/) 1262 .filter(line => !line.includes('resource://zotero/bluebird')) 1263 .filter(line => !line.includes('XPCOMUtils.jsm')) 1264 .join('\n'); 1265 }, 1266 1267 1268 quitZotero: function(restart=false) { 1269 Zotero.debug("Zotero.Utilities.Internal.quitZotero() is deprecated -- use quit()"); 1270 this.quit(restart); 1271 }, 1272 1273 1274 /** 1275 * Quits the program, optionally restarting. 1276 * @param {Boolean} [restart=false] 1277 */ 1278 quit: function(restart=false) { 1279 var startup = Services.startup; 1280 if (restart) { 1281 Zotero.restarting = true; 1282 } 1283 startup.quit(startup.eAttemptQuit | (restart ? startup.eRestart : 0) ); 1284 } 1285 } 1286 1287 /** 1288 * Runs an AppleScript on OS X 1289 * 1290 * @param script {String} 1291 * @param block {Boolean} Whether the script should block until the process is finished. 1292 */ 1293 Zotero.Utilities.Internal.executeAppleScript = new function() { 1294 var _osascriptFile; 1295 1296 return function(script, block) { 1297 if(_osascriptFile === undefined) { 1298 _osascriptFile = Components.classes["@mozilla.org/file/local;1"]. 1299 createInstance(Components.interfaces.nsILocalFile); 1300 _osascriptFile.initWithPath("/usr/bin/osascript"); 1301 if(!_osascriptFile.exists()) _osascriptFile = false; 1302 } 1303 if(_osascriptFile) { 1304 var proc = Components.classes["@mozilla.org/process/util;1"]. 1305 createInstance(Components.interfaces.nsIProcess); 1306 proc.init(_osascriptFile); 1307 try { 1308 proc.run(!!block, ['-e', script], 2); 1309 } catch(e) {} 1310 } 1311 } 1312 } 1313 1314 1315 /** 1316 * Activates Firefox 1317 */ 1318 Zotero.Utilities.Internal.activate = new function() { 1319 // For Carbon and X11 1320 var _carbon, ProcessSerialNumber, SetFrontProcessWithOptions; 1321 var _x11, _x11Display, _x11RootWindow, XClientMessageEvent, XFetchName, XFree, XQueryTree, 1322 XOpenDisplay, XCloseDisplay, XFlush, XDefaultRootWindow, XInternAtom, XSendEvent, 1323 XMapRaised, XGetWindowProperty, X11Atom, X11Bool, X11Display, X11Window, X11Status; 1324 1325 /** 1326 * Bring a window to the foreground by interfacing directly with X11 1327 */ 1328 function _X11BringToForeground(win, intervalID) { 1329 var windowTitle = win.QueryInterface(Ci.nsIInterfaceRequestor) 1330 .getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIBaseWindow).title; 1331 1332 var x11Window = _X11FindWindow(_x11RootWindow, windowTitle); 1333 if(!x11Window) return; 1334 win.clearInterval(intervalID); 1335 1336 var event = new XClientMessageEvent(); 1337 event.type = 33; /* ClientMessage*/ 1338 event.serial = 0; 1339 event.send_event = 1; 1340 event.message_type = XInternAtom(_x11Display, "_NET_ACTIVE_WINDOW", 0); 1341 event.display = _x11Display; 1342 event.window = x11Window; 1343 event.format = 32; 1344 event.l0 = 2; 1345 var mask = 1<<20 /* SubstructureRedirectMask */ | 1<<19 /* SubstructureNotifyMask */; 1346 1347 if(XSendEvent(_x11Display, _x11RootWindow, 0, mask, event.address())) { 1348 XMapRaised(_x11Display, x11Window); 1349 XFlush(_x11Display); 1350 Zotero.debug("Integration: Activated successfully"); 1351 } else { 1352 Zotero.debug("Integration: An error occurred activating the window"); 1353 } 1354 } 1355 1356 /** 1357 * Find an X11 window given a name 1358 */ 1359 function _X11FindWindow(w, searchName) { 1360 Components.utils.import("resource://gre/modules/ctypes.jsm"); 1361 1362 var res = _X11GetProperty(w, "_NET_CLIENT_LIST", 33 /** XA_WINDOW **/) 1363 || _X11GetProperty(w, "_WIN_CLIENT_LIST", 6 /** XA_CARDINAL **/); 1364 if(!res) return false; 1365 1366 var nClients = res[1], 1367 clientList = ctypes.cast(res[0], X11Window.array(nClients).ptr).contents, 1368 foundName = new ctypes.char.ptr(); 1369 for(var i=0; i<nClients; i++) { 1370 if(XFetchName(_x11Display, clientList.addressOfElement(i).contents, 1371 foundName.address())) { 1372 var foundNameString = undefined; 1373 try { 1374 foundNameString = foundName.readString(); 1375 } catch(e) {} 1376 XFree(foundName); 1377 if(foundNameString === searchName) return clientList.addressOfElement(i).contents; 1378 } 1379 } 1380 XFree(res[0]); 1381 1382 return false; 1383 } 1384 1385 /** 1386 * Get a property from an X11 window 1387 */ 1388 function _X11GetProperty(win, propertyName, propertyType) { 1389 Components.utils.import("resource://gre/modules/ctypes.jsm"); 1390 1391 var returnType = new X11Atom(), 1392 returnFormat = new ctypes.int(), 1393 nItemsReturned = new ctypes.unsigned_long(), 1394 nBytesAfterReturn = new ctypes.unsigned_long(), 1395 data = new ctypes.char.ptr(); 1396 if(!XGetWindowProperty(_x11Display, win, XInternAtom(_x11Display, propertyName, 0), 0, 1024, 1397 0, propertyType, returnType.address(), returnFormat.address(), 1398 nItemsReturned.address(), nBytesAfterReturn.address(), data.address())) { 1399 var nElements = ctypes.cast(nItemsReturned, ctypes.unsigned_int).value; 1400 if(nElements) return [data, nElements]; 1401 } 1402 return null; 1403 } 1404 1405 return function(win) { 1406 if (Zotero.isMac) { 1407 const BUNDLE_IDS = { 1408 "Zotero":"org.zotero.zotero", 1409 "Firefox":"org.mozilla.firefox", 1410 "Aurora":"org.mozilla.aurora", 1411 "Nightly":"org.mozilla.nightly" 1412 }; 1413 1414 if (win) { 1415 Components.utils.import("resource://gre/modules/ctypes.jsm"); 1416 win.focus(); 1417 1418 if(!_carbon) { 1419 _carbon = ctypes.open("/System/Library/Frameworks/Carbon.framework/Carbon"); 1420 /* 1421 * struct ProcessSerialNumber { 1422 * unsigned long highLongOfPSN; 1423 * unsigned long lowLongOfPSN; 1424 * }; 1425 */ 1426 ProcessSerialNumber = new ctypes.StructType("ProcessSerialNumber", 1427 [{"highLongOfPSN":ctypes.uint32_t}, {"lowLongOfPSN":ctypes.uint32_t}]); 1428 1429 /* 1430 * OSStatus SetFrontProcessWithOptions ( 1431 * const ProcessSerialNumber *inProcess, 1432 * OptionBits inOptions 1433 * ); 1434 */ 1435 SetFrontProcessWithOptions = _carbon.declare("SetFrontProcessWithOptions", 1436 ctypes.default_abi, ctypes.int32_t, ProcessSerialNumber.ptr, 1437 ctypes.uint32_t); 1438 } 1439 1440 var psn = new ProcessSerialNumber(); 1441 psn.highLongOfPSN = 0; 1442 psn.lowLongOfPSN = 2 // kCurrentProcess 1443 1444 win.addEventListener("load", function() { 1445 var res = SetFrontProcessWithOptions( 1446 psn.address(), 1447 1 // kSetFrontProcessFrontWindowOnly = (1 << 0) 1448 ); 1449 }, false); 1450 } else { 1451 Zotero.Utilities.Internal.executeAppleScript('tell application id "'+BUNDLE_IDS[Zotero.appName]+'" to activate'); 1452 } 1453 } else if(!Zotero.isWin && win) { 1454 Components.utils.import("resource://gre/modules/ctypes.jsm"); 1455 1456 if(_x11 === false) return; 1457 if(!_x11) { 1458 try { 1459 _x11 = ctypes.open("libX11.so.6"); 1460 } catch(e) { 1461 try { 1462 var libName = ctypes.libraryName("X11"); 1463 } catch(e) { 1464 _x11 = false; 1465 Zotero.debug("Integration: Could not get libX11 name; not activating"); 1466 Zotero.logError(e); 1467 return; 1468 } 1469 1470 try { 1471 _x11 = ctypes.open(libName); 1472 } catch(e) { 1473 _x11 = false; 1474 Zotero.debug("Integration: Could not open "+libName+"; not activating"); 1475 Zotero.logError(e); 1476 return; 1477 } 1478 } 1479 1480 X11Atom = ctypes.unsigned_long; 1481 X11Bool = ctypes.int; 1482 X11Display = new ctypes.StructType("Display"); 1483 X11Window = ctypes.unsigned_long; 1484 X11Status = ctypes.int; 1485 1486 /* 1487 * typedef struct { 1488 * int type; 1489 * unsigned long serial; / * # of last request processed by server * / 1490 * Bool send_event; / * true if this came from a SendEvent request * / 1491 * Display *display; / * Display the event was read from * / 1492 * Window window; 1493 * Atom message_type; 1494 * int format; 1495 * union { 1496 * char b[20]; 1497 * short s[10]; 1498 * long l[5]; 1499 * } data; 1500 * } XClientMessageEvent; 1501 */ 1502 XClientMessageEvent = new ctypes.StructType("XClientMessageEvent", 1503 [ 1504 {"type":ctypes.int}, 1505 {"serial":ctypes.unsigned_long}, 1506 {"send_event":X11Bool}, 1507 {"display":X11Display.ptr}, 1508 {"window":X11Window}, 1509 {"message_type":X11Atom}, 1510 {"format":ctypes.int}, 1511 {"l0":ctypes.long}, 1512 {"l1":ctypes.long}, 1513 {"l2":ctypes.long}, 1514 {"l3":ctypes.long}, 1515 {"l4":ctypes.long} 1516 ] 1517 ); 1518 1519 /* 1520 * Status XFetchName( 1521 * Display* display, 1522 * Window w, 1523 * char** window_name_return 1524 * ); 1525 */ 1526 XFetchName = _x11.declare("XFetchName", ctypes.default_abi, X11Status, 1527 X11Display.ptr, X11Window, ctypes.char.ptr.ptr); 1528 1529 /* 1530 * Status XQueryTree( 1531 * Display* display, 1532 * Window w, 1533 * Window* root_return, 1534 * Window* parent_return, 1535 * Window** children_return, 1536 * unsigned int* nchildren_return 1537 * ); 1538 */ 1539 XQueryTree = _x11.declare("XQueryTree", ctypes.default_abi, X11Status, 1540 X11Display.ptr, X11Window, X11Window.ptr, X11Window.ptr, X11Window.ptr.ptr, 1541 ctypes.unsigned_int.ptr); 1542 1543 /* 1544 * int XFree( 1545 * void* data 1546 * ); 1547 */ 1548 XFree = _x11.declare("XFree", ctypes.default_abi, ctypes.int, ctypes.voidptr_t); 1549 1550 /* 1551 * Display *XOpenDisplay( 1552 * _Xconst char* display_name 1553 * ); 1554 */ 1555 XOpenDisplay = _x11.declare("XOpenDisplay", ctypes.default_abi, X11Display.ptr, 1556 ctypes.char.ptr); 1557 1558 /* 1559 * int XCloseDisplay( 1560 * Display* display 1561 * ); 1562 */ 1563 XCloseDisplay = _x11.declare("XCloseDisplay", ctypes.default_abi, ctypes.int, 1564 X11Display.ptr); 1565 1566 /* 1567 * int XFlush( 1568 * Display* display 1569 * ); 1570 */ 1571 XFlush = _x11.declare("XFlush", ctypes.default_abi, ctypes.int, X11Display.ptr); 1572 1573 /* 1574 * Window XDefaultRootWindow( 1575 * Display* display 1576 * ); 1577 */ 1578 XDefaultRootWindow = _x11.declare("XDefaultRootWindow", ctypes.default_abi, 1579 X11Window, X11Display.ptr); 1580 1581 /* 1582 * Atom XInternAtom( 1583 * Display* display, 1584 * _Xconst char* atom_name, 1585 * Bool only_if_exists 1586 * ); 1587 */ 1588 XInternAtom = _x11.declare("XInternAtom", ctypes.default_abi, X11Atom, 1589 X11Display.ptr, ctypes.char.ptr, X11Bool); 1590 1591 /* 1592 * Status XSendEvent( 1593 * Display* display, 1594 * Window w, 1595 * Bool propagate, 1596 * long event_mask, 1597 * XEvent* event_send 1598 * ); 1599 */ 1600 XSendEvent = _x11.declare("XSendEvent", ctypes.default_abi, X11Status, 1601 X11Display.ptr, X11Window, X11Bool, ctypes.long, XClientMessageEvent.ptr); 1602 1603 /* 1604 * int XMapRaised( 1605 * Display* display, 1606 * Window w 1607 * ); 1608 */ 1609 XMapRaised = _x11.declare("XMapRaised", ctypes.default_abi, ctypes.int, 1610 X11Display.ptr, X11Window); 1611 1612 /* 1613 * extern int XGetWindowProperty( 1614 * Display* display, 1615 * Window w, 1616 * Atom property, 1617 * long long_offset, 1618 * long long_length, 1619 * Bool delete, 1620 * Atom req_type, 1621 * Atom* actual_type_return, 1622 * int* actual_format_return, 1623 * unsigned long* nitems_return, 1624 * unsigned long* bytes_after_return, 1625 * unsigned char** prop_return 1626 * ); 1627 */ 1628 XGetWindowProperty = _x11.declare("XGetWindowProperty", ctypes.default_abi, 1629 ctypes.int, X11Display.ptr, X11Window, X11Atom, ctypes.long, ctypes.long, 1630 X11Bool, X11Atom, X11Atom.ptr, ctypes.int.ptr, ctypes.unsigned_long.ptr, 1631 ctypes.unsigned_long.ptr, ctypes.char.ptr.ptr); 1632 1633 1634 _x11Display = XOpenDisplay(null); 1635 if(!_x11Display) { 1636 Zotero.debug("Integration: Could not open display; not activating"); 1637 _x11 = false; 1638 return; 1639 } 1640 1641 Zotero.addShutdownListener(function() { 1642 XCloseDisplay(_x11Display); 1643 }); 1644 1645 _x11RootWindow = XDefaultRootWindow(_x11Display); 1646 if(!_x11RootWindow) { 1647 Zotero.debug("Integration: Could not get root window; not activating"); 1648 _x11 = false; 1649 return; 1650 } 1651 } 1652 1653 win.addEventListener("load", function() { 1654 var intervalID; 1655 intervalID = win.setInterval(function() { 1656 _X11BringToForeground(win, intervalID); 1657 }, 50); 1658 }, false); 1659 } 1660 } 1661 }; 1662 1663 Zotero.Utilities.Internal.sendToBack = function() { 1664 if (Zotero.isMac) { 1665 Zotero.Utilities.Internal.executeAppleScript(` 1666 tell application "System Events" 1667 if frontmost of application id "org.zotero.zotero" then 1668 set visible of process "Zotero" to false 1669 end if 1670 end tell 1671 `); 1672 } 1673 } 1674 1675 /** 1676 * Base64 encode / decode 1677 * From http://www.webtoolkit.info/ 1678 */ 1679 Zotero.Utilities.Internal.Base64 = { 1680 // private property 1681 _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", 1682 1683 // public method for encoding 1684 encode : function (input) { 1685 var output = ""; 1686 var chr1, chr2, chr3, enc1, enc2, enc3, enc4; 1687 var i = 0; 1688 1689 input = this._utf8_encode(input); 1690 1691 while (i < input.length) { 1692 1693 chr1 = input.charCodeAt(i++); 1694 chr2 = input.charCodeAt(i++); 1695 chr3 = input.charCodeAt(i++); 1696 1697 enc1 = chr1 >> 2; 1698 enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 1699 enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 1700 enc4 = chr3 & 63; 1701 1702 if (isNaN(chr2)) { 1703 enc3 = enc4 = 64; 1704 } else if (isNaN(chr3)) { 1705 enc4 = 64; 1706 } 1707 1708 output = output + 1709 this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) + 1710 this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4); 1711 1712 } 1713 1714 return output; 1715 }, 1716 1717 // public method for decoding 1718 decode : function (input) { 1719 var output = ""; 1720 var chr1, chr2, chr3; 1721 var enc1, enc2, enc3, enc4; 1722 var i = 0; 1723 1724 input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); 1725 1726 while (i < input.length) { 1727 1728 enc1 = this._keyStr.indexOf(input.charAt(i++)); 1729 enc2 = this._keyStr.indexOf(input.charAt(i++)); 1730 enc3 = this._keyStr.indexOf(input.charAt(i++)); 1731 enc4 = this._keyStr.indexOf(input.charAt(i++)); 1732 1733 chr1 = (enc1 << 2) | (enc2 >> 4); 1734 chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); 1735 chr3 = ((enc3 & 3) << 6) | enc4; 1736 1737 output = output + String.fromCharCode(chr1); 1738 1739 if (enc3 != 64) { 1740 output = output + String.fromCharCode(chr2); 1741 } 1742 if (enc4 != 64) { 1743 output = output + String.fromCharCode(chr3); 1744 } 1745 1746 } 1747 1748 output = this._utf8_decode(output); 1749 1750 return output; 1751 1752 }, 1753 1754 // private method for UTF-8 encoding 1755 _utf8_encode : function (string) { 1756 string = string.replace(/\r\n/g,"\n"); 1757 var utftext = ""; 1758 1759 for (var n = 0; n < string.length; n++) { 1760 1761 var c = string.charCodeAt(n); 1762 1763 if (c < 128) { 1764 utftext += String.fromCharCode(c); 1765 } 1766 else if((c > 127) && (c < 2048)) { 1767 utftext += String.fromCharCode((c >> 6) | 192); 1768 utftext += String.fromCharCode((c & 63) | 128); 1769 } 1770 else { 1771 utftext += String.fromCharCode((c >> 12) | 224); 1772 utftext += String.fromCharCode(((c >> 6) & 63) | 128); 1773 utftext += String.fromCharCode((c & 63) | 128); 1774 } 1775 1776 } 1777 1778 return utftext; 1779 }, 1780 1781 // private method for UTF-8 decoding 1782 _utf8_decode : function (utftext) { 1783 var string = ""; 1784 var i = 0; 1785 var c = c1 = c2 = 0; 1786 1787 while ( i < utftext.length ) { 1788 1789 c = utftext.charCodeAt(i); 1790 1791 if (c < 128) { 1792 string += String.fromCharCode(c); 1793 i++; 1794 } 1795 else if((c > 191) && (c < 224)) { 1796 c2 = utftext.charCodeAt(i+1); 1797 string += String.fromCharCode(((c & 31) << 6) | (c2 & 63)); 1798 i += 2; 1799 } 1800 else { 1801 c2 = utftext.charCodeAt(i+1); 1802 c3 = utftext.charCodeAt(i+2); 1803 string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); 1804 i += 3; 1805 } 1806 1807 } 1808 1809 return string; 1810 } 1811 }