attachments.js (43890B)
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 Zotero.Attachments = new function(){ 27 // Keep in sync with Zotero.Schema.integrityCheck() 28 this.LINK_MODE_IMPORTED_FILE = 0; 29 this.LINK_MODE_IMPORTED_URL = 1; 30 this.LINK_MODE_LINKED_FILE = 2; 31 this.LINK_MODE_LINKED_URL = 3; 32 this.BASE_PATH_PLACEHOLDER = 'attachments:'; 33 34 var self = this; 35 36 37 /** 38 * @param {Object} options 39 * @param {nsIFile|String} [options.file] - File to add 40 * @param {Integer} [options.libraryID] 41 * @param {Integer[]|String[]} [options.parentItemID] - Parent item to add item to 42 * @param {Integer[]} [options.collections] - Collection keys or ids to add new item to 43 * @param {String} [options.fileBaseName] 44 * @param {String} [options.contentType] 45 * @param {String} [options.charset] 46 * @param {Object} [options.saveOptions] - Options to pass to Zotero.Item::save() 47 * @return {Promise<Zotero.Item>} 48 */ 49 this.importFromFile = Zotero.Promise.coroutine(function* (options) { 50 Zotero.debug('Importing attachment from file'); 51 52 var libraryID = options.libraryID; 53 var file = Zotero.File.pathToFile(options.file); 54 var path = file.path; 55 var leafName = file.leafName; 56 var parentItemID = options.parentItemID; 57 var collections = options.collections; 58 var fileBaseName = options.fileBaseName; 59 var contentType = options.contentType; 60 var charset = options.charset; 61 var saveOptions = options.saveOptions; 62 63 if (fileBaseName) { 64 let ext = Zotero.File.getExtension(path); 65 var newName = fileBaseName + (ext != '' ? '.' + ext : ''); 66 } 67 else { 68 var newName = Zotero.File.getValidFileName(OS.Path.basename(leafName)); 69 } 70 71 if (leafName.endsWith(".lnk")) { 72 throw new Error("Cannot add Windows shortcut"); 73 } 74 if (parentItemID && collections) { 75 throw new Error("parentItemID and collections cannot both be provided"); 76 } 77 78 var attachmentItem, itemID, newFile, contentType, destDir; 79 try { 80 yield Zotero.DB.executeTransaction(function* () { 81 // Create a new attachment 82 attachmentItem = new Zotero.Item('attachment'); 83 if (parentItemID) { 84 let {libraryID: parentLibraryID, key: parentKey} = 85 Zotero.Items.getLibraryAndKeyFromID(parentItemID); 86 attachmentItem.libraryID = parentLibraryID; 87 } 88 else if (libraryID) { 89 attachmentItem.libraryID = libraryID; 90 } 91 attachmentItem.setField('title', newName); 92 attachmentItem.parentID = parentItemID; 93 attachmentItem.attachmentLinkMode = this.LINK_MODE_IMPORTED_FILE; 94 if (collections) { 95 attachmentItem.setCollections(collections); 96 } 97 yield attachmentItem.save(saveOptions); 98 99 // Create directory for attachment files within storage directory 100 destDir = yield this.createDirectoryForItem(attachmentItem); 101 102 // Point to copied file 103 newFile = OS.Path.join(destDir, newName); 104 105 // Copy file to unique filename, which automatically shortens long filenames 106 newFile = Zotero.File.copyToUnique(file, newFile); 107 108 yield Zotero.File.setNormalFilePermissions(newFile.path); 109 110 if (!contentType) { 111 contentType = yield Zotero.MIME.getMIMETypeFromFile(newFile); 112 } 113 attachmentItem.attachmentContentType = contentType; 114 if (charset) { 115 attachmentItem.attachmentCharset = charset; 116 } 117 attachmentItem.attachmentPath = newFile.path; 118 yield attachmentItem.save(saveOptions); 119 }.bind(this)); 120 yield _postProcessFile(attachmentItem, newFile, contentType); 121 } 122 catch (e) { 123 Zotero.logError(e); 124 Zotero.logError("Failed importing file " + file.path); 125 126 // Clean up 127 try { 128 if (destDir && (yield OS.File.exists(destDir))) { 129 yield OS.File.removeDir(destDir); 130 } 131 } 132 catch (e) { 133 Zotero.logError(e); 134 } 135 136 throw e; 137 } 138 139 return attachmentItem; 140 }); 141 142 143 /** 144 * @param {nsIFile|String} [options.file] - File to add 145 * @param {Integer[]|String[]} [options.parentItemID] - Parent item to add item to 146 * @param {Integer[]} [options.collections] - Collection keys or ids to add new item to 147 * @param {Object} [options.saveOptions] - Options to pass to Zotero.Item::save() 148 * @return {Promise<Zotero.Item>} 149 */ 150 this.linkFromFile = Zotero.Promise.coroutine(function* (options) { 151 Zotero.debug('Linking attachment from file'); 152 153 var file = Zotero.File.pathToFile(options.file); 154 var parentItemID = options.parentItemID; 155 var collections = options.collections; 156 var saveOptions = options.saveOptions; 157 158 if (parentItemID && collections) { 159 throw new Error("parentItemID and collections cannot both be provided"); 160 } 161 162 var title = file.leafName; 163 var contentType = yield Zotero.MIME.getMIMETypeFromFile(file); 164 var item = yield _addToDB({ 165 file, 166 title, 167 linkMode: this.LINK_MODE_LINKED_FILE, 168 contentType, 169 parentItemID, 170 collections, 171 saveOptions 172 }); 173 yield _postProcessFile(item, file, contentType); 174 return item; 175 }); 176 177 178 /** 179 * @param {Object} options - 'file', 'url', 'title', 'contentType', 'charset', 'parentItemID', 'singleFile' 180 * @return {Promise<Zotero.Item>} 181 */ 182 this.importSnapshotFromFile = Zotero.Promise.coroutine(function* (options) { 183 Zotero.debug('Importing snapshot from file'); 184 185 var file = Zotero.File.pathToFile(options.file); 186 // TODO: Fix main filename when copying directory, though in that case it's probably 187 // from our own export and already clean 188 var fileName = options.singleFile 189 ? Zotero.File.getValidFileName(file.leafName) 190 : file.leafName; 191 var url = options.url; 192 var title = options.title; 193 var contentType = options.contentType; 194 var charset = options.charset; 195 var parentItemID = options.parentItemID; 196 197 if (!parentItemID) { 198 throw new Error("parentItemID not provided"); 199 } 200 201 var attachmentItem, itemID, destDir, newPath; 202 try { 203 yield Zotero.DB.executeTransaction(function* () { 204 // Create a new attachment 205 attachmentItem = new Zotero.Item('attachment'); 206 let {libraryID, key: parentKey} = Zotero.Items.getLibraryAndKeyFromID(parentItemID); 207 attachmentItem.libraryID = libraryID; 208 attachmentItem.setField('title', title); 209 attachmentItem.setField('url', url); 210 attachmentItem.parentID = parentItemID; 211 attachmentItem.attachmentLinkMode = this.LINK_MODE_IMPORTED_URL; 212 attachmentItem.attachmentContentType = contentType; 213 attachmentItem.attachmentCharset = charset; 214 attachmentItem.attachmentPath = 'storage:' + fileName; 215 216 // DEBUG: this should probably insert access date too so as to 217 // create a proper item, but at the moment this is only called by 218 // translate.js, which sets the metadata fields itself 219 itemID = yield attachmentItem.save(); 220 221 var storageDir = Zotero.getStorageDirectory(); 222 destDir = this.getStorageDirectory(attachmentItem); 223 yield OS.File.removeDir(destDir.path); 224 newPath = OS.Path.join(destDir.path, fileName); 225 // Copy single file to new directory 226 if (options.singleFile) { 227 yield this.createDirectoryForItem(attachmentItem); 228 yield OS.File.copy(file.path, newPath); 229 } 230 // Copy entire parent directory (for HTML snapshots) 231 else { 232 file.parent.copyTo(storageDir, destDir.leafName); 233 } 234 }.bind(this)); 235 yield _postProcessFile( 236 attachmentItem, 237 Zotero.File.pathToFile(newPath), 238 contentType, 239 charset 240 ); 241 } 242 catch (e) { 243 Zotero.logError(e); 244 245 // Clean up 246 try { 247 if (destDir && destDir.exists()) { 248 destDir.remove(true); 249 } 250 } 251 catch (e) { 252 Zotero.logError(e, 1); 253 } 254 255 throw e; 256 } 257 return attachmentItem; 258 }); 259 260 261 /** 262 * @param {Object} options 263 * @param {Integer} options.libraryID 264 * @param {String} options.url 265 * @param {Integer} [options.parentItemID] 266 * @param {Integer[]} [options.collections] 267 * @param {String} [options.title] 268 * @param {String} [options.fileBaseName] 269 * @param {Boolean} [options.renameIfAllowedType=false] 270 * @param {String} [options.contentType] 271 * @param {String} [options.referrer] 272 * @param {CookieSandbox} [options.cookieSandbox] 273 * @param {Object} [options.saveOptions] 274 * @return {Promise<Zotero.Item>} - A promise for the created attachment item 275 */ 276 this.importFromURL = Zotero.Promise.coroutine(function* (options) { 277 var libraryID = options.libraryID; 278 var url = options.url; 279 var parentItemID = options.parentItemID; 280 var collections = options.collections; 281 var title = options.title; 282 var fileBaseName = options.fileBaseName; 283 var renameIfAllowedType = options.renameIfAllowedType; 284 var contentType = options.contentType; 285 var referrer = options.referrer; 286 var cookieSandbox = options.cookieSandbox; 287 var saveOptions = options.saveOptions; 288 289 Zotero.debug('Importing attachment from URL ' + url); 290 291 if (parentItemID && collections) { 292 throw new Error("parentItemID and collections cannot both be provided"); 293 } 294 295 // Throw error on invalid URLs 296 // 297 // TODO: allow other schemes 298 var urlRe = /^https?:\/\/[^\s]*$/; 299 var matches = urlRe.exec(url); 300 if (!matches) { 301 throw new Error("Invalid URL '" + url + "'"); 302 } 303 304 // Save using a hidden browser 305 var nativeHandlerImport = function () { 306 return new Zotero.Promise(function (resolve, reject) { 307 var browser = Zotero.HTTP.loadDocuments( 308 url, 309 Zotero.Promise.coroutine(function* () { 310 let channel = browser.docShell.currentDocumentChannel; 311 if (channel && (channel instanceof Components.interfaces.nsIHttpChannel)) { 312 if (channel.responseStatus < 200 || channel.responseStatus >= 400) { 313 reject(new Error("Invalid response " + channel.responseStatus + " " 314 + channel.responseStatusText + " for '" + url + "'")); 315 return false; 316 } 317 } 318 try { 319 let attachmentItem = yield Zotero.Attachments.importFromDocument({ 320 libraryID, 321 document: browser.contentDocument, 322 parentItemID, 323 title, 324 collections, 325 saveOptions 326 }); 327 resolve(attachmentItem); 328 } 329 catch (e) { 330 Zotero.logError(e); 331 reject(e); 332 } 333 finally { 334 Zotero.Browser.deleteHiddenBrowser(browser); 335 } 336 }), 337 undefined, 338 undefined, 339 true, 340 cookieSandbox 341 ); 342 }); 343 }; 344 345 // Save using remote web browser persist 346 var externalHandlerImport = Zotero.Promise.coroutine(function* (contentType) { 347 // Rename attachment 348 if (renameIfAllowedType && !fileBaseName && this.getRenamedFileTypes().includes(contentType)) { 349 let parentItem = Zotero.Items.get(parentItemID); 350 fileBaseName = this.getFileBaseNameFromItem(parentItem); 351 } 352 if (fileBaseName) { 353 let ext = _getExtensionFromURL(url, contentType); 354 var fileName = fileBaseName + (ext != '' ? '.' + ext : ''); 355 } 356 else { 357 var fileName = _getFileNameFromURL(url, contentType); 358 } 359 360 const nsIWBP = Components.interfaces.nsIWebBrowserPersist; 361 var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"] 362 .createInstance(nsIWBP); 363 if(cookieSandbox) cookieSandbox.attachToInterfaceRequestor(wbp); 364 var encodingFlags = false; 365 366 // Create a temporary directory to save to within the storage directory. 367 // We don't use the normal temp directory because people might have 'storage' 368 // symlinked to another volume, which makes moving complicated. 369 var tmpDir = (yield this.createTemporaryStorageDirectory()).path; 370 var tmpFile = OS.Path.join(tmpDir, fileName); 371 372 // Save to temp dir 373 var deferred = Zotero.Promise.defer(); 374 wbp.progressListener = new Zotero.WebProgressFinishListener(function() { 375 deferred.resolve(); 376 }); 377 378 var nsIURL = Components.classes["@mozilla.org/network/standard-url;1"] 379 .createInstance(Components.interfaces.nsIURL); 380 nsIURL.spec = url; 381 var headers = {}; 382 if (referrer) { 383 headers.Referer = referrer; 384 } 385 Zotero.Utilities.Internal.saveURI(wbp, nsIURL, tmpFile, headers); 386 387 388 yield deferred.promise; 389 let sample = yield Zotero.File.getContentsAsync(tmpFile, null, 1000); 390 try { 391 if (contentType == 'application/pdf' && 392 Zotero.MIME.sniffForMIMEType(sample) != 'application/pdf') { 393 let errString = "Downloaded PDF did not have MIME type " 394 + "'application/pdf' in Attachments.importFromURL()"; 395 Zotero.debug(errString, 2); 396 Zotero.debug(sample, 3); 397 throw(new Error(errString)); 398 } 399 400 // Create DB item 401 var attachmentItem; 402 var destDir; 403 yield Zotero.DB.executeTransaction(function*() { 404 // Create a new attachment 405 attachmentItem = new Zotero.Item('attachment'); 406 if (libraryID) { 407 attachmentItem.libraryID = libraryID; 408 } 409 else if (parentItemID) { 410 let {libraryID: parentLibraryID, key: parentKey} = 411 Zotero.Items.getLibraryAndKeyFromID(parentItemID); 412 attachmentItem.libraryID = parentLibraryID; 413 } 414 attachmentItem.setField('title', title ? title : fileName); 415 attachmentItem.setField('url', url); 416 attachmentItem.setField('accessDate', "CURRENT_TIMESTAMP"); 417 attachmentItem.parentID = parentItemID; 418 attachmentItem.attachmentLinkMode = Zotero.Attachments.LINK_MODE_IMPORTED_URL; 419 attachmentItem.attachmentContentType = contentType; 420 if (collections) { 421 attachmentItem.setCollections(collections); 422 } 423 attachmentItem.attachmentPath = 'storage:' + fileName; 424 var itemID = yield attachmentItem.save(saveOptions); 425 426 Zotero.Fulltext.queueItem(attachmentItem); 427 428 // DEBUG: Does this fail if 'storage' is symlinked to another drive? 429 destDir = this.getStorageDirectory(attachmentItem).path; 430 yield OS.File.move(tmpDir, destDir); 431 }.bind(this)); 432 } catch (e) { 433 try { 434 if (tmpDir) { 435 yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); 436 } 437 if (destDir) { 438 yield OS.File.removeDir(destDir, { ignoreAbsent: true }); 439 } 440 } 441 catch (e) { 442 Zotero.debug(e, 1); 443 } 444 throw e; 445 } 446 447 return attachmentItem; 448 }.bind(this)); 449 450 var process = function (contentType, hasNativeHandler) { 451 // If we can load this natively, use a hidden browser 452 // (so we can get the charset and title and index the document) 453 if (hasNativeHandler) { 454 return nativeHandlerImport(); 455 } 456 457 // Otherwise use a remote web page persist 458 return externalHandlerImport(contentType); 459 } 460 461 if (contentType) { 462 return process(contentType, Zotero.MIME.hasNativeHandler(contentType)); 463 } 464 465 return Zotero.MIME.getMIMETypeFromURL(url, cookieSandbox).spread(process); 466 }); 467 468 469 /** 470 * Create a link attachment from a URL 471 * 472 * @param {Object} options - 'url', 'parentItemID', 'contentType', 'title', 'collections' 473 * @return {Promise<Zotero.Item>} - A promise for the created attachment item 474 */ 475 this.linkFromURL = Zotero.Promise.coroutine(function* (options) { 476 Zotero.debug('Linking attachment from URL'); 477 478 var url = options.url; 479 var parentItemID = options.parentItemID; 480 var contentType = options.contentType; 481 var title = options.title; 482 var collections = options.collections; 483 484 /* Throw error on invalid URLs 485 We currently accept the following protocols: 486 PersonalBrain (brain://) 487 DevonThink (x-devonthink-item://) 488 Notational Velocity (nv://) 489 MyLife Organized (mlo://) 490 Evernote (evernote://) 491 OneNote (onenote://) 492 Kindle (kindle://) 493 Logos (logosres:) 494 Zotero (zotero://) */ 495 496 var urlRe = /^((https?|zotero|evernote|onenote|brain|nv|mlo|kindle|x-devonthink-item|ftp):\/\/|logosres:)[^\s]*$/; 497 var matches = urlRe.exec(url); 498 if (!matches) { 499 throw ("Invalid URL '" + url + "' in Zotero.Attachments.linkFromURL()"); 500 } 501 502 // If no title provided, figure it out from the URL 503 // Web addresses with paths will be whittled to the last element 504 // excluding references and queries. All others are the full string 505 if (!title) { 506 var ioService = Components.classes["@mozilla.org/network/io-service;1"] 507 .getService(Components.interfaces.nsIIOService); 508 var titleURL = ioService.newURI(url, null, null); 509 510 if (titleURL.scheme == 'http' || titleURL.scheme == 'https') { 511 titleURL = titleURL.QueryInterface(Components.interfaces.nsIURL); 512 if (titleURL.path == '/') { 513 title = titleURL.host; 514 } 515 else if (titleURL.fileName) { 516 title = titleURL.fileName; 517 } 518 else { 519 var dir = titleURL.directory.split('/'); 520 title = dir[dir.length - 2]; 521 } 522 } 523 524 if (!title) { 525 title = url; 526 } 527 } 528 529 // Override MIME type to application/pdf if extension is .pdf -- 530 // workaround for sites that respond to the HEAD request with an 531 // invalid MIME type (https://www.zotero.org/trac/ticket/460) 532 var ext = _getExtensionFromURL(url); 533 if (ext == 'pdf') { 534 contentType = 'application/pdf'; 535 } 536 537 return _addToDB({ 538 url, 539 title, 540 linkMode: this.LINK_MODE_LINKED_URL, 541 contentType, 542 parentItemID, 543 collections 544 }); 545 }); 546 547 548 /** 549 * TODO: what if called on file:// document? 550 * 551 * @param {Object} options - 'document', 'parentItemID', 'collections' 552 * @return {Promise<Zotero.Item>} 553 */ 554 this.linkFromDocument = Zotero.Promise.coroutine(function* (options) { 555 Zotero.debug('Linking attachment from document'); 556 557 var document = options.document; 558 var parentItemID = options.parentItemID; 559 var collections = options.collections; 560 561 if (parentItemID && collections) { 562 throw new Error("parentItemID and collections cannot both be provided"); 563 } 564 565 var url = document.location.href; 566 var title = document.title; // TODO: don't use Mozilla-generated title for images, etc. 567 var contentType = document.contentType; 568 569 var item = yield _addToDB({ 570 url, 571 title, 572 linkMode: this.LINK_MODE_LINKED_URL, 573 contentType, 574 charset: document.characterSet, 575 parentItemID, 576 collections 577 }); 578 579 if (Zotero.Fulltext.isCachedMIMEType(contentType)) { 580 // No file, so no point running the PDF indexer 581 //Zotero.Fulltext.indexItems([itemID]); 582 } 583 else if (Zotero.MIME.isTextType(document.contentType)) { 584 yield Zotero.Fulltext.indexDocument(document, item.id); 585 } 586 587 return item; 588 }); 589 590 591 /** 592 * Save a snapshot from a Document 593 * 594 * @param {Object} options - 'libraryID', 'document', 'parentItemID', 'forceTitle', 'collections' 595 * @return {Promise<Zotero.Item>} - A promise for the created attachment item 596 */ 597 this.importFromDocument = Zotero.Promise.coroutine(function* (options) { 598 Zotero.debug('Importing attachment from document'); 599 600 var libraryID = options.libraryID; 601 var document = options.document; 602 var parentItemID = options.parentItemID; 603 var title = options.title; 604 var collections = options.collections; 605 606 if (parentItemID && collections) { 607 throw new Error("parentItemID and parentCollectionIDs cannot both be provided"); 608 } 609 610 var url = document.location.href; 611 title = title ? title : document.title; 612 var contentType = document.contentType; 613 if (Zotero.Attachments.isPDFJS(document)) { 614 contentType = "application/pdf"; 615 } 616 617 var tmpDir = (yield this.createTemporaryStorageDirectory()).path; 618 try { 619 var fileName = Zotero.File.truncateFileName(_getFileNameFromURL(url, contentType), 100); 620 var tmpFile = OS.Path.join(tmpDir, fileName); 621 622 // If we're using the title from the document, make some adjustments 623 if (!options.title) { 624 // Remove e.g. " - Scaled (-17%)" from end of images saved from links, 625 // though I'm not sure why it's getting added to begin with 626 if (contentType.indexOf('image/') === 0) { 627 title = title.replace(/(.+ \([^,]+, [0-9]+x[0-9]+[^\)]+\)) - .+/, "$1" ); 628 } 629 // If not native type, strip mime type data in parens 630 else if (!Zotero.MIME.hasNativeHandler(contentType, _getExtensionFromURL(url))) { 631 title = title.replace(/(.+) \([a-z]+\/[^\)]+\)/, "$1" ); 632 } 633 } 634 635 if ((contentType === 'text/html' || contentType === 'application/xhtml+xml') 636 // Documents from XHR don't work here 637 && document instanceof Ci.nsIDOMDocument) { 638 Zotero.debug('Saving document with saveDocument()'); 639 yield Zotero.Utilities.Internal.saveDocument(document, tmpFile); 640 } 641 else { 642 Zotero.debug("Saving file with saveURI()"); 643 const nsIWBP = Components.interfaces.nsIWebBrowserPersist; 644 var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"] 645 .createInstance(nsIWBP); 646 wbp.persistFlags = nsIWBP.PERSIST_FLAGS_FROM_CACHE; 647 var ioService = Components.classes["@mozilla.org/network/io-service;1"] 648 .getService(Components.interfaces.nsIIOService); 649 var nsIURL = ioService.newURI(url, null, null); 650 var deferred = Zotero.Promise.defer(); 651 wbp.progressListener = new Zotero.WebProgressFinishListener(function () { 652 deferred.resolve(); 653 }); 654 Zotero.Utilities.Internal.saveURI(wbp, nsIURL, tmpFile); 655 yield deferred.promise; 656 } 657 658 var attachmentItem; 659 var destDir; 660 yield Zotero.DB.executeTransaction(function* () { 661 // Create a new attachment 662 attachmentItem = new Zotero.Item('attachment'); 663 if (libraryID) { 664 attachmentItem.libraryID = libraryID; 665 } 666 else if (parentItemID) { 667 let {libraryID: parentLibraryID, key: parentKey} = 668 Zotero.Items.getLibraryAndKeyFromID(parentItemID); 669 attachmentItem.libraryID = parentLibraryID; 670 } 671 attachmentItem.setField('title', title); 672 attachmentItem.setField('url', url); 673 attachmentItem.setField('accessDate', "CURRENT_TIMESTAMP"); 674 attachmentItem.parentID = parentItemID; 675 attachmentItem.attachmentLinkMode = Zotero.Attachments.LINK_MODE_IMPORTED_URL; 676 attachmentItem.attachmentCharset = 'utf-8'; // WPD will output UTF-8 677 attachmentItem.attachmentContentType = contentType; 678 if (collections && collections.length) { 679 attachmentItem.setCollections(collections); 680 } 681 attachmentItem.attachmentPath = 'storage:' + fileName; 682 var itemID = yield attachmentItem.save(); 683 684 Zotero.Fulltext.queueItem(attachmentItem); 685 686 // DEBUG: Does this fail if 'storage' is symlinked to another drive? 687 destDir = this.getStorageDirectory(attachmentItem).path; 688 yield OS.File.move(tmpDir, destDir); 689 }.bind(this)); 690 } 691 catch (e) { 692 Zotero.debug(e, 1); 693 694 // Clean up 695 try { 696 if (tmpDir) { 697 yield OS.File.removeDir(tmpDir, { ignoreAbsent: true }); 698 } 699 if (destDir) { 700 yield OS.File.removeDir(destDir, { ignoreAbsent: true }); 701 } 702 } 703 catch (e) { 704 Zotero.debug(e, 1); 705 } 706 707 throw e; 708 } 709 710 return attachmentItem; 711 }); 712 713 714 /** 715 * @deprecated Use Zotero.Utilities.cleanURL instead 716 */ 717 this.cleanAttachmentURI = function (uri, tryHttp) { 718 Zotero.debug("Zotero.Attachments.cleanAttachmentURI() is deprecated -- use Zotero.Utilities.cleanURL"); 719 return Zotero.Utilities.cleanURL(uri, tryHttp); 720 } 721 722 723 /* 724 * Returns a formatted string to use as the basename of an attachment 725 * based on the metadata of the specified item and a format string 726 * 727 * (Optional) |formatString| specifies the format string -- otherwise 728 * the 'attachmentRenameFormatString' pref is used 729 * 730 * Valid substitution markers: 731 * 732 * %c -- firstCreator 733 * %y -- year (extracted from Date field) 734 * %t -- title 735 * 736 * Fields can be truncated to a certain length by appending an integer 737 * within curly brackets -- e.g. %t{50} truncates the title to 50 characters 738 * 739 * @param {Zotero.Item} item 740 * @param {String} formatString 741 */ 742 this.getFileBaseNameFromItem = function (item, formatString) { 743 if (!(item instanceof Zotero.Item)) { 744 throw new Error("'item' must be a Zotero.Item"); 745 } 746 747 if (!formatString) { 748 formatString = Zotero.Prefs.get('attachmentRenameFormatString'); 749 } 750 751 // Replaces the substitution marker with the field value, 752 // truncating based on the {[0-9]+} modifier if applicable 753 function rpl(field, str) { 754 if (!str) { 755 str = formatString; 756 } 757 758 switch (field) { 759 case 'creator': 760 field = 'firstCreator'; 761 var rpl = '%c'; 762 break; 763 764 case 'year': 765 var rpl = '%y'; 766 break; 767 768 case 'title': 769 var rpl = '%t'; 770 break; 771 } 772 773 switch (field) { 774 case 'year': 775 var value = item.getField('date', true, true); 776 if (value) { 777 value = Zotero.Date.multipartToSQL(value).substr(0, 4); 778 if (value == '0000') { 779 value = ''; 780 } 781 } 782 break; 783 784 default: 785 var value = '' + item.getField(field, false, true); 786 } 787 788 var re = new RegExp("\{?([^%\{\}]*)" + rpl + "(\{[0-9]+\})?" + "([^%\{\}]*)\}?"); 789 790 // If no value for this field, strip entire conditional block 791 // (within curly braces) 792 if (!value) { 793 if (str.match(re)) { 794 return str.replace(re, '') 795 } 796 } 797 798 var f = function(match, p1, p2, p3) { 799 var maxChars = p2 ? p2.replace(/[^0-9]+/g, '') : false; 800 return p1 + (maxChars ? value.substr(0, maxChars) : value) + p3; 801 } 802 803 return str.replace(re, f); 804 } 805 806 formatString = rpl('creator'); 807 formatString = rpl('year'); 808 formatString = rpl('title'); 809 810 formatString = Zotero.File.getValidFileName(formatString); 811 return formatString; 812 } 813 814 815 this.getRenamedFileTypes = function () { 816 try { 817 var types = Zotero.Prefs.get('autoRenameFiles.fileTypes'); 818 return types ? types.split(',') : []; 819 } 820 catch (e) { 821 return []; 822 } 823 }; 824 825 826 this.getRenamedFileBaseNameIfAllowedType = async function (parentItem, file) { 827 var types = this.getRenamedFileTypes(); 828 var contentType = file.endsWith('.pdf') 829 // Don't bother reading file if there's a .pdf extension 830 ? 'application/pdf' 831 : await Zotero.MIME.getMIMETypeFromFile(file); 832 if (!types.includes(contentType)) { 833 return false; 834 } 835 return this.getFileBaseNameFromItem(parentItem); 836 } 837 838 839 /** 840 * Create directory for attachment files within storage directory 841 * 842 * If a directory exists, delete and recreate 843 * 844 * @param {Number} itemID - Item id 845 * @return {Promise<String>} - Path of new directory 846 */ 847 this.createDirectoryForItem = Zotero.Promise.coroutine(function* (item) { 848 if (!(item instanceof Zotero.Item)) { 849 throw new Error("'item' must be a Zotero.Item"); 850 } 851 var dir = this.getStorageDirectory(item).path; 852 // Testing for directories in OS.File, used by removeDir(), is broken on Travis, so use nsIFile 853 if (Zotero.automatedTest) { 854 let nsIFile = Zotero.File.pathToFile(dir); 855 if (nsIFile.exists()) { 856 nsIFile.remove(true); 857 } 858 } 859 else { 860 yield OS.File.removeDir(dir, { ignoreAbsent: true }); 861 } 862 yield Zotero.File.createDirectoryIfMissingAsync(dir); 863 return dir; 864 }); 865 866 867 this.getStorageDirectory = function (item) { 868 if (!(item instanceof Zotero.Item)) { 869 throw new Error("'item' must be a Zotero.Item"); 870 } 871 return this.getStorageDirectoryByLibraryAndKey(item.libraryID, item.key); 872 } 873 874 875 this.getStorageDirectoryByID = function (itemID) { 876 if (!itemID) { 877 throw new Error("itemID not provided"); 878 } 879 var {libraryID, key} = Zotero.Items.getLibraryAndKeyFromID(itemID); 880 if (!key) { 881 throw new Error("Item " + itemID + " not found"); 882 } 883 var dir = Zotero.getStorageDirectory(); 884 dir.append(key); 885 return dir; 886 } 887 888 889 this.getStorageDirectoryByLibraryAndKey = function (libraryID, key) { 890 if (typeof key != 'string' || !key.match(/^[A-Z0-9]{8}$/)) { 891 throw ('key must be an 8-character string in ' 892 + 'Zotero.Attachments.getStorageDirectoryByLibraryAndKey()') 893 } 894 var dir = Zotero.getStorageDirectory(); 895 dir.append(key); 896 return dir; 897 } 898 899 900 this.createTemporaryStorageDirectory = Zotero.Promise.coroutine(function* () { 901 var tmpDir = Zotero.getStorageDirectory(); 902 tmpDir.append("tmp-" + Zotero.Utilities.randomString(6)); 903 yield OS.File.makeDir(tmpDir.path, { 904 unixMode: 0o755 905 }); 906 return tmpDir; 907 }); 908 909 910 /** 911 * If path is within the attachment base directory, return a relative 912 * path prefixed by BASE_PATH_PLACEHOLDER. Otherwise, return unchanged. 913 */ 914 this.getBaseDirectoryRelativePath = function (path) { 915 if (!path || path.startsWith(this.BASE_PATH_PLACEHOLDER)) { 916 return path; 917 } 918 919 var basePath = Zotero.Prefs.get('baseAttachmentPath'); 920 if (!basePath) { 921 return path; 922 } 923 924 if (Zotero.File.directoryContains(basePath, path)) { 925 // Since stored paths can be synced to other platforms, use forward slashes for consistency. 926 // resolveRelativePath() will convert to the appropriate platform-specific slash on use. 927 basePath = OS.Path.normalize(basePath).replace(/\\/g, "/"); 928 path = OS.Path.normalize(path).replace(/\\/g, "/"); 929 // Normalize D:\ vs. D:\foo 930 if (!basePath.endsWith('/')) { 931 basePath += '/'; 932 } 933 path = this.BASE_PATH_PLACEHOLDER + path.substr(basePath.length) 934 } 935 936 return path; 937 }; 938 939 940 /** 941 * Get an absolute path from this base-dir relative path, if we can 942 * 943 * @param {String} path - Absolute path or relative path prefixed by BASE_PATH_PLACEHOLDER 944 * @return {String|false} - Absolute path, or FALSE if no path 945 */ 946 this.resolveRelativePath = function (path) { 947 if (!path.startsWith(Zotero.Attachments.BASE_PATH_PLACEHOLDER)) { 948 return false; 949 } 950 951 var basePath = Zotero.Prefs.get('baseAttachmentPath'); 952 if (!basePath) { 953 Zotero.debug("No base attachment path set -- can't resolve '" + path + "'", 2); 954 return false; 955 } 956 957 return this.fixPathSlashes(OS.Path.join( 958 OS.Path.normalize(basePath), 959 path.substr(Zotero.Attachments.BASE_PATH_PLACEHOLDER.length) 960 )); 961 } 962 963 964 this.fixPathSlashes = function (path) { 965 return path.replace(Zotero.isWin ? /\//g : /\\/g, Zotero.isWin ? "\\" : "/"); 966 } 967 968 969 this.hasMultipleFiles = Zotero.Promise.coroutine(function* (item) { 970 if (!item.isAttachment()) { 971 throw new Error("Item is not an attachment"); 972 } 973 974 var linkMode = item.attachmentLinkMode; 975 switch (linkMode) { 976 case Zotero.Attachments.LINK_MODE_IMPORTED_URL: 977 case Zotero.Attachments.LINK_MODE_IMPORTED_FILE: 978 break; 979 980 default: 981 throw new Error("Invalid attachment link mode"); 982 } 983 984 if (item.attachmentContentType != 'text/html') { 985 return false; 986 } 987 988 var path = yield item.getFilePathAsync(); 989 if (!path) { 990 throw new Error("File not found"); 991 } 992 993 var numFiles = 0; 994 var parent = OS.Path.dirname(path); 995 var iterator = new OS.File.DirectoryIterator(parent); 996 try { 997 while (true) { 998 let entry = yield iterator.next(); 999 if (entry.name.startsWith('.')) { 1000 continue; 1001 } 1002 numFiles++; 1003 if (numFiles > 1) { 1004 break; 1005 } 1006 } 1007 } 1008 catch (e) { 1009 if (e != StopIteration) { 1010 throw e; 1011 } 1012 } 1013 finally { 1014 iterator.close(); 1015 } 1016 return numFiles > 1; 1017 }); 1018 1019 1020 /** 1021 * Returns the number of files in the attachment directory 1022 * 1023 * Only counts if MIME type is text/html 1024 * 1025 * @param {Zotero.Item} item Attachment item 1026 */ 1027 this.getNumFiles = Zotero.Promise.coroutine(function* (item) { 1028 if (!item.isAttachment()) { 1029 throw new Error("Item is not an attachment"); 1030 } 1031 1032 var linkMode = item.attachmentLinkMode; 1033 switch (linkMode) { 1034 case Zotero.Attachments.LINK_MODE_IMPORTED_URL: 1035 case Zotero.Attachments.LINK_MODE_IMPORTED_FILE: 1036 break; 1037 1038 default: 1039 throw new Error("Invalid attachment link mode"); 1040 } 1041 1042 if (item.attachmentContentType != 'text/html') { 1043 return 1; 1044 } 1045 1046 var path = yield item.getFilePathAsync(); 1047 if (!path) { 1048 throw new Error("File not found"); 1049 } 1050 1051 var numFiles = 0; 1052 var parent = OS.Path.dirname(path); 1053 var iterator = new OS.File.DirectoryIterator(parent); 1054 try { 1055 yield iterator.forEach(function (entry) { 1056 if (entry.name.startsWith('.')) { 1057 return; 1058 } 1059 numFiles++; 1060 }) 1061 } 1062 finally { 1063 iterator.close(); 1064 } 1065 return numFiles; 1066 }); 1067 1068 1069 /** 1070 * @param {Zotero.Item} item 1071 * @param {Boolean} [skipHidden=true] - Don't count hidden files 1072 * @return {Promise<Integer>} - Promise for the total file size in bytes 1073 */ 1074 this.getTotalFileSize = Zotero.Promise.coroutine(function* (item, skipHidden = true) { 1075 if (!item.isAttachment()) { 1076 throw new Error("Item is not an attachment"); 1077 } 1078 1079 var linkMode = item.attachmentLinkMode; 1080 switch (linkMode) { 1081 case Zotero.Attachments.LINK_MODE_IMPORTED_URL: 1082 case Zotero.Attachments.LINK_MODE_IMPORTED_FILE: 1083 case Zotero.Attachments.LINK_MODE_LINKED_FILE: 1084 break; 1085 1086 default: 1087 throw new Error("Invalid attachment link mode"); 1088 } 1089 1090 var path = yield item.getFilePathAsync(); 1091 if (!path) { 1092 throw new Error("File not found"); 1093 } 1094 1095 if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) { 1096 return (yield OS.File.stat(path)).size; 1097 } 1098 1099 var size = 0; 1100 var parent = OS.Path.dirname(path); 1101 let iterator = new OS.File.DirectoryIterator(parent); 1102 try { 1103 yield iterator.forEach(function (entry) { 1104 if (skipHidden && entry.name.startsWith('.')) { 1105 return; 1106 } 1107 return OS.File.stat(entry.path) 1108 .then( 1109 function (info) { 1110 size += info.size; 1111 }, 1112 function (e) { 1113 // Can happen if there's a symlink to a missing file 1114 if (e instanceof OS.File.Error && e.becauseNoSuchFile) { 1115 return; 1116 } 1117 else { 1118 throw e; 1119 } 1120 } 1121 ); 1122 }) 1123 } 1124 finally { 1125 iterator.close(); 1126 } 1127 return size; 1128 }); 1129 1130 1131 /** 1132 * Move attachment item, including file, to another library 1133 */ 1134 this.moveAttachmentToLibrary = async function (attachment, libraryID, parentItemID) { 1135 if (attachment.libraryID == libraryID) { 1136 throw new Error("Attachment is already in library " + libraryID); 1137 } 1138 1139 Zotero.DB.requireTransaction(); 1140 1141 var newAttachment = attachment.clone(libraryID); 1142 if (attachment.isImportedAttachment()) { 1143 // Attachment path isn't copied over by clone() if libraryID is different 1144 newAttachment.attachmentPath = attachment.attachmentPath; 1145 } 1146 if (parentItemID) { 1147 newAttachment.parentID = parentItemID; 1148 } 1149 await newAttachment.save(); 1150 1151 // Move files over if they exist 1152 var oldDir; 1153 var newDir; 1154 if (newAttachment.isImportedAttachment()) { 1155 oldDir = this.getStorageDirectory(attachment).path; 1156 if (await OS.File.exists(oldDir)) { 1157 newDir = this.getStorageDirectory(newAttachment).path; 1158 // Target directory shouldn't exist, but remove it if it does 1159 // 1160 // Testing for directories in OS.File, used by removeDir(), is broken on Travis, 1161 // so use nsIFile 1162 if (Zotero.automatedTest) { 1163 let nsIFile = Zotero.File.pathToFile(newDir); 1164 if (nsIFile.exists()) { 1165 nsIFile.remove(true); 1166 } 1167 } 1168 else { 1169 await OS.File.removeDir(newDir, { ignoreAbsent: true }); 1170 } 1171 await OS.File.move(oldDir, newDir); 1172 } 1173 } 1174 1175 try { 1176 await attachment.erase(); 1177 } 1178 catch (e) { 1179 // Move files back if old item can't be deleted 1180 if (newAttachment.isImportedAttachment()) { 1181 try { 1182 await OS.File.move(newDir, oldDir); 1183 } 1184 catch (e) { 1185 Zotero.logError(e); 1186 } 1187 } 1188 throw e; 1189 } 1190 1191 return newAttachment.id; 1192 }; 1193 1194 1195 /** 1196 * Copy attachment item, including file, to another library 1197 */ 1198 this.copyAttachmentToLibrary = Zotero.Promise.coroutine(function* (attachment, libraryID, parentItemID) { 1199 if (attachment.libraryID == libraryID) { 1200 throw new Error("Attachment is already in library " + libraryID); 1201 } 1202 1203 Zotero.DB.requireTransaction(); 1204 1205 var newAttachment = attachment.clone(libraryID); 1206 if (attachment.isImportedAttachment()) { 1207 // Attachment path isn't copied over by clone() if libraryID is different 1208 newAttachment.attachmentPath = attachment.attachmentPath; 1209 } 1210 if (parentItemID) { 1211 newAttachment.parentID = parentItemID; 1212 } 1213 yield newAttachment.save(); 1214 1215 // Copy over files if they exist 1216 if (newAttachment.isImportedAttachment() && (yield attachment.fileExists())) { 1217 let dir = Zotero.Attachments.getStorageDirectory(attachment); 1218 let newDir = yield Zotero.Attachments.createDirectoryForItem(newAttachment); 1219 yield Zotero.File.copyDirectory(dir, newDir); 1220 } 1221 1222 yield newAttachment.addLinkedItem(attachment); 1223 return newAttachment.id; 1224 }); 1225 1226 1227 function _getFileNameFromURL(url, contentType){ 1228 var nsIURL = Components.classes["@mozilla.org/network/standard-url;1"] 1229 .createInstance(Components.interfaces.nsIURL); 1230 nsIURL.spec = url; 1231 1232 var ext = Zotero.MIME.getPrimaryExtension(contentType, nsIURL.fileExtension); 1233 1234 if (!nsIURL.fileName) { 1235 var matches = nsIURL.directory.match(/\/([^\/]+)\/$/); 1236 // If no filename, use the last part of the path if there is one 1237 if (matches) { 1238 nsIURL.fileName = matches[1]; 1239 } 1240 // Or just use the host 1241 else { 1242 nsIURL.fileName = nsIURL.host; 1243 var tld = nsIURL.fileExtension; 1244 } 1245 } 1246 1247 // If we found a better extension, use that 1248 if (ext && (!nsIURL.fileExtension || nsIURL.fileExtension != ext)) { 1249 nsIURL.fileExtension = ext; 1250 } 1251 1252 // If we replaced the TLD (which would've been interpreted as the extension), add it back 1253 if (tld && tld != nsIURL.fileExtension) { 1254 nsIURL.fileBaseName = nsIURL.fileBaseName + '.' + tld; 1255 } 1256 1257 // Test unencoding fileBaseName 1258 try { 1259 decodeURIComponent(nsIURL.fileBaseName); 1260 } 1261 catch (e) { 1262 if (e.name == 'URIError') { 1263 // If we got a 'malformed URI sequence' while decoding, 1264 // use MD5 of fileBaseName 1265 nsIURL.fileBaseName = Zotero.Utilities.Internal.md5(nsIURL.fileBaseName, false); 1266 } 1267 else { 1268 throw e; 1269 } 1270 } 1271 1272 // Pass unencoded name to getValidFileName() so that percent-encoded 1273 // characters aren't stripped to just numbers 1274 return Zotero.File.getValidFileName(decodeURIComponent(nsIURL.fileName)); 1275 } 1276 1277 1278 function _getExtensionFromURL(url, contentType) { 1279 var nsIURL = Components.classes["@mozilla.org/network/standard-url;1"] 1280 .createInstance(Components.interfaces.nsIURL); 1281 nsIURL.spec = url; 1282 return Zotero.MIME.getPrimaryExtension(contentType, nsIURL.fileExtension); 1283 } 1284 1285 1286 /** 1287 * Create a new item of type 'attachment' and add to the itemAttachments table 1288 * 1289 * @param {Object} options - 'file', 'url', 'title', 'linkMode', 'contentType', 'charsetID', 1290 * 'parentItemID', 'saveOptions' 1291 * @return {Promise<Zotero.Item>} - A promise for the new attachment 1292 */ 1293 function _addToDB(options) { 1294 var file = options.file; 1295 var url = options.url; 1296 var title = options.title; 1297 var linkMode = options.linkMode; 1298 var contentType = options.contentType; 1299 var charset = options.charset; 1300 var parentItemID = options.parentItemID; 1301 var collections = options.collections; 1302 var saveOptions = options.saveOptions; 1303 1304 return Zotero.DB.executeTransaction(function* () { 1305 var attachmentItem = new Zotero.Item('attachment'); 1306 if (parentItemID) { 1307 let {libraryID: parentLibraryID, key: parentKey} = 1308 Zotero.Items.getLibraryAndKeyFromID(parentItemID); 1309 if (parentLibraryID != Zotero.Libraries.userLibraryID 1310 && linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) { 1311 throw new Error("Cannot save linked file in non-local library"); 1312 } 1313 attachmentItem.libraryID = parentLibraryID; 1314 } 1315 attachmentItem.setField('title', title); 1316 if (linkMode == self.LINK_MODE_IMPORTED_URL || linkMode == self.LINK_MODE_LINKED_URL) { 1317 attachmentItem.setField('url', url); 1318 attachmentItem.setField('accessDate', "CURRENT_TIMESTAMP"); 1319 } 1320 1321 attachmentItem.parentID = parentItemID; 1322 attachmentItem.attachmentLinkMode = linkMode; 1323 attachmentItem.attachmentContentType = contentType; 1324 attachmentItem.attachmentCharset = charset; 1325 if (file) { 1326 attachmentItem.attachmentPath = file.path; 1327 } 1328 1329 if (collections) { 1330 attachmentItem.setCollections(collections); 1331 } 1332 yield attachmentItem.save(saveOptions); 1333 1334 return attachmentItem; 1335 }.bind(this)); 1336 } 1337 1338 1339 /** 1340 * If necessary/possible, detect the file charset and index the file 1341 * 1342 * Since we have to load the content into the browser to get the 1343 * character set (at least until we figure out a better way to get 1344 * at the native detectors), we create the item above and update 1345 * asynchronously after the fact 1346 * 1347 * @return {Promise} 1348 */ 1349 var _postProcessFile = Zotero.Promise.coroutine(function* (item, file, contentType) { 1350 // Don't try to process if MIME type is unknown 1351 if (!contentType) { 1352 return; 1353 } 1354 1355 // Items with content types that get cached by the fulltext indexer can just be indexed, 1356 // since a charset isn't necessary 1357 if (Zotero.Fulltext.isCachedMIMEType(contentType)) { 1358 return Zotero.Fulltext.indexItems([item.id]); 1359 } 1360 1361 // Ignore non-text types 1362 var ext = Zotero.File.getExtension(file); 1363 if (!Zotero.MIME.hasInternalHandler(contentType, ext) || !Zotero.MIME.isTextType(contentType)) { 1364 return; 1365 } 1366 1367 // If the charset is already set, index item directly 1368 if (item.attachmentCharset) { 1369 return Zotero.Fulltext.indexItems([item.id]); 1370 } 1371 1372 // Otherwise, load in a hidden browser to get the charset, and then index the document 1373 var deferred = Zotero.Promise.defer(); 1374 var browser = Zotero.Browser.createHiddenBrowser(); 1375 1376 if (item.attachmentCharset) { 1377 var onpageshow = function(){ 1378 // ignore spurious about:blank loads 1379 if(browser.contentDocument.location.href == "about:blank") return; 1380 1381 browser.removeEventListener("pageshow", onpageshow, false); 1382 1383 Zotero.Fulltext.indexDocument(browser.contentDocument, itemID) 1384 .then(deferred.resolve, deferred.reject) 1385 .finally(function () { 1386 Zotero.Browser.deleteHiddenBrowser(browser); 1387 }); 1388 }; 1389 browser.addEventListener("pageshow", onpageshow, false); 1390 } 1391 else { 1392 let callback = Zotero.Promise.coroutine(function* (charset, args) { 1393 // ignore spurious about:blank loads 1394 if(browser.contentDocument.location.href == "about:blank") return; 1395 1396 try { 1397 if (charset) { 1398 charset = Zotero.CharacterSets.toCanonical(charset); 1399 if (charset) { 1400 item.attachmentCharset = charset; 1401 yield item.saveTx({ 1402 skipNotifier: true 1403 }); 1404 } 1405 } 1406 1407 yield Zotero.Fulltext.indexDocument(browser.contentDocument, item.id); 1408 Zotero.Browser.deleteHiddenBrowser(browser); 1409 1410 deferred.resolve(); 1411 } 1412 catch (e) { 1413 deferred.reject(e); 1414 } 1415 }); 1416 Zotero.File.addCharsetListener(browser, callback, item.id); 1417 } 1418 1419 var url = Components.classes["@mozilla.org/network/protocol;1?name=file"] 1420 .getService(Components.interfaces.nsIFileProtocolHandler) 1421 .getURLSpecFromFile(file); 1422 browser.loadURI(url); 1423 1424 return deferred.promise; 1425 }); 1426 1427 /** 1428 * Determines if a given document is an instance of PDFJS 1429 * @return {Boolean} 1430 */ 1431 this.isPDFJS = function(doc) { 1432 // pdf.js HACK 1433 // This may no longer be necessary (as of Fx 23) 1434 if(doc.contentType === "text/html") { 1435 var win = doc.defaultView; 1436 if(win) { 1437 win = win.wrappedJSObject; 1438 if(win && "PDFJS" in win) { 1439 return true; 1440 } 1441 } 1442 } 1443 return false; 1444 } 1445 1446 1447 this.linkModeToName = function (linkMode) { 1448 switch (linkMode) { 1449 case this.LINK_MODE_IMPORTED_FILE: 1450 return 'imported_file'; 1451 case this.LINK_MODE_IMPORTED_URL: 1452 return 'imported_url'; 1453 case this.LINK_MODE_LINKED_FILE: 1454 return 'linked_file'; 1455 case this.LINK_MODE_LINKED_URL: 1456 return 'linked_url'; 1457 default: 1458 throw new Error(`Invalid link mode ${linkMode}`); 1459 } 1460 } 1461 1462 1463 this.linkModeFromName = function (linkModeName) { 1464 var prop = "LINK_MODE_" + linkModeName.toUpperCase(); 1465 if (this[prop] !== undefined) { 1466 return this[prop]; 1467 } 1468 throw new Error(`Invalid link mode name '${linkModeName}'`); 1469 } 1470 }