translate_item.js (33262B)
1 /* 2 ***** BEGIN LICENSE BLOCK ***** 3 4 Copyright © 2012 Center for History and New Media 5 George Mason University, Fairfax, Virginia, USA 6 http://zotero.org 7 8 This file is part of Zotero. 9 10 Zotero is free software: you can redistribute it and/or modify 11 it under the terms of the GNU Affero General Public License as published by 12 the Free Software Foundation, either version 3 of the License, or 13 (at your option) any later version. 14 15 Zotero is distributed in the hope that it will be useful, 16 but WITHOUT ANY WARRANTY; without even the implied warranty of 17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 GNU Affero General Public License for more details. 19 20 You should have received a copy of the GNU Affero General Public License 21 along with Zotero. If not, see <http://www.gnu.org/licenses/>. 22 23 ***** END LICENSE BLOCK ***** 24 */ 25 26 27 /** 28 * Save translator items 29 * @constructor 30 * @param {Object} options 31 * <li>libraryID - ID of library in which items should be saved</li> 32 * <li>collections - New collections to create (used during Import translation</li> 33 * <li>attachmentMode - One of Zotero.Translate.ItemSaver.ATTACHMENT_* specifying how attachments should be saved</li> 34 * <li>forceTagType - Force tags to specified tag type</li> 35 * <li>cookieSandbox - Cookie sandbox for attachment requests</li> 36 * <li>proxy - A proxy to deproxify item URLs</li> 37 * <li>baseURI - URI to which attachment paths should be relative</li> 38 * <li>saveOptions - Options to pass to DataObject::save() (e.g., skipSelect)</li> 39 */ 40 Zotero.Translate.ItemSaver = function(options) { 41 // initialize constants 42 this._IDMap = {}; 43 44 // determine library ID 45 if(!options.libraryID) { 46 this._libraryID = Zotero.Libraries.userLibraryID; 47 } else { 48 this._libraryID = options.libraryID; 49 } 50 51 this._collections = options.collections || false; 52 53 // If group filesEditable==false, don't save attachments 54 this.attachmentMode = Zotero.Libraries.get(this._libraryID).filesEditable ? options.attachmentMode : 55 Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE; 56 this._forceTagType = options.forceTagType; 57 this._referrer = options.referrer; 58 this._cookieSandbox = options.cookieSandbox; 59 this._proxy = options.proxy; 60 61 // the URI to which other URIs are assumed to be relative 62 if(typeof options.baseURI === "object" && options.baseURI instanceof Components.interfaces.nsIURI) { 63 this._baseURI = options.baseURI; 64 } else { 65 // try to convert to a URI 66 try { 67 this._baseURI = Components.classes["@mozilla.org/network/io-service;1"]. 68 getService(Components.interfaces.nsIIOService).newURI(options.baseURI, null, null); 69 } catch(e) {}; 70 } 71 this._saveOptions = options.saveOptions || {}; 72 }; 73 74 Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE = 0; 75 Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD = 1; 76 Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE = 2; 77 78 Zotero.Translate.ItemSaver.prototype = { 79 /** 80 * Saves items to Standalone or the server 81 * @param items Items in Zotero.Item.toArray() format 82 * @param {Function} [attachmentCallback] A callback that receives information about attachment 83 * save progress. The callback will be called as attachmentCallback(attachment, false, error) 84 * on failure or attachmentCallback(attachment, progressPercent) periodically during saving. 85 * @param {Function} [itemsDoneCallback] A callback that is called once all top-level items are 86 * done saving with a list of items. Will include saved notes, but exclude attachments. 87 */ 88 saveItems: Zotero.Promise.coroutine(function* (items, attachmentCallback, itemsDoneCallback) { 89 let newItems = [], standaloneAttachments = [], childAttachments = []; 90 yield Zotero.DB.executeTransaction(function* () { 91 for (let iitem=0; iitem<items.length; iitem++) { 92 let item = items[iitem], newItem, myID; 93 // Type defaults to "webpage" 94 let type = (item.itemType ? item.itemType : "webpage"); 95 96 if (type == "note") { // handle notes differently 97 newItem = yield this._saveNote(item); 98 } 99 // Handle standalone attachments differently 100 else if (type == "attachment") { 101 if (this._canSaveAttachment(item)) { 102 standaloneAttachments.push(item); 103 attachmentCallback(item, 0); 104 } 105 continue; 106 } else { 107 newItem = new Zotero.Item(type); 108 newItem.libraryID = this._libraryID; 109 if (item.creators) this._cleanCreators(item.creators); 110 if(item.tags) item.tags = this._cleanTags(item.tags); 111 112 if (item.accessDate == 'CURRENT_TIMESTAMP') { 113 item.accessDate = Zotero.Date.dateToISO(new Date()); 114 } 115 116 // Need to handle these specially. Put them in a separate object to 117 // avoid a warning from fromJSON() 118 let specialFields = { 119 attachments:item.attachments, 120 notes:item.notes, 121 seeAlso:item.seeAlso, 122 id:item.itemID || item.id 123 }; 124 newItem.fromJSON(this._deleteIrrelevantFields(item)); 125 126 // deproxify url 127 if (this._proxy && item.url) { 128 let url = this._proxy.toProper(item.url); 129 Zotero.debug(`Deproxifying item url ${item.url} with scheme ${this._proxy.scheme} to ${url}`, 5); 130 newItem.setField('url', url); 131 } 132 133 if (this._collections) { 134 newItem.setCollections(this._collections); 135 } 136 137 // save item 138 myID = yield newItem.save(this._saveOptions); 139 140 // handle notes 141 if (specialFields.notes) { 142 for (let i=0; i<specialFields.notes.length; i++) { 143 yield this._saveNote(specialFields.notes[i], myID); 144 } 145 item.notes = specialFields.notes; 146 } 147 148 // handle attachments 149 if (specialFields.attachments) { 150 for (let attachment of specialFields.attachments) { 151 if (!this._canSaveAttachment(attachment)) { 152 continue; 153 } 154 attachmentCallback(attachment, 0); 155 childAttachments.push([attachment, myID]); 156 } 157 // Restore the attachments field, since we use it later in 158 // translation 159 item.attachments = specialFields.attachments; 160 } 161 162 // handle see also 163 this._handleRelated(specialFields, newItem); 164 } 165 166 // add to new item list 167 newItems.push(newItem); 168 } 169 }.bind(this)); 170 171 if (itemsDoneCallback) { 172 itemsDoneCallback(newItems.splice()); 173 } 174 175 // Handle attachments outside of the transaction, because they can involve downloading 176 for (let item of standaloneAttachments) { 177 let newItem = yield this._saveAttachment(item, null, attachmentCallback); 178 if (newItem) newItems.push(newItem); 179 } 180 for (let a of childAttachments) { 181 // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=449811 (fixed in Fx51?) 182 let [item, parentItemID] = a; 183 yield this._saveAttachment(item, parentItemID, attachmentCallback); 184 } 185 186 return newItems; 187 }), 188 189 "saveCollections": Zotero.Promise.coroutine(function* (collections) { 190 var collectionsToProcess = collections.slice(); 191 // Use first collection passed to translate process as the root 192 var rootCollectionID = (this._collections && this._collections.length) 193 ? this._collections[0] : null; 194 var parentIDs = collections.map(c => null); 195 var topLevelCollections = []; 196 197 yield Zotero.DB.executeTransaction(function* () { 198 while(collectionsToProcess.length) { 199 var collection = collectionsToProcess.shift(); 200 var parentID = parentIDs.shift(); 201 202 var newCollection = new Zotero.Collection; 203 newCollection.libraryID = this._libraryID; 204 newCollection.name = collection.name; 205 if (parentID) { 206 newCollection.parentID = parentID; 207 } 208 else { 209 newCollection.parentID = rootCollectionID; 210 topLevelCollections.push(newCollection) 211 } 212 yield newCollection.save(this._saveOptions); 213 214 var toAdd = []; 215 216 for(var i=0; i<collection.children.length; i++) { 217 var child = collection.children[i]; 218 if(child.type === "collection") { 219 // do recursive processing of collections 220 collectionsToProcess.push(child); 221 parentIDs.push(newCollection.id); 222 } else { 223 // add mapped items to collection 224 if(this._IDMap[child.id]) { 225 toAdd.push(this._IDMap[child.id]); 226 } else { 227 Zotero.debug("Translate: Could not map "+child.id+" to an imported item", 2); 228 } 229 } 230 } 231 232 if(toAdd.length) { 233 Zotero.debug("Translate: Adding " + toAdd, 5); 234 yield newCollection.addItems(toAdd); 235 } 236 } 237 }.bind(this)); 238 239 return topLevelCollections; 240 }), 241 242 /** 243 * Deletes irrelevant fields from an item object to avoid warnings in Item#fromJSON 244 * Also delete some things like dateAdded, dateModified, and path that translators 245 * should not be able to set directly. 246 */ 247 "_deleteIrrelevantFields": function(item) { 248 const DELETE_FIELDS = ["attachments", "notes", "dateAdded", "dateModified", "seeAlso", "version", "id", "itemID", "path"]; 249 for (let i=0; i<DELETE_FIELDS.length; i++) delete item[DELETE_FIELDS[i]]; 250 return item; 251 }, 252 253 254 _canSaveAttachment: function (attachment) { 255 if (this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD) { 256 if (!attachment.url && !attachment.document) { 257 Zotero.debug("Translate: Not adding attachment: no URL specified"); 258 return false; 259 } 260 if (attachment.snapshot !== false) { 261 if (attachment.document || Zotero.MIME.isWebPageType(attachment.mimeType)) { 262 if (!Zotero.Prefs.get("automaticSnapshots")) { 263 Zotero.debug("Translate: Not adding attachment: automatic snapshots are disabled"); 264 return false; 265 } 266 } 267 else { 268 if (!Zotero.Prefs.get("downloadAssociatedFiles")) { 269 Zotero.debug("Translate: Not adding attachment: automatic file attachments are disabled"); 270 return false; 271 } 272 } 273 } 274 return true; 275 } 276 else if (this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE) { 277 return true; 278 } 279 Zotero.debug('Translate: Ignoring attachment due to ATTACHMENT_MODE_IGNORE'); 280 return false; 281 }, 282 283 284 /** 285 * Saves a translator attachment to the database 286 * 287 * @param {Translator Attachment} attachment 288 * @param {Integer} parentItemID - Item to attach to 289 * @param {Function} attachmentCallback Callback function that takes three 290 * parameters: translator attachment object, percent completion (integer), 291 * and an optional error object 292 * 293 * @return {Zotero.Primise<Zotero.Item|False} Flase is returned if attachment 294 * was not saved due to error or user settings. 295 */ 296 _saveAttachment: Zotero.Promise.coroutine(function* (attachment, parentItemID, attachmentCallback) { 297 try { 298 let newAttachment; 299 300 // determine whether to save files and attachments 301 if (this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD) { 302 newAttachment = yield this._saveAttachmentDownload.apply(this, arguments); 303 } else if (this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE) { 304 newAttachment = yield this._saveAttachmentFile.apply(this, arguments); 305 } else { 306 Zotero.debug('Translate: Ignoring attachment due to ATTACHMENT_MODE_IGNORE'); 307 } 308 309 if (!newAttachment) return false; // attachmentCallback should not have been called in this case 310 311 // deproxify url 312 let url = newAttachment.getField('url'); 313 if (this._proxy && url) { 314 newAttachment.setField('url', this._proxy.toProper(url)); 315 } 316 317 // save fields 318 if (attachment.accessDate) newAttachment.setField("accessDate", attachment.accessDate); 319 if (attachment.tags) newAttachment.setTags(this._cleanTags(attachment.tags)); 320 if (attachment.note) newAttachment.setNote(attachment.note); 321 this._handleRelated(attachment, newAttachment); 322 yield newAttachment.saveTx(this._saveOptions); 323 324 Zotero.debug("Translate: Created attachment; id is " + newAttachment.id, 4); 325 attachmentCallback(attachment, 100); 326 return newAttachment; 327 } catch(e) { 328 Zotero.debug(e, 2); 329 attachmentCallback(attachment, false, e); 330 return false; 331 } 332 }), 333 334 _saveAttachmentFile: Zotero.Promise.coroutine(function* (attachment, parentItemID, attachmentCallback) { 335 Zotero.debug("Translate: Adding attachment", 4); 336 attachmentCallback(attachment, 0); 337 338 if(!attachment.url && !attachment.path) { 339 throw new Error("Translate: Ignoring attachment: no path or URL specified"); 340 } 341 342 if (attachment.path) { 343 var url = Zotero.Attachments.cleanAttachmentURI(attachment.path, false); 344 if (url && /^(?:https?|ftp):/.test(url)) { 345 // A web URL. Don't bother parsing it as path below 346 // Some paths may look like URIs though, so don't just test for 'file' 347 // E.g. C:\something 348 if (!attachment.url) attachment.url = attachment.path; 349 delete attachment.path; 350 } 351 } 352 353 let newItem; 354 var file = attachment.path && this._parsePath(attachment.path); 355 if (!file) { 356 if (attachment.path) { 357 let asUrl = Zotero.Attachments.cleanAttachmentURI(attachment.path); 358 if (!attachment.url && !asUrl) { 359 throw new Error("Translate: Could not parse attachment path <" + attachment.path + ">"); 360 } 361 362 if (!attachment.url && asUrl) { 363 Zotero.debug("Translate: attachment path looks like a URI: " + attachment.path); 364 attachment.url = asUrl; 365 delete attachment.path; 366 } 367 } 368 369 let url = Zotero.Attachments.cleanAttachmentURI(attachment.url); 370 if (!url) { 371 throw new Error("Translate: Invalid attachment.url specified <" + attachment.url + ">"); 372 } 373 374 attachment.url = url; 375 url = Components.classes["@mozilla.org/network/io-service;1"] 376 .getService(Components.interfaces.nsIIOService) 377 .newURI(url, null, null); // This cannot fail, since we check above 378 379 // see if this is actually a file URL 380 if(url.scheme == "file") { 381 throw new Error("Translate: Local file attachments cannot be specified in attachment.url"); 382 } else if(url.scheme != "http" && url.scheme != "https") { 383 throw new Error("Translate: " + url.scheme + " protocol is not allowed for attachments from translators."); 384 } 385 386 // At this point, must be a valid HTTP/HTTPS url 387 attachment.linkMode = "linked_file"; 388 newItem = yield Zotero.Attachments.linkFromURL({ 389 url: attachment.url, 390 parentItemID, 391 contentType: attachment.mimeType || undefined, 392 title: attachment.title || undefined, 393 collections: !parentItemID ? this._collections : undefined 394 }); 395 } else { 396 if (attachment.url) { 397 attachment.linkMode = "imported_url"; 398 newItem = yield Zotero.Attachments.importSnapshotFromFile({ 399 file: file, 400 url: attachment.url, 401 title: attachment.title, 402 contentType: attachment.mimeType, 403 charset: attachment.charset, 404 parentItemID, 405 collections: !parentItemID ? this._collections : undefined 406 }); 407 } 408 else { 409 attachment.linkMode = "imported_file"; 410 newItem = yield Zotero.Attachments.importFromFile({ 411 file: file, 412 parentItemID, 413 collections: !parentItemID ? this._collections : undefined 414 }); 415 if (attachment.title) newItem.setField("title", attachment.title); 416 } 417 } 418 419 return newItem; 420 }), 421 422 "_parsePathURI":function(path) { 423 try { 424 var uri = Services.io.newURI(path, "", this._baseURI); 425 } catch(e) { 426 Zotero.debug("Translate: " + path + " is not a valid URI"); 427 return false; 428 } 429 430 try { 431 var file = uri.QueryInterface(Components.interfaces.nsIFileURL).file; 432 } 433 catch (e) { 434 Zotero.debug("Translate: " + uri.spec + " is not a file URI"); 435 return false; 436 } 437 438 if(file.path == '/') { 439 Zotero.debug("Translate: " + path + " points to root directory"); 440 return false; 441 } 442 443 if(!file.exists()) { 444 Zotero.debug("Translate: File at " + file.path + " does not exist"); 445 return false; 446 } 447 448 return file; 449 }, 450 451 "_parseAbsolutePath":function(path) { 452 var file = Components.classes["@mozilla.org/file/local;1"]. 453 createInstance(Components.interfaces.nsILocalFile); 454 try { 455 file.initWithPath(path); 456 } catch(e) { 457 Zotero.debug("Translate: Invalid absolute path: " + path); 458 return false; 459 } 460 461 if(!file.exists()) { 462 Zotero.debug("Translate: File at absolute path " + file.path + " does not exist"); 463 return false; 464 } 465 466 return file; 467 }, 468 469 "_parseRelativePath":function(path) { 470 if (!this._baseURI) { 471 Zotero.debug("Translate: Cannot parse as relative path. No base URI available."); 472 return false; 473 } 474 475 var file = this._baseURI.QueryInterface(Components.interfaces.nsIFileURL).file.parent; 476 var splitPath = path.split(/\//g); 477 for(var i=0; i<splitPath.length; i++) { 478 if(splitPath[i] !== "") file.append(splitPath[i]); 479 } 480 481 if(!file.exists()) { 482 Zotero.debug("Translate: File at " + file.path + " does not exist"); 483 return false; 484 } 485 486 return file; 487 }, 488 489 "_parsePath":function(path) { 490 Zotero.debug("Translate: Attempting to parse path " + path); 491 492 var file; 493 494 // First, try to parse as absolute path 495 if((/^[a-zA-Z]:[\\\/]|^\\\\/.test(path) && Zotero.isWin) // Paths starting with drive letter or network shares starting with \\ 496 || (path[0] === "/" && !Zotero.isWin)) { 497 // Forward slashes on Windows are not allowed in filenames, so we can 498 // assume they're meant to be backslashes. Backslashes are technically 499 // allowed on Linux, so the reverse cannot be done reliably. 500 var nativePath = Zotero.isWin ? path.replace('/', '\\', 'g') : path; 501 if (file = this._parseAbsolutePath(nativePath)) { 502 Zotero.debug("Translate: Got file "+nativePath+" as absolute path"); 503 return file; 504 } 505 } 506 507 // Next, try to parse as URI 508 if((file = this._parsePathURI(path))) { 509 Zotero.debug("Translate: Got "+path+" as URI") 510 return file; 511 } else if(path.substr(0, 7) !== "file://") { 512 // If it was a fully qualified file URI, we can give up now 513 514 // Next, try to parse as relative path, replacing backslashes with slashes 515 if((file = this._parseRelativePath(path.replace(/\\/g, "/")))) { 516 Zotero.debug("Translate: Got file "+path+" as relative path"); 517 return file; 518 } 519 520 // Next, try to parse as relative path, without replacing backslashes with slashes 521 if((file = this._parseRelativePath(path))) { 522 Zotero.debug("Translate: Got file "+path+" as relative path"); 523 return file; 524 } 525 526 if(path[0] !== "/") { 527 // Next, try to parse a path with no / as an absolute URI or path 528 if((file = this._parsePathURI("/"+path))) { 529 Zotero.debug("Translate: Got file "+path+" as broken URI"); 530 return file; 531 } 532 533 if((file = this._parseAbsolutePath("/"+path))) { 534 Zotero.debug("Translate: Got file "+path+" as broken absolute path"); 535 return file; 536 } 537 538 } 539 } 540 541 // Give up 542 Zotero.debug("Translate: Could not find file "+path) 543 544 return false; 545 }, 546 547 _saveAttachmentDownload: Zotero.Promise.coroutine(function* (attachment, parentItemID, attachmentCallback) { 548 Zotero.debug("Translate: Adding attachment", 4); 549 550 let doc = undefined; 551 if(attachment.document) { 552 doc = new XPCNativeWrapper(Zotero.Translate.DOMWrapper.unwrap(attachment.document)); 553 if(!attachment.title) attachment.title = doc.title; 554 } 555 556 // If no title provided, use "Attachment" as title for progress UI (but not for item) 557 let title = attachment.title || null; 558 if(!attachment.title) { 559 attachment.title = Zotero.getString("itemTypes.attachment"); 560 } 561 562 // Commit to saving 563 attachmentCallback(attachment, 0); 564 565 if(attachment.snapshot === false || this.attachmentMode === Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE) { 566 // if snapshot is explicitly set to false, attach as link 567 attachment.linkMode = "linked_url"; 568 let url, mimeType; 569 if(attachment.document) { 570 url = attachment.document.location.href; 571 mimeType = attachment.mimeType || attachment.document.contentType; 572 } else { 573 url = attachment.url 574 mimeType = attachment.mimeType || undefined; 575 } 576 577 if(!mimeType || !title) { 578 Zotero.debug("Translate: mimeType or title is missing; attaching link to URL will be slower"); 579 } 580 581 let cleanURI = Zotero.Attachments.cleanAttachmentURI(url); 582 if (!cleanURI) { 583 throw new Error("Translate: Invalid attachment URL specified <" + url + ">"); 584 } 585 url = Components.classes["@mozilla.org/network/io-service;1"] 586 .getService(Components.interfaces.nsIIOService) 587 .newURI(cleanURI, null, null); // This cannot fail, since we check above 588 589 // Only HTTP/HTTPS links are allowed 590 if(url.scheme != "http" && url.scheme != "https") { 591 throw new Error("Translate: " + url.scheme + " protocol is not allowed for attachments from translators."); 592 } 593 594 return Zotero.Attachments.linkFromURL({ 595 url: cleanURI, 596 parentItemID, 597 contentType: mimeType, 598 title, 599 collections: !parentItemID ? this._collections : undefined 600 }); 601 } 602 603 // Snapshot is not explicitly set to false, import as file attachment 604 605 // Import from document 606 if(attachment.document) { 607 Zotero.debug('Importing attachment from document'); 608 attachment.linkMode = "imported_url"; 609 610 return Zotero.Attachments.importFromDocument({ 611 libraryID: this._libraryID, 612 document: attachment.document, 613 parentItemID, 614 title, 615 collections: !parentItemID ? this._collections : undefined 616 }); 617 } 618 619 // Import from URL 620 let mimeType = attachment.mimeType ? attachment.mimeType : null; 621 let fileBaseName; 622 if (parentItemID) { 623 let parentItem = yield Zotero.Items.getAsync(parentItemID); 624 fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem); 625 } 626 627 Zotero.debug('Importing attachment from URL'); 628 attachment.linkMode = "imported_url"; 629 630 attachmentCallback(attachment, 0); 631 632 return Zotero.Attachments.importFromURL({ 633 libraryID: this._libraryID, 634 url: attachment.url, 635 parentItemID, 636 title, 637 fileBaseName, 638 contentType: mimeType, 639 referrer: this._referrer, 640 cookieSandbox: this._cookieSandbox, 641 collections: !parentItemID ? this._collections : undefined 642 }); 643 }), 644 645 "_saveNote":Zotero.Promise.coroutine(function* (note, parentItemID) { 646 var myNote = new Zotero.Item('note'); 647 myNote.libraryID = this._libraryID; 648 if (parentItemID) { 649 myNote.parentItemID = parentItemID; 650 } 651 652 if(typeof note == "object") { 653 myNote.setNote(note.note); 654 if(note.tags) myNote.setTags(this._cleanTags(note.tags)); 655 this._handleRelated(note, myNote); 656 } else { 657 myNote.setNote(note); 658 } 659 if (!parentItemID && this._collections) { 660 myNote.setCollections(this._collections); 661 } 662 yield myNote.save(this._saveOptions); 663 return myNote; 664 }), 665 666 _cleanCreators: function (creators) { 667 creators.forEach(creator => { 668 if (!creator.creatorType) { 669 Zotero.warn(".creatorType missing in creator -- update translator code"); 670 creator.creatorType = "author"; 671 } 672 }); 673 }, 674 675 /** 676 * Remove automatic tags if automatic tags pref is on, and set type 677 * to automatic if forced 678 */ 679 "_cleanTags":function(tags) { 680 // If all tags are automatic and automatic tags pref is on, return immediately 681 let tagPref = Zotero.Prefs.get("automaticTags"); 682 if(this._forceTagType == 1 && !tagPref) return []; 683 684 let newTags = []; 685 for(let i=0; i<tags.length; i++) { 686 let tag = tags[i]; 687 // Convert raw string to object with 'tag' property 688 if (typeof tag == 'string') { 689 tag = { tag }; 690 } 691 tag.type = this._forceTagType || tag.type || 0; 692 newTags.push(tag); 693 } 694 return newTags; 695 }, 696 697 "_handleRelated":function(item, newItem) { 698 // add to ID map 699 if(item.itemID || item.id) { 700 this._IDMap[item.itemID || item.id] = newItem.id; 701 } 702 703 // // add see alsos 704 // if(item.seeAlso) { 705 // for(var i=0; i<item.seeAlso.length; i++) { 706 // var seeAlso = item.seeAlso[i]; 707 // if(this._IDMap[seeAlso]) { 708 // newItem.addRelatedItem(this._IDMap[seeAlso]); 709 // } 710 // } 711 // newItem.save(); 712 // } 713 } 714 } 715 716 Zotero.Translate.ItemGetter = function() { 717 this._itemsLeft = []; 718 this._collectionsLeft = null; 719 this._exportFileDirectory = null; 720 this.legacy = false; 721 }; 722 723 Zotero.Translate.ItemGetter.prototype = { 724 "setItems":function(items) { 725 this._itemsLeft = items; 726 this._itemsLeft.sort(function(a, b) { return a.id - b.id; }); 727 this.numItems = this._itemsLeft.length; 728 }, 729 730 "setCollection": function (collection, getChildCollections) { 731 // get items in this collection 732 var items = new Set(collection.getChildItems()); 733 734 if(getChildCollections) { 735 // get child collections 736 this._collectionsLeft = Zotero.Collections.getByParent(collection.id, true); 737 738 // get items in child collections 739 for (let collection of this._collectionsLeft) { 740 var childItems = collection.getChildItems(); 741 childItems.forEach(item => items.add(item)); 742 } 743 } 744 745 this._itemsLeft = Array.from(items.values()); 746 this._itemsLeft.sort(function(a, b) { return a.id - b.id; }); 747 this.numItems = this._itemsLeft.length; 748 }, 749 750 "setAll": Zotero.Promise.coroutine(function* (libraryID, getChildCollections) { 751 this._itemsLeft = yield Zotero.Items.getAll(libraryID, true); 752 753 if(getChildCollections) { 754 this._collectionsLeft = Zotero.Collections.getByLibrary(libraryID, true); 755 } 756 757 this._itemsLeft.sort(function(a, b) { return a.id - b.id; }); 758 this.numItems = this._itemsLeft.length; 759 }), 760 761 "exportFiles":function(dir, extension) { 762 // generate directory 763 this._exportFileDirectory = Components.classes["@mozilla.org/file/local;1"]. 764 createInstance(Components.interfaces.nsILocalFile); 765 this._exportFileDirectory.initWithFile(dir.parent); 766 767 // delete this file if it exists 768 if(dir.exists()) { 769 dir.remove(true); 770 } 771 772 // get name 773 var name = dir.leafName; 774 this._exportFileDirectory.append(name); 775 776 // create directory 777 this._exportFileDirectory.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o700); 778 779 // generate a new location for the exported file, with the appropriate 780 // extension 781 var location = Components.classes["@mozilla.org/file/local;1"]. 782 createInstance(Components.interfaces.nsILocalFile); 783 location.initWithFile(this._exportFileDirectory); 784 location.append(name+"."+extension); 785 786 return location; 787 }, 788 789 /** 790 * Converts an attachment to array format and copies it to the export folder if desired 791 */ 792 "_attachmentToArray": function (attachment) { 793 var attachmentArray = Zotero.Utilities.Internal.itemToExportFormat(attachment, this.legacy); 794 var linkMode = attachment.attachmentLinkMode; 795 if(linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) { 796 attachmentArray.localPath = attachment.getFilePath(); 797 798 if(this._exportFileDirectory) { 799 var exportDir = this._exportFileDirectory; 800 801 // Add path and filename if not an internet link 802 let attachFile; 803 if (attachmentArray.localPath) { 804 attachFile = Zotero.File.pathToFile(attachmentArray.localPath); 805 } 806 else { 807 Zotero.logError(`Path doesn't exist for attachment ${attachment.libraryKey} ` 808 + '-- not exporting file'); 809 } 810 // TODO: Make async, but that will require translator changes 811 if (attachFile && attachFile.exists()) { 812 attachmentArray.defaultPath = "files/" + attachment.id + "/" + attachFile.leafName; 813 attachmentArray.filename = attachFile.leafName; 814 815 /** 816 * Copies the attachment file to the specified relative path from the 817 * export directory. 818 * @param {String} attachPath The path to which the file should be exported 819 * including the filename. If supporting files are included, they will be 820 * copied as well without any renaming. 821 * @param {Boolean} overwriteExisting Optional - If this is set to false, the 822 * function will throw an error when exporting a file would require an existing 823 * file to be overwritten. If true, the file will be silently overwritten. 824 * defaults to false if not provided. 825 */ 826 attachmentArray.saveFile = function(attachPath, overwriteExisting) { 827 // Ensure a valid path is specified 828 if(attachPath === undefined || attachPath == "") { 829 throw new Error("ERROR_EMPTY_PATH"); 830 } 831 832 // Set the default value of overwriteExisting if it was not provided 833 if (overwriteExisting === undefined) { 834 overwriteExisting = false; 835 } 836 837 // Separate the path into a list of subdirectories and the attachment filename, 838 // and initialize the required file objects 839 var targetFile = Components.classes["@mozilla.org/file/local;1"]. 840 createInstance(Components.interfaces.nsILocalFile); 841 targetFile.initWithFile(exportDir); 842 for (let dir of attachPath.split("/")) targetFile.append(dir); 843 844 // First, check that we have not gone lower than exportDir in the hierarchy 845 var parent = targetFile, inExportFileDirectory; 846 while((parent = parent.parent)) { 847 if(exportDir.equals(parent)) { 848 inExportFileDirectory = true; 849 break; 850 } 851 } 852 853 if(!inExportFileDirectory) { 854 throw new Error("Invalid path; attachment cannot be placed above export "+ 855 "directory in the file hirarchy"); 856 } 857 858 // Create intermediate directories if they don't exist 859 parent = targetFile; 860 while((parent = parent.parent) && !parent.exists()) { 861 parent.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o700); 862 } 863 864 // Delete any existing file if overwriteExisting is set, or throw an exception 865 // if it is not 866 if(targetFile.exists()) { 867 if(overwriteExisting) { 868 targetFile.remove(false); 869 } else { 870 throw new Error("ERROR_FILE_EXISTS " + targetFile.leafName); 871 } 872 } 873 874 var directory = targetFile.parent; 875 876 // The only attachments that can have multiple supporting files are imported 877 // attachments of mime type text/html 878 // 879 // TEMP: This used to check getNumFiles() here, but that's now async. 880 // It could be restored (using hasMultipleFiles()) when this is made 881 // async, but it's probably not necessary. (The below can also be changed 882 // to use OS.File.DirectoryIterator.) 883 if(attachment.attachmentContentType == "text/html" 884 && linkMode != Zotero.Attachments.LINK_MODE_LINKED_FILE) { 885 // Attachment is a snapshot with supporting files. Check if any of the 886 // supporting files would cause a name conflict, and build a list of transfers 887 // that should be performed 888 var copySrcs = []; 889 var files = attachment.getFile().parent.directoryEntries; 890 while (files.hasMoreElements()) { 891 file = files.getNext(); 892 file.QueryInterface(Components.interfaces.nsIFile); 893 894 // Ignore the main attachment file (has already been checked for name conflict) 895 if(attachFile.equals(file)) { 896 continue; 897 } 898 899 // Remove any existing files in the target destination if overwriteExisting 900 // is set, or throw an exception if it is not 901 var targetSupportFile = targetFile.parent.clone(); 902 targetSupportFile.append(file.leafName); 903 if(targetSupportFile.exists()) { 904 if(overwriteExisting) { 905 targetSupportFile.remove(false); 906 } else { 907 throw new Error("ERROR_FILE_EXISTS " + targetSupportFile.leafName); 908 } 909 } 910 copySrcs.push(file.clone()); 911 } 912 913 // No conflicts were detected or all conflicts were resolved, perform the copying 914 attachFile.copyTo(directory, targetFile.leafName); 915 for(var i = 0; i < copySrcs.length; i++) { 916 copySrcs[i].copyTo(directory, copySrcs[i].leafName); 917 } 918 } else { 919 // Attachment is a single file 920 // Copy the file to the specified location 921 attachFile.copyTo(directory, targetFile.leafName); 922 } 923 924 attachmentArray.path = targetFile.path; 925 }; 926 } 927 } 928 } 929 930 return attachmentArray; 931 }, 932 933 /** 934 * Retrieves the next available item 935 */ 936 "nextItem": function () { 937 while(this._itemsLeft.length != 0) { 938 var returnItem = this._itemsLeft.shift(); 939 // export file data for single files 940 if(returnItem.isAttachment()) { // an independent attachment 941 var returnItemArray = this._attachmentToArray(returnItem); 942 if(returnItemArray) return returnItemArray; 943 } else { 944 var returnItemArray = Zotero.Utilities.Internal.itemToExportFormat(returnItem, this.legacy); 945 946 // get attachments, although only urls will be passed if exportFileData is off 947 returnItemArray.attachments = []; 948 if (returnItem.isRegularItem()) { 949 var attachments = returnItem.getAttachments(); 950 for (let attachmentID of attachments) { 951 var attachment = Zotero.Items.get(attachmentID); 952 var attachmentInfo = this._attachmentToArray(attachment); 953 954 if(attachmentInfo) { 955 returnItemArray.attachments.push(attachmentInfo); 956 } 957 } 958 } 959 960 return returnItemArray; 961 } 962 } 963 return false; 964 }, 965 966 "nextCollection":function() { 967 if(!this._collectionsLeft || this._collectionsLeft.length == 0) return false; 968 969 var returnItem = this._collectionsLeft.shift(); 970 var obj = returnItem.serialize(true); 971 obj.id = obj.primary.collectionID; 972 obj.name = obj.fields.name; 973 return obj; 974 } 975 } 976 Zotero.Translate.ItemGetter.prototype.__defineGetter__("numItemsRemaining", function() { return this._itemsLeft.length });