mendeleyImport.js (34261B)
1 var EXPORTED_SYMBOLS = ["Zotero_Import_Mendeley"]; 2 3 Components.utils.import("resource://gre/modules/Services.jsm"); 4 Components.utils.import("resource://gre/modules/osfile.jsm"); 5 Services.scriptloader.loadSubScript("chrome://zotero/content/include.js"); 6 7 var Zotero_Import_Mendeley = function () { 8 this.createNewCollection = null; 9 this.newItems = []; 10 11 this._db; 12 this._file; 13 this._itemDone; 14 this._progress = 0; 15 this._progressMax; 16 }; 17 18 Zotero_Import_Mendeley.prototype.setLocation = function (file) { 19 this._file = file.path || file; 20 }; 21 22 Zotero_Import_Mendeley.prototype.setHandler = function (name, handler) { 23 switch (name) { 24 case 'itemDone': 25 this._itemDone = handler; 26 break; 27 } 28 }; 29 30 Zotero_Import_Mendeley.prototype.getProgress = function () { 31 return this._progress / this._progressMax * 100; 32 }; 33 34 Zotero_Import_Mendeley.prototype.getTranslators = async function () { 35 return [{ 36 label: Zotero.getString('fileInterface.appDatabase', 'Mendeley') 37 }]; 38 }; 39 40 Zotero_Import_Mendeley.prototype.setTranslator = function () {}; 41 42 Zotero_Import_Mendeley.prototype.translate = async function (options) { 43 if (true) { 44 Services.scriptloader.loadSubScript("chrome://zotero/content/import/mendeley/mendeleySchemaMap.js"); 45 } 46 // TEMP: Load uncached from ~/zotero-client for development 47 else { 48 Components.utils.import("resource://gre/modules/FileUtils.jsm"); 49 let file = FileUtils.getDir("Home", []); 50 file = OS.Path.join(file.path, 'zotero-client', 'chrome', 'content', 'zotero', 'import', 'mendeley', 'mendeleySchemaMap.js'); 51 let fileURI = OS.Path.toFileURI(file); 52 let xmlhttp = await Zotero.HTTP.request( 53 'GET', 54 fileURI, 55 { 56 dontCache: true, 57 responseType: 'text' 58 } 59 ); 60 eval(xmlhttp.response); 61 } 62 63 const libraryID = options.libraryID || Zotero.Libraries.userLibraryID; 64 const { key: rootCollectionKey } = options.collections 65 ? Zotero.Collections.getLibraryAndKeyFromID(options.collections[0]) 66 : {}; 67 68 // TODO: Get appropriate version based on schema version 69 const mapVersion = 83; 70 map = map[mapVersion]; 71 72 const mendeleyGroupID = 0; 73 74 // Disable syncing while we're importing 75 var resumeSync = Zotero.Sync.Runner.delayIndefinite(); 76 77 this._db = new Zotero.DBConnection(this._file); 78 79 try { 80 if (!await this._isValidDatabase()) { 81 throw new Error("Not a valid Mendeley database"); 82 } 83 84 // Collections 85 let folders = await this._getFolders(mendeleyGroupID); 86 let collectionJSON = this._foldersToAPIJSON(folders, rootCollectionKey); 87 let folderKeys = this._getFolderKeys(collectionJSON); 88 await this._saveCollections(libraryID, collectionJSON, folderKeys); 89 90 // 91 // Items 92 // 93 let documents = await this._getDocuments(mendeleyGroupID); 94 this._progressMax = documents.length; 95 // Get various attributes mapped to document ids 96 let urls = await this._getDocumentURLs(mendeleyGroupID); 97 let creators = await this._getDocumentCreators(mendeleyGroupID, map.creatorTypes); 98 let tags = await this._getDocumentTags(mendeleyGroupID); 99 let collections = await this._getDocumentCollections( 100 mendeleyGroupID, 101 documents, 102 rootCollectionKey, 103 folderKeys 104 ); 105 let files = await this._getDocumentFiles(mendeleyGroupID); 106 let annotations = await this._getDocumentAnnotations(mendeleyGroupID); 107 for (let document of documents) { 108 let docURLs = urls.get(document.id); 109 let docFiles = files.get(document.id); 110 111 // If there's a single PDF file and a single PDF URL and the file exists, make an 112 // imported_url attachment instead of separate file and linked_url attachments 113 if (docURLs && docFiles) { 114 let pdfFiles = docFiles.filter(x => x.fileURL.endsWith('.pdf')); 115 let pdfURLs = docURLs.filter(x => x.includes('pdf')); 116 if (pdfFiles.length == 1 117 && pdfURLs.length == 1 118 && await this._getRealFilePath(OS.Path.fromFileURI(pdfFiles[0].fileURL))) { 119 // Add URL to PDF attachment 120 docFiles.forEach((x) => { 121 if (x.fileURL.endsWith('.pdf')) { 122 x.title = 'PDF'; 123 x.url = pdfURLs[0]; 124 x.contentType = 'application/pdf'; 125 } 126 }); 127 // Remove PDF URL from URLs array 128 docURLs = docURLs.filter(x => !x.includes('pdf')); 129 } 130 } 131 132 // Save each document with its attributes 133 let itemJSON = await this._documentToAPIJSON( 134 map, 135 document, 136 docURLs, 137 creators.get(document.id), 138 tags.get(document.id), 139 collections.get(document.id), 140 annotations.get(document.id) 141 ); 142 let documentIDMap = await this._saveItems(libraryID, itemJSON); 143 // Save the document's attachments and extracted annotations for any of them 144 if (docFiles) { 145 await this._saveFilesAndAnnotations( 146 docFiles, 147 libraryID, 148 documentIDMap.get(document.id), 149 annotations.get(document.id) 150 ); 151 } 152 this.newItems.push(Zotero.Items.get(documentIDMap.get(document.id))); 153 this._progress++; 154 if (this._itemDone) { 155 this._itemDone(); 156 } 157 } 158 } 159 finally { 160 try { 161 await this._db.closeDatabase(); 162 } 163 catch (e) { 164 Zotero.logError(e); 165 } 166 167 resumeSync(); 168 } 169 }; 170 171 Zotero_Import_Mendeley.prototype._isValidDatabase = async function () { 172 var tables = [ 173 'DocumentContributors', 174 'DocumentFiles', 175 'DocumentFolders', 176 'DocumentKeywords', 177 'DocumentTags', 178 'DocumentUrls', 179 'Documents', 180 'Files', 181 'Folders', 182 'RemoteDocuments', 183 'RemoteFolders' 184 ]; 185 for (let table of tables) { 186 if (!await this._db.tableExists(table)) { 187 return false; 188 } 189 } 190 return true; 191 }; 192 193 // 194 // Collections 195 // 196 Zotero_Import_Mendeley.prototype._getFolders = async function (groupID) { 197 return this._db.queryAsync( 198 `SELECT F.*, RF.remoteUuid FROM Folders F ` 199 + `JOIN RemoteFolders RF ON (F.id=RF.folderId) ` 200 + `WHERE groupId=?`, 201 groupID 202 ); 203 }; 204 205 /** 206 * Get flat array of collection API JSON with parentCollection set 207 * 208 * The returned objects include an extra 'id' property for matching collections to documents. 209 */ 210 Zotero_Import_Mendeley.prototype._foldersToAPIJSON = function (folderRows, parentKey) { 211 var maxDepth = 50; 212 return this._getFolderDescendents(-1, parentKey, folderRows, maxDepth); 213 }; 214 215 Zotero_Import_Mendeley.prototype._getFolderDescendents = function (folderID, folderKey, folderRows, maxDepth) { 216 if (maxDepth == 0) return [] 217 var descendents = []; 218 var children = folderRows 219 .filter(f => f.parentId == folderID) 220 .map(f => { 221 let c = { 222 folderID: f.id, 223 remoteUUID: f.remoteUuid, 224 key: Zotero.DataObjectUtilities.generateKey(), 225 name: f.name, 226 parentCollection: folderKey 227 }; 228 if (f.remoteUuid) { 229 c.relations = { 230 'mendeleyDB:remoteFolderUUID': f.remoteUuid 231 }; 232 } 233 return c; 234 }); 235 236 for (let child of children) { 237 descendents.push( 238 child, 239 ...this._getFolderDescendents(child.folderID, child.key, folderRows, maxDepth - 1) 240 ); 241 } 242 return descendents; 243 }; 244 245 Zotero_Import_Mendeley.prototype._getFolderKeys = function (collections) { 246 var map = new Map(); 247 for (let collection of collections) { 248 map.set(collection.folderID, collection.key); 249 } 250 return map; 251 }; 252 253 /** 254 * @param {Integer} libraryID 255 * @param {Object[]} json 256 */ 257 Zotero_Import_Mendeley.prototype._saveCollections = async function (libraryID, json, folderKeys) { 258 var keyMap = new Map(); 259 for (let i = 0; i < json.length; i++) { 260 let collectionJSON = json[i]; 261 262 // Check if the collection was previously imported 263 let collection = this._findExistingCollection( 264 libraryID, 265 collectionJSON, 266 collectionJSON.parentCollection ? keyMap.get(collectionJSON.parentCollection) : null 267 ); 268 if (collection) { 269 // Update any child collections to point to the existing collection's key instead of 270 // the new generated one 271 this._updateParentKeys('collection', json, i + 1, collectionJSON.key, collection.key); 272 // And update the map of Mendeley folderIDs to Zotero collection keys 273 folderKeys.set(collectionJSON.folderID, collection.key); 274 } 275 else { 276 collection = new Zotero.Collection; 277 collection.libraryID = libraryID; 278 if (collectionJSON.key) { 279 collection.key = collectionJSON.key; 280 await collection.loadPrimaryData(); 281 } 282 } 283 284 // Remove external ids before saving 285 let toSave = Object.assign({}, collectionJSON); 286 delete toSave.folderID; 287 delete toSave.remoteUUID; 288 289 collection.fromJSON(toSave); 290 await collection.saveTx({ 291 skipSelect: true 292 }); 293 } 294 }; 295 296 297 Zotero_Import_Mendeley.prototype._findExistingCollection = function (libraryID, collectionJSON, parentCollection) { 298 // Don't use existing collections if the import is creating a top-level collection 299 if (this.createNewCollection || !collectionJSON.relations) { 300 return false; 301 } 302 303 var predicate = 'mendeleyDB:remoteFolderUUID'; 304 var uuid = collectionJSON.relations[predicate]; 305 306 var collections = Zotero.Relations.getByPredicateAndObject('collection', predicate, uuid) 307 .filter((c) => { 308 if (c.libraryID != libraryID) { 309 return false; 310 } 311 // If there's a parent collection it has to be the one we've already used 312 return parentCollection ? c.parentID == parentCollection.id : true; 313 }); 314 if (!collections.length) { 315 return false; 316 } 317 318 Zotero.debug(`Found existing collection ${collections[0].libraryKey} for ` 319 + `${predicate} ${collectionJSON.relations[predicate]}`); 320 return collections[0]; 321 } 322 323 324 // 325 // Items 326 // 327 Zotero_Import_Mendeley.prototype._getDocuments = async function (groupID) { 328 return this._db.queryAsync( 329 `SELECT D.*, RD.remoteUuid FROM Documents D ` 330 + `JOIN RemoteDocuments RD ON (D.id=RD.documentId) ` 331 + `WHERE groupId=? AND inTrash='false'`, 332 groupID 333 ); 334 }; 335 336 /** 337 * Get a Map of document ids to arrays of URLs 338 * 339 * @return {Map<Number,String[]>} 340 */ 341 Zotero_Import_Mendeley.prototype._getDocumentURLs = async function (groupID) { 342 var rows = await this._db.queryAsync( 343 `SELECT documentId, CAST(url AS TEXT) AS url FROM DocumentUrls DU ` 344 + `JOIN RemoteDocuments USING (documentId) ` 345 + `WHERE groupId=? ORDER BY position`, 346 groupID 347 ); 348 var map = new Map(); 349 for (let row of rows) { 350 let docURLs = map.get(row.documentId); 351 if (!docURLs) docURLs = []; 352 docURLs.push(row.url); 353 map.set(row.documentId, docURLs); 354 } 355 return map; 356 }; 357 358 /** 359 * Get a Map of document ids to arrays of creator API JSON 360 * 361 * @param {Integer} groupID 362 * @param {Object} creatorTypeMap - Mapping of Mendeley creator types to Zotero creator types 363 */ 364 Zotero_Import_Mendeley.prototype._getDocumentCreators = async function (groupID, creatorTypeMap) { 365 var rows = await this._db.queryAsync( 366 `SELECT * FROM DocumentContributors ` 367 + `JOIN RemoteDocuments USING (documentId) ` 368 + `WHERE groupId=?`, 369 groupID 370 ); 371 var map = new Map(); 372 for (let row of rows) { 373 let docCreators = map.get(row.documentId); 374 if (!docCreators) docCreators = []; 375 docCreators.push(this._makeCreator( 376 creatorTypeMap[row.contribution] || 'author', 377 row.firstNames, 378 row.lastName 379 )); 380 map.set(row.documentId, docCreators); 381 } 382 return map; 383 }; 384 385 /** 386 * Get a Map of document ids to arrays of tag API JSON 387 */ 388 Zotero_Import_Mendeley.prototype._getDocumentTags = async function (groupID) { 389 var rows = await this._db.queryAsync( 390 // Manual tags 391 `SELECT documentId, tag, 0 AS type FROM DocumentTags ` 392 + `JOIN RemoteDocuments USING (documentId) ` 393 + `WHERE groupId=? ` 394 + `UNION ` 395 // Automatic tags 396 + `SELECT documentId, keyword AS tag, 1 AS type FROM DocumentKeywords ` 397 + `JOIN RemoteDocuments USING (documentId) ` 398 + `WHERE groupId=?`, 399 [groupID, groupID] 400 ); 401 var map = new Map(); 402 for (let row of rows) { 403 let docTags = map.get(row.documentId); 404 if (!docTags) docTags = []; 405 docTags.push({ 406 tag: row.tag, 407 type: row.type 408 }); 409 map.set(row.documentId, docTags); 410 } 411 return map; 412 }; 413 414 /** 415 * Get a Map of document ids to arrays of collection keys 416 */ 417 Zotero_Import_Mendeley.prototype._getDocumentCollections = async function (groupID, documents, rootCollectionKey, folderKeys) { 418 var rows = await this._db.queryAsync( 419 `SELECT documentId, folderId FROM DocumentFolders DF ` 420 + `JOIN RemoteDocuments USING (documentId) ` 421 + `WHERE groupId=?`, 422 groupID 423 ); 424 var map = new Map( 425 // Add all documents to root collection if specified 426 documents.map(d => [d.id, rootCollectionKey ? [rootCollectionKey] : []]) 427 ); 428 for (let row of rows) { 429 let keys = map.get(row.documentId); 430 if (!keys) keys = []; 431 let key = folderKeys.get(row.folderId); 432 if (!key) { 433 Zotero.debug(`Document folder ${row.folderId} not found -- skipping`, 2); 434 continue; 435 } 436 keys.push(key); 437 map.set(row.documentId, keys); 438 } 439 return map; 440 }; 441 442 /** 443 * Get a Map of document ids to arrays of file metadata 444 * 445 * @return {Map<Number,Object[]>} 446 */ 447 Zotero_Import_Mendeley.prototype._getDocumentFiles = async function (groupID) { 448 var rows = await this._db.queryAsync( 449 `SELECT documentId, hash, localUrl FROM DocumentFiles ` 450 + `JOIN Files USING (hash) ` 451 + `JOIN RemoteDocuments USING (documentId) ` 452 + `WHERE groupId=?`, 453 groupID 454 ); 455 var map = new Map(); 456 for (let row of rows) { 457 let docFiles = map.get(row.documentId); 458 if (!docFiles) docFiles = []; 459 docFiles.push({ 460 hash: row.hash, 461 fileURL: row.localUrl 462 }); 463 map.set(row.documentId, docFiles); 464 } 465 return map; 466 }; 467 468 /** 469 * Get a Map of document ids to arrays of annotations 470 */ 471 Zotero_Import_Mendeley.prototype._getDocumentAnnotations = async function (groupID) { 472 var rows = await this._db.queryAsync( 473 `SELECT documentId, uuid, fileHash, page, note, color ` 474 + `FROM FileNotes ` 475 + `JOIN RemoteDocuments USING (documentId) ` 476 + `WHERE groupId=? ` 477 + `ORDER BY page, y, x`, 478 groupID 479 ); 480 var map = new Map(); 481 for (let row of rows) { 482 let docAnnotations = map.get(row.documentId); 483 if (!docAnnotations) docAnnotations = []; 484 docAnnotations.push({ 485 uuid: row.uuid, 486 hash: row.fileHash, 487 note: row.note, 488 page: row.page, 489 color: row.color 490 }); 491 map.set(row.documentId, docAnnotations); 492 } 493 return map; 494 }; 495 496 /** 497 * Create API JSON array with item and any child attachments or notes 498 */ 499 Zotero_Import_Mendeley.prototype._documentToAPIJSON = async function (map, documentRow, urls, creators, tags, collections, annotations) { 500 var parent = { 501 key: Zotero.DataObjectUtilities.generateKey() 502 }; 503 var children = []; 504 505 parent.itemType = map.itemTypes[documentRow.type]; 506 if (!parent.itemType) { 507 Zotero.warn(`Unmapped item type ${documentRow.type}`); 508 } 509 if (!parent.itemType || parent.itemType == 'document') { 510 parent.itemType = this._guessItemType(documentRow); 511 Zotero.debug(`Guessing type ${parent.itemType}`); 512 } 513 var itemTypeID = Zotero.ItemTypes.getID(parent.itemType); 514 515 for (let [mField, zField] of Object.entries(map.fields)) { 516 // If not mapped, skip 517 if (!zField) { 518 continue; 519 } 520 let val = documentRow[mField]; 521 // If no value, skip 522 if (!val) { 523 continue; 524 } 525 526 if (typeof zField == 'string') { 527 this._processField(parent, children, zField, val); 528 } 529 // Function embedded in map file 530 else if (typeof zField == 'function') { 531 let [field, val] = zField(documentRow[mField], parent); 532 this._processField(parent, children, field, val); 533 } 534 } 535 536 // URLs 537 if (urls) { 538 for (let i = 0; i < urls.length; i++) { 539 let url = urls[i]; 540 let isPDF = url.includes('pdf'); 541 if (i == 0 && !isPDF) { 542 parent.url = url; 543 } 544 else { 545 children.push({ 546 itemType: 'attachment', 547 parentItem: parent.key, 548 linkMode: 'linked_url', 549 url, 550 title: isPDF ? 'PDF' : '', 551 contentType: isPDF ? 'application/pdf' : '' 552 }); 553 } 554 } 555 } 556 557 // Combine date parts if present 558 if (documentRow.year) { 559 parent.date = documentRow.year.toString().substr(0, 4).padStart(4, '0'); 560 if (documentRow.month) { 561 parent.date += '-' + documentRow.month.toString().substr(0, 2).padStart(2, '0'); 562 if (documentRow.day) { 563 parent.date += '-' + documentRow.day.toString().substr(0, 2).padStart(2, '0'); 564 } 565 } 566 } 567 568 for (let field in parent) { 569 switch (field) { 570 case 'itemType': 571 case 'key': 572 case 'parentItem': 573 case 'note': 574 case 'creators': 575 case 'dateAdded': 576 case 'dateModified': 577 continue; 578 } 579 580 // Move unknown/invalid fields to Extra 581 let fieldID = Zotero.ItemFields.getID(field) 582 && Zotero.ItemFields.getFieldIDFromTypeAndBase(parent.itemType, field); 583 if (!fieldID) { 584 Zotero.warn(`Moving '${field}' to Extra for type ${parent.itemType}`); 585 parent.extra = this._addExtraField(parent.extra, field, parent[field]); 586 delete parent[field]; 587 continue; 588 } 589 let newField = Zotero.ItemFields.getName(fieldID); 590 if (field != newField) { 591 parent[newField] = parent[field]; 592 delete parent[field]; 593 } 594 } 595 596 if (!parent.dateModified) { 597 parent.dateModified = parent.dateAdded; 598 } 599 600 if (creators) { 601 // Add main creators before any added by fields (e.g., seriesEditor) 602 parent.creators = [...creators, ...(parent.creators || [])]; 603 604 // If item type has a different primary type, use that for author to prevent a warning 605 let primaryCreatorType = Zotero.CreatorTypes.getName( 606 Zotero.CreatorTypes.getPrimaryIDForType(itemTypeID) 607 ); 608 if (primaryCreatorType != 'author') { 609 for (let creator of parent.creators) { 610 if (creator.creatorType == 'author') { 611 creator.creatorType = primaryCreatorType; 612 } 613 } 614 } 615 616 for (let creator of parent.creators) { 617 // seriesEditor isn't valid on some item types (e.g., book) 618 if (creator.creatorType == 'seriesEditor' 619 && !Zotero.CreatorTypes.isValidForItemType( 620 Zotero.CreatorTypes.getID('seriesEditor'), itemTypeID)) { 621 creator.creatorType = 'editor'; 622 } 623 } 624 } 625 parent.tags = []; 626 // Add star tag for favorites 627 if (documentRow.favourite == 'true') { 628 parent.tags.push('\u2605'); 629 } 630 if (tags) { 631 parent.tags.push(...tags); 632 } 633 if (collections) parent.collections = collections; 634 635 // Copy date added/modified to child item 636 var parentDateAdded = parent.dateAdded; 637 var parentDateModified = parent.dateModified; 638 for (let child of children) { 639 child.dateAdded = parentDateAdded; 640 child.dateModified = parentDateModified; 641 } 642 643 // Don't set an explicit key if no children 644 if (!children.length) { 645 delete parent.key; 646 } 647 648 var documentUUID = documentRow.uuid.replace(/^\{/, '').replace(/\}$/, ''); 649 parent.relations = { 650 'mendeleyDB:documentUUID': documentUUID 651 }; 652 if (documentRow.remoteUuid) { 653 parent.relations['mendeleyDB:remoteDocumentUUID'] = documentRow.remoteUuid; 654 } 655 656 for (let child of children) { 657 // Add relation to child note 658 if (child.itemType == 'note') { 659 child.relations = { 660 'mendeleyDB:relatedDocumentUUID': documentUUID 661 }; 662 if (documentRow.remoteUuid) { 663 child.relations['mendeleyDB:relatedRemoteDocumentUUID'] = documentRow.remoteUuid; 664 } 665 break; 666 } 667 } 668 669 parent.documentID = documentRow.id; 670 671 var json = [parent, ...children]; 672 //Zotero.debug(json); 673 return json; 674 }; 675 676 /** 677 * Try to figure out item type based on available fields 678 */ 679 Zotero_Import_Mendeley.prototype._guessItemType = function (documentRow) { 680 if (documentRow.issn || documentRow.issue) { 681 return 'journalArticle'; 682 } 683 if (documentRow.isbn) { 684 return 'book'; 685 } 686 return 'document'; 687 }; 688 689 Zotero_Import_Mendeley.prototype._extractSubfield = function (field) { 690 var sub = field.match(/([a-z]+)\[([^\]]+)]/); 691 return sub ? { field: sub[1], subfield: sub[2] } : { field }; 692 }; 693 694 Zotero_Import_Mendeley.prototype._processField = function (parent, children, zField, val) { 695 var { field, subfield } = this._extractSubfield(zField); 696 if (subfield) { 697 // Combine 'city' and 'country' into 'place' 698 if (field == 'place') { 699 if (subfield == 'city') { 700 parent.place = val + (parent.place ? ', ' + parent.place : ''); 701 } 702 else if (subfield == 'country') { 703 parent.place = (parent.place ? ', ' + parent.place : '') + val; 704 } 705 } 706 // Convert some item fields as creators 707 else if (field == 'creator') { 708 if (!parent.creators) { 709 parent.creators = []; 710 } 711 parent.creators.push(this._makeCreator(subfield, null, val)); 712 } 713 else if (field == 'extra') { 714 parent.extra = this._addExtraField(parent.extra, subfield, val); 715 } 716 // Functions 717 else if (field == 'func') { 718 // Convert unix timestamps to ISO dates 719 if (subfield.startsWith('fromUnixtime')) { 720 let [, zField] = subfield.split(':'); 721 parent[zField] = Zotero.Date.dateToISO(new Date(val)); 722 } 723 // If 'pages' isn't valid for itemType, use 'numPages' instead 724 else if (subfield == 'pages') { 725 let itemTypeID = Zotero.ItemTypes.getID(parent.itemType); 726 if (!Zotero.ItemFields.isValidForType('pages', itemTypeID) 727 && Zotero.ItemFields.isValidForType('numPages', itemTypeID)) { 728 zField = 'numPages'; 729 } 730 else { 731 zField = 'pages'; 732 } 733 parent[zField] = val; 734 } 735 // Notes become child items 736 else if (subfield == 'note') { 737 children.push({ 738 parentItem: parent.key, 739 itemType: 'note', 740 note: this._convertNote(val) 741 }); 742 } 743 else { 744 Zotero.warn(`Unknown function subfield: ${subfield}`); 745 return; 746 } 747 } 748 else { 749 Zotero.warn(`Unknown field: ${field}[${subfield}]`); 750 } 751 } 752 else { 753 // These are added separately so that they're available for notes 754 if (zField == 'dateAdded' || zField == 'dateModified') { 755 return; 756 } 757 parent[zField] = val; 758 } 759 }; 760 761 Zotero_Import_Mendeley.prototype._makeCreator = function (creatorType, firstName, lastName) { 762 var creator = { creatorType }; 763 if (firstName) { 764 creator.firstName = firstName; 765 creator.lastName = lastName; 766 } 767 else { 768 creator.name = lastName; 769 } 770 return creator; 771 }; 772 773 Zotero_Import_Mendeley.prototype._addExtraField = function (extra, field, val) { 774 // Strip the field if it appears at the beginning of the value (to avoid "DOI: DOI: 10...") 775 if (typeof val == 'string') { 776 val = val.replace(new RegExp(`^${field}:\s*`, 'i'), ""); 777 } 778 extra = extra ? extra + '\n' : ''; 779 if (field != 'arXiv') { 780 field = field[0].toUpperCase() + field.substr(1); 781 field = field.replace(/([a-z])([A-Z][a-z])/, "$1 $2"); 782 } 783 return extra + `${field}: ${val}`; 784 }; 785 786 Zotero_Import_Mendeley.prototype._convertNote = function (note) { 787 return note 788 // Add newlines after <br> 789 .replace(/<br\s*\/>/g, '<br\/>\n') 790 // 791 // Legacy pre-HTML stuff 792 // 793 // <m:linebreak> 794 .replace(/<m:linebreak><\/m:linebreak>/g, '<br/>') 795 // <m:bold> 796 .replace(/<(\/)?m:bold>/g, '<$1b>') 797 // <m:italic> 798 .replace(/<(\/)?m:italic>/g, '<$1i>') 799 // <m:center> 800 .replace(/<m:center>/g, '<p style="text-align: center;">') 801 .replace(/<\/m:center>/g, '</p>') 802 // <m:underline> 803 .replace(/<m:underline>/g, '<span style="text-decoration: underline;">') 804 .replace(/<\/m:underline>/g, '</span>'); 805 }; 806 807 Zotero_Import_Mendeley.prototype._saveItems = async function (libraryID, json) { 808 var idMap = new Map(); 809 810 var lastExistingParentItem; 811 for (let i = 0; i < json.length; i++) { 812 let itemJSON = json[i]; 813 814 // Check if the item has been previously imported 815 let item = this._findExistingItem(libraryID, itemJSON, lastExistingParentItem); 816 if (item) { 817 if (item.isRegularItem()) { 818 lastExistingParentItem = item; 819 820 // Update any child items to point to the existing item's key instead of the 821 // new generated one 822 this._updateParentKeys('item', json, i + 1, itemJSON.key, item.key); 823 824 // Leave item in any collections it's in 825 itemJSON.collections = item.getCollections() 826 .map(id => Zotero.Collections.getLibraryAndKeyFromID(id).key) 827 .concat(itemJSON.collections || []); 828 } 829 } 830 else { 831 lastExistingParentItem = null; 832 833 item = new Zotero.Item; 834 item.libraryID = libraryID; 835 if (itemJSON.key) { 836 item.key = itemJSON.key; 837 await item.loadPrimaryData(); 838 } 839 } 840 841 // Remove external id before save 842 let toSave = Object.assign({}, itemJSON); 843 delete toSave.documentID; 844 845 item.fromJSON(toSave); 846 await item.saveTx({ 847 skipSelect: true, 848 skipDateModifiedUpdate: true 849 }); 850 if (itemJSON.documentID) { 851 idMap.set(itemJSON.documentID, item.id); 852 } 853 } 854 return idMap; 855 }; 856 857 858 Zotero_Import_Mendeley.prototype._findExistingItem = function (libraryID, itemJSON, existingParentItem) { 859 var predicate; 860 861 // 862 // Child item 863 // 864 if (existingParentItem) { 865 if (itemJSON.itemType == 'note') { 866 if (!itemJSON.relations) { 867 return false; 868 } 869 870 // Main note 871 let parentUUID = itemJSON.relations['mendeleyDB:relatedDocumentUUID']; 872 let parentRemoteUUID = itemJSON.relations['mendeleyDB:relatedRemoteDocumentUUID']; 873 if (parentUUID) { 874 let notes = existingParentItem.getNotes().map(id => Zotero.Items.get(id)); 875 for (let note of notes) { 876 predicate = 'mendeleyDB:relatedDocumentUUID'; 877 let rels = note.getRelationsByPredicate(predicate); 878 if (rels.length && rels[0] == parentUUID) { 879 Zotero.debug(`Found existing item ${note.libraryKey} for ` 880 + `${predicate} ${parentUUID}`); 881 return note; 882 } 883 if (parentRemoteUUID) { 884 predicate = 'mendeleyDB:relatedRemoteDocumentUUID'; 885 rels = note.getRelationsByPredicate(predicate); 886 if (rels.length && rels[0] == parentRemoteUUID) { 887 Zotero.debug(`Found existing item ${note.libraryKey} for ` 888 + `${predicate} ${parentRemoteUUID}`); 889 return note; 890 } 891 } 892 } 893 return false; 894 } 895 } 896 else if (itemJSON.itemType == 'attachment') { 897 // Linked-URL attachments (other attachments are handled in _saveFilesAndAnnotations()) 898 if (itemJSON.linkMode == 'linked_url') { 899 let attachments = existingParentItem.getAttachments().map(id => Zotero.Items.get(id)); 900 for (let attachment of attachments) { 901 if (attachment.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL 902 && attachment.getField('url') == itemJSON.url) { 903 Zotero.debug(`Found existing link attachment ${attachment.libraryKey}`); 904 return attachment; 905 } 906 } 907 } 908 } 909 910 return false; 911 } 912 913 // 914 // Parent item 915 // 916 if (!itemJSON.relations) { 917 return false; 918 } 919 var existingItem; 920 predicate = 'mendeleyDB:documentUUID'; 921 if (itemJSON.relations[predicate]) { 922 existingItem = this._getItemByRelation( 923 libraryID, 924 predicate, 925 itemJSON.relations[predicate] 926 ); 927 } 928 if (!existingItem) { 929 predicate = 'mendeleyDB:remoteDocumentUUID'; 930 if (itemJSON.relations[predicate]) { 931 existingItem = this._getItemByRelation( 932 libraryID, 933 predicate, 934 itemJSON.relations[predicate] 935 ); 936 } 937 } 938 // If not found or in trash 939 if (!existingItem) { 940 return false; 941 } 942 Zotero.debug(`Found existing item ${existingItem.libraryKey} for ` 943 + `${predicate} ${itemJSON.relations[predicate]}`); 944 return existingItem; 945 } 946 947 948 Zotero_Import_Mendeley.prototype._getItemByRelation = function (libraryID, predicate, object) { 949 var items = Zotero.Relations.getByPredicateAndObject('item', predicate, object) 950 .filter(item => item.libraryID == libraryID && !item.deleted); 951 if (!items.length) { 952 return false; 953 } 954 return items[0]; 955 }; 956 957 958 /** 959 * Saves attachments and extracted annotations for a given document 960 */ 961 Zotero_Import_Mendeley.prototype._saveFilesAndAnnotations = async function (files, libraryID, parentItemID, annotations) { 962 for (let file of files) { 963 try { 964 if (!file.fileURL) continue; 965 966 let path = OS.Path.fromFileURI(file.fileURL); 967 let realPath = await this._getRealFilePath(path); 968 969 let attachment; 970 if (realPath) { 971 if (this._findExistingFile(parentItemID, file)) { 972 continue; 973 } 974 975 let options = { 976 libraryID, 977 parentItemID, 978 file: realPath 979 }; 980 // If file is in Mendeley downloads folder, import it 981 if (this._isDownloadedFile(path)) { 982 if (file.url) { 983 options.title = file.title; 984 options.url = file.url; 985 options.contentType = file.contentType; 986 options.singleFile = true; 987 attachment = await Zotero.Attachments.importSnapshotFromFile(options); 988 } 989 else { 990 attachment = await Zotero.Attachments.importFromFile(options); 991 } 992 } 993 // Otherwise link it 994 else { 995 attachment = await Zotero.Attachments.linkFromFile(options); 996 } 997 attachment.setRelations({ 998 'mendeleyDB:fileHash': file.hash 999 }); 1000 await attachment.saveTx({ 1001 skipSelect: true 1002 }); 1003 } 1004 else { 1005 Zotero.warn(path + " not found -- not importing"); 1006 } 1007 1008 if (annotations) { 1009 await this._saveAnnotations( 1010 // We have annotations from all files for this document, so limit to just those on 1011 // this file 1012 annotations.filter(a => a.hash == file.hash), 1013 parentItemID, 1014 attachment ? attachment.id : null, 1015 file.hash 1016 ); 1017 } 1018 } 1019 catch (e) { 1020 Zotero.logError(e); 1021 } 1022 } 1023 } 1024 1025 1026 Zotero_Import_Mendeley.prototype._findExistingFile = function (parentItemID, file) { 1027 var item = Zotero.Items.get(parentItemID); 1028 var attachmentIDs = item.getAttachments(); 1029 for (let attachmentID of attachmentIDs) { 1030 let attachment = Zotero.Items.get(attachmentID); 1031 let predicate = 'mendeleyDB:fileHash'; 1032 let rels = attachment.getRelationsByPredicate(predicate); 1033 if (rels.includes(file.hash)) { 1034 Zotero.debug(`Found existing file ${attachment.libraryKey} for ` 1035 + `${predicate} ${file.hash}`); 1036 return attachment; 1037 } 1038 } 1039 return false; 1040 } 1041 1042 Zotero_Import_Mendeley.prototype._isDownloadedFile = function (path) { 1043 var parentDir = OS.Path.dirname(path); 1044 return parentDir.endsWith(OS.Path.join('Application Support', 'Mendeley Desktop', 'Downloaded')) 1045 || parentDir.endsWith(OS.Path.join('Local', 'Mendeley Ltd', 'Mendeley Desktop', 'Downloaded')) 1046 || parentDir.endsWith(OS.Path.join('data', 'Mendeley Ltd.', 'Mendeley Desktop', 'Downloaded')); 1047 } 1048 1049 /** 1050 * Get the path to use for a file that exists, or false if none 1051 * 1052 * This can be either the original path or, for a file in the Downloaded directory, in a directory 1053 * relative to the database. 1054 * 1055 * @return {String|false} 1056 */ 1057 Zotero_Import_Mendeley.prototype._getRealFilePath = async function (path) { 1058 if (await OS.File.exists(path)) { 1059 return path; 1060 } 1061 var isDownloadedFile = this._isDownloadedFile(path); 1062 if (!isDownloadedFile) { 1063 return false; 1064 } 1065 // For file paths in Downloaded folder, try relative to database if not found at the 1066 // absolute location, in case this is a DB backup 1067 var dataDir = OS.Path.dirname(this._file); 1068 var altPath = OS.Path.join(dataDir, 'Downloaded', OS.Path.basename(path)); 1069 if (altPath != path && await OS.File.exists(altPath)) { 1070 return altPath; 1071 } 1072 return false; 1073 } 1074 1075 Zotero_Import_Mendeley.prototype._saveAnnotations = async function (annotations, parentItemID, attachmentItemID, fileHash) { 1076 if (!annotations.length) return; 1077 var noteStrings = []; 1078 var parentItem = Zotero.Items.get(parentItemID); 1079 var libraryID = parentItem.libraryID; 1080 if (attachmentItemID) { 1081 var attachmentItem = Zotero.Items.get(attachmentItemID); 1082 var attachmentURIPath = Zotero.API.getLibraryPrefix(libraryID) + '/items/' + attachmentItem.key; 1083 } 1084 1085 for (let annotation of annotations) { 1086 if (!annotation.note || !annotation.note.trim()) continue; 1087 1088 let linkStr; 1089 let linkText = `note on p. ${annotation.page}`; 1090 if (attachmentItem) { 1091 let url = `zotero://open-pdf/${attachmentURIPath}?page=${annotation.page}`; 1092 linkStr = `<a href="${url}">${linkText}</a>`; 1093 } 1094 else { 1095 linkStr = linkText; 1096 } 1097 1098 noteStrings.push( 1099 Zotero.Utilities.text2html(annotation.note.trim()) 1100 + `<p class="pdf-link" style="margin-top: -0.5em; margin-bottom: 2em; font-size: .9em; text-align: right;">(${linkStr})</p>` 1101 ); 1102 } 1103 1104 if (!noteStrings.length) return; 1105 1106 // Look for an existing note 1107 var existingNotes = parentItem.getNotes().map(id => Zotero.Items.get(id)); 1108 var predicate = 'mendeleyDB:relatedFileHash'; 1109 var note; 1110 for (let n of existingNotes) { 1111 let rels = n.getRelationsByPredicate(predicate); 1112 if (rels.length && rels[0] == fileHash) { 1113 Zotero.debug(`Found existing note ${n.libraryKey} for ${predicate} ${fileHash}`); 1114 note = n; 1115 break; 1116 } 1117 } 1118 // If not found, create new one 1119 if (!note) { 1120 note = new Zotero.Item('note'); 1121 note.libraryID = libraryID; 1122 note.parentItemID = parentItemID; 1123 1124 // Add relation to associated file 1125 note.setRelations({ 1126 'mendeleyDB:relatedFileHash': fileHash 1127 }); 1128 } 1129 note.setNote('<h1>' + Zotero.getString('extractedAnnotations') + '</h1>\n' + noteStrings.join('\n')); 1130 return note.saveTx({ 1131 skipSelect: true 1132 }); 1133 }; 1134 1135 1136 Zotero_Import_Mendeley.prototype._updateParentKeys = function (objectType, json, i, oldKey, newKey) { 1137 var prop = 'parent' + objectType[0].toUpperCase() + objectType.substr(1); 1138 1139 for (; i < json.length; i++) { 1140 let x = json[i]; 1141 if (x[prop] == oldKey) { 1142 x[prop] = newKey; 1143 } 1144 // Child items are grouped together, so we can stop as soon as we stop seeing the prop 1145 else if (objectType == 'item') { 1146 break; 1147 } 1148 } 1149 } 1150 1151 Zotero_Import_Mendeley.prototype._updateItemCollectionKeys = function (json, oldKey, newKey) { 1152 for (; i < json.length; i++) { 1153 let x = json[i]; 1154 if (x[prop] == oldKey) { 1155 x[prop] = newKey; 1156 } 1157 } 1158 } 1159 1160 1161 // 1162 // Clean up extra files created <5.0.51 1163 // 1164 Zotero_Import_Mendeley.prototype.hasImportedFiles = async function () { 1165 return !!(await Zotero.DB.valueQueryAsync( 1166 "SELECT itemID FROM itemRelations JOIN relationPredicates USING (predicateID) " 1167 + "WHERE predicate='mendeleyDB:fileHash' LIMIT 1" 1168 )); 1169 }; 1170 1171 Zotero_Import_Mendeley.prototype.queueFileCleanup = async function () { 1172 await Zotero.DB.queryAsync("INSERT INTO settings VALUES ('mImport', 'cleanup', 1)"); 1173 }; 1174 1175 Zotero_Import_Mendeley.prototype.deleteNonPrimaryFiles = async function () { 1176 var rows = await Zotero.DB.queryAsync( 1177 "SELECT key, path FROM itemRelations " 1178 + "JOIN relationPredicates USING (predicateID) " 1179 + "JOIN items USING (itemID) " 1180 + "JOIN itemAttachments USING (itemID) " 1181 + "WHERE predicate='mendeleyDB:fileHash' AND linkMode=1" // imported_url 1182 ); 1183 for (let row of rows) { 1184 let dir = (Zotero.Attachments.getStorageDirectoryByLibraryAndKey(1, row.key)).path; 1185 if (!row.path.startsWith('storage:')) { 1186 Zotero.logError(row.path + " does not start with 'storage:'"); 1187 continue; 1188 } 1189 let filename = row.path.substr(8); 1190 1191 Zotero.debug(`Checking for extra files in ${dir}`); 1192 await Zotero.File.iterateDirectory(dir, function* (iterator) { 1193 while (true) { 1194 let entry = yield iterator.next(); 1195 if (entry.name.startsWith('.zotero') || entry.name == filename) { 1196 continue; 1197 } 1198 Zotero.debug(`Deleting ${entry.path}`); 1199 try { 1200 yield OS.File.remove(entry.path); 1201 } 1202 catch (e) { 1203 Zotero.logError(e); 1204 } 1205 } 1206 }); 1207 } 1208 1209 await Zotero.DB.queryAsync("DELETE FROM settings WHERE setting='mImport' AND key='cleanup'"); 1210 };