server_connector.js (44238B)
1 /* 2 ***** BEGIN LICENSE BLOCK ***** 3 4 Copyright © 2011 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 const CONNECTOR_API_VERSION = 2; 26 27 Zotero.Server.Connector = { 28 _waitingForSelection: {}, 29 30 getSaveTarget: function (allowReadOnly) { 31 var zp = Zotero.getActiveZoteroPane(); 32 var library = null; 33 var collection = null; 34 var editable = null; 35 36 if (zp && zp.collectionsView) { 37 if (zp.collectionsView.editable || allowReadOnly) { 38 library = Zotero.Libraries.get(zp.getSelectedLibraryID()); 39 collection = zp.getSelectedCollection(); 40 editable = zp.collectionsView.editable; 41 } 42 // If not editable, switch to My Library if it exists and is editable 43 else { 44 let userLibrary = Zotero.Libraries.userLibrary; 45 if (userLibrary && userLibrary.editable) { 46 Zotero.debug("Save target isn't editable -- switching to My Library"); 47 48 // Don't wait for this, because we don't want to slow down all conenctor 49 // requests by making this function async 50 zp.collectionsView.selectByID(userLibrary.treeViewID); 51 52 library = userLibrary; 53 collection = null; 54 editable = true; 55 } 56 } 57 } 58 else { 59 let id = Zotero.Prefs.get('lastViewedFolder'); 60 if (id) { 61 ({ library, collection, editable } = this.resolveTarget(id)); 62 if (!editable && !allowReadOnly) { 63 let userLibrary = Zotero.Libraries.userLibrary; 64 if (userLibrary && userLibrary.editable) { 65 Zotero.debug("Save target isn't editable -- switching to My Library"); 66 let treeViewID = userLibrary.treeViewID; 67 Zotero.Prefs.set('lastViewedFolder', treeViewID); 68 ({ library, collection, editable } = this.resolveTarget(treeViewID)); 69 } 70 } 71 } 72 } 73 74 // Default to My Library if present if pane not yet opened 75 // (which should never be the case anymore) 76 if (!library) { 77 let userLibrary = Zotero.Libraries.userLibrary; 78 if (userLibrary && userLibrary.editable) { 79 library = userLibrary; 80 } 81 } 82 83 return { library, collection, editable }; 84 }, 85 86 resolveTarget: function (targetID) { 87 var library; 88 var collection; 89 var editable; 90 91 var type = targetID[0]; 92 var id = parseInt(('' + targetID).substr(1)); 93 94 switch (type) { 95 case 'L': 96 library = Zotero.Libraries.get(id); 97 editable = library.editable; 98 break; 99 100 case 'C': 101 collection = Zotero.Collections.get(id); 102 library = collection.library; 103 editable = collection.editable; 104 break; 105 106 default: 107 throw new Error(`Unsupported target type '${type}'`); 108 } 109 110 return { library, collection, editable }; 111 } 112 }; 113 Zotero.Server.Connector.Data = {}; 114 115 Zotero.Server.Connector.SessionManager = { 116 _sessions: new Map(), 117 118 get: function (id) { 119 return this._sessions.get(id); 120 }, 121 122 create: function (id, action, requestData) { 123 // Legacy connector 124 if (!id) { 125 Zotero.debug("No session id provided by client", 2); 126 id = Zotero.Utilities.randomString(); 127 } 128 if (this._sessions.has(id)) { 129 throw new Error(`Session ID ${id} exists`); 130 } 131 Zotero.debug("Creating connector save session " + id); 132 var session = new Zotero.Server.Connector.SaveSession(id, action, requestData); 133 this._sessions.set(id, session); 134 this.gc(); 135 return session; 136 }, 137 138 gc: function () { 139 // Delete sessions older than 10 minutes, or older than 1 minute if more than 10 sessions 140 var ttl = this._sessions.size >= 10 ? 60 : 600; 141 var deleteBefore = new Date() - ttl * 1000; 142 143 for (let session of this._sessions) { 144 if (session.created < deleteBefore) { 145 this._session.delete(session.id); 146 } 147 } 148 } 149 }; 150 151 152 Zotero.Server.Connector.SaveSession = function (id, action, requestData) { 153 this.id = id; 154 this.created = new Date(); 155 this._action = action; 156 this._requestData = requestData; 157 this._items = new Set(); 158 }; 159 160 Zotero.Server.Connector.SaveSession.prototype.addItem = async function (item) { 161 return this.addItems([item]); 162 }; 163 164 Zotero.Server.Connector.SaveSession.prototype.addItems = async function (items) { 165 for (let item of items) { 166 this._items.add(item); 167 } 168 169 // Update the items with the current target data, in case it changed since the save began 170 await this._updateItems(items); 171 }; 172 173 /** 174 * Change the target data for this session and update any items that have already been saved 175 */ 176 Zotero.Server.Connector.SaveSession.prototype.update = async function (targetID, tags) { 177 var previousTargetID = this._currentTargetID; 178 this._currentTargetID = targetID; 179 this._currentTags = tags || ""; 180 181 // Select new destination in collections pane 182 var zp = Zotero.getActiveZoteroPane(); 183 if (zp && zp.collectionsView) { 184 await zp.collectionsView.selectByID(targetID); 185 } 186 // If window is closed, select target collection re-open 187 else { 188 Zotero.Prefs.set('lastViewedFolder', targetID); 189 } 190 191 // If moving from a non-filesEditable library to a filesEditable library, resave from 192 // original data, since there might be files that weren't saved or were removed 193 if (previousTargetID && previousTargetID != targetID) { 194 let { library: oldLibrary } = Zotero.Server.Connector.resolveTarget(previousTargetID); 195 let { library: newLibrary } = Zotero.Server.Connector.resolveTarget(targetID); 196 if (oldLibrary != newLibrary && !oldLibrary.filesEditable && newLibrary.filesEditable) { 197 Zotero.debug("Resaving items to filesEditable library"); 198 if (this._action == 'saveItems' || this._action == 'saveSnapshot') { 199 // Delete old items 200 for (let item of this._items) { 201 await item.eraseTx(); 202 } 203 let actionUC = Zotero.Utilities.capitalize(this._action); 204 let newItems = await Zotero.Server.Connector[actionUC].prototype[this._action]( 205 targetID, this._requestData 206 ); 207 // saveSnapshot only returns a single item 208 if (this._action == 'saveSnapshot') { 209 newItems = [newItems]; 210 } 211 this._items = new Set(newItems); 212 } 213 } 214 } 215 216 await this._updateItems(this._items); 217 218 // If a single item was saved, select it (or its parent, if it now has one) 219 if (zp && zp.collectionsView && this._items.size == 1) { 220 let item = Array.from(this._items)[0]; 221 item = item.isTopLevelItem() ? item : item.parentItem; 222 // Don't select if in trash 223 if (!item.deleted) { 224 await zp.selectItem(item.id); 225 } 226 } 227 }; 228 229 /** 230 * Update the passed items with the current target and tags 231 */ 232 Zotero.Server.Connector.SaveSession.prototype._updateItems = Zotero.serial(async function (items) { 233 if (items.length == 0) { 234 return; 235 } 236 237 var { library, collection, editable } = Zotero.Server.Connector.resolveTarget(this._currentTargetID); 238 var libraryID = library.libraryID; 239 240 var tags = this._currentTags.trim(); 241 tags = tags ? tags.split(/\s*,\s*/) : []; 242 243 Zotero.debug("Updating items for connector save session " + this.id); 244 245 for (let item of items) { 246 let newLibrary = Zotero.Libraries.get(library.libraryID); 247 248 if (item.libraryID != libraryID) { 249 let newItem = await item.moveToLibrary(libraryID); 250 // Replace item in session 251 this._items.delete(item); 252 this._items.add(newItem); 253 } 254 255 // If the item is now a child item (e.g., from Retrieve Metadata for PDF), update the 256 // parent item instead 257 if (!item.isTopLevelItem()) { 258 item = item.parentItem; 259 } 260 // Skip deleted items 261 if (!Zotero.Items.exists(item.id)) { 262 Zotero.debug(`Item ${item.id} in save session no longer exists`); 263 continue; 264 } 265 // Keep automatic tags 266 let originalTags = item.getTags().filter(tag => tag.type == 1); 267 item.setTags(originalTags.concat(tags)); 268 item.setCollections(collection ? [collection.id] : []); 269 await item.saveTx(); 270 } 271 272 this._updateRecents(); 273 }); 274 275 276 Zotero.Server.Connector.SaveSession.prototype._updateRecents = function () { 277 var targetID = this._currentTargetID; 278 try { 279 let numRecents = 7; 280 let recents = Zotero.Prefs.get('recentSaveTargets') || '[]'; 281 recents = JSON.parse(recents); 282 // If there's already a target from this session in the list, update it 283 for (let recent of recents) { 284 if (recent.sessionID == this.id) { 285 recent.id = targetID; 286 break; 287 } 288 } 289 // If a session is found with the same target, move it to the end without changing 290 // the sessionID. This could be the current session that we updated above or a different 291 // one. (We need to leave the old sessionID for the same target or we'll end up removing 292 // the previous target from the history if it's changed in the current one.) 293 let pos = recents.findIndex(r => r.id == targetID); 294 if (pos != -1) { 295 recents = [ 296 ...recents.slice(0, pos), 297 ...recents.slice(pos + 1), 298 recents[pos] 299 ]; 300 } 301 // Otherwise just add this one to the end 302 else { 303 recents = recents.concat([{ 304 id: targetID, 305 sessionID: this.id 306 }]); 307 } 308 recents = recents.slice(-1 * numRecents); 309 Zotero.Prefs.set('recentSaveTargets', JSON.stringify(recents)); 310 } 311 catch (e) { 312 Zotero.logError(e); 313 Zotero.Prefs.clear('recentSaveTargets'); 314 } 315 }; 316 317 318 Zotero.Server.Connector.AttachmentProgressManager = new function() { 319 var attachmentsInProgress = new WeakMap(), 320 attachmentProgress = {}, 321 id = 1; 322 323 /** 324 * Adds attachments to attachment progress manager 325 */ 326 this.add = function(attachments) { 327 for(var i=0; i<attachments.length; i++) { 328 var attachment = attachments[i]; 329 attachmentsInProgress.set(attachment, (attachment.id = id++)); 330 } 331 }; 332 333 /** 334 * Called on attachment progress 335 */ 336 this.onProgress = function(attachment, progress, error) { 337 attachmentProgress[attachmentsInProgress.get(attachment)] = progress; 338 }; 339 340 /** 341 * Gets progress for a given progressID 342 */ 343 this.getProgressForID = function(progressID) { 344 return progressID in attachmentProgress ? attachmentProgress[progressID] : 0; 345 }; 346 347 /** 348 * Check if we have received progress for a given attachment 349 */ 350 this.has = function(attachment) { 351 return attachmentsInProgress.has(attachment) 352 && attachmentsInProgress.get(attachment) in attachmentProgress; 353 } 354 }; 355 356 /** 357 * Lists all available translators, including code for translators that should be run on every page 358 * 359 * Accepts: 360 * Nothing 361 * Returns: 362 * Array of Zotero.Translator objects 363 */ 364 Zotero.Server.Connector.GetTranslators = function() {}; 365 Zotero.Server.Endpoints["/connector/getTranslators"] = Zotero.Server.Connector.GetTranslators; 366 Zotero.Server.Connector.GetTranslators.prototype = { 367 supportedMethods: ["POST"], 368 supportedDataTypes: ["application/json"], 369 permitBookmarklet: true, 370 371 /** 372 * Gets available translator list and other important data 373 * @param {Object} data POST data or GET query string 374 * @param {Function} sendResponseCallback function to send HTTP response 375 */ 376 init: function(data, sendResponseCallback) { 377 // Translator data 378 var me = this; 379 if(data.url) { 380 Zotero.Translators.getWebTranslatorsForLocation(data.url, data.rootUrl).then(function(data) { 381 sendResponseCallback(200, "application/json", 382 JSON.stringify(me._serializeTranslators(data[0]))); 383 }); 384 } else { 385 Zotero.Translators.getAll().then(function(translators) { 386 var responseData = me._serializeTranslators(translators); 387 sendResponseCallback(200, "application/json", JSON.stringify(responseData)); 388 }).catch(function(e) { 389 sendResponseCallback(500); 390 throw e; 391 }).done(); 392 } 393 }, 394 395 _serializeTranslators: function(translators) { 396 var responseData = []; 397 let properties = ["translatorID", "translatorType", "label", "creator", "target", "targetAll", 398 "minVersion", "maxVersion", "priority", "browserSupport", "inRepository", "lastUpdated"]; 399 for (var translator of translators) { 400 responseData.push(translator.serialize(properties)); 401 } 402 return responseData; 403 } 404 } 405 406 /** 407 * Detects whether there is an available translator to handle a given page 408 * 409 * Accepts: 410 * uri - The URI of the page to be saved 411 * html - document.innerHTML or equivalent 412 * cookie - document.cookie or equivalent 413 * 414 * Returns a list of available translators as an array 415 */ 416 Zotero.Server.Connector.Detect = function() {}; 417 Zotero.Server.Endpoints["/connector/detect"] = Zotero.Server.Connector.Detect; 418 Zotero.Server.Connector.Detect.prototype = { 419 supportedMethods: ["POST"], 420 supportedDataTypes: ["application/json"], 421 permitBookmarklet: true, 422 423 /** 424 * Loads HTML into a hidden browser and initiates translator detection 425 * @param {Object} data POST data or GET query string 426 * @param {Function} sendResponseCallback function to send HTTP response 427 */ 428 init: function(url, data, sendResponseCallback) { 429 this.sendResponse = sendResponseCallback; 430 this._parsedPostData = data; 431 432 this._translate = new Zotero.Translate("web"); 433 this._translate.setHandler("translators", function(obj, item) { me._translatorsAvailable(obj, item) }); 434 435 Zotero.Server.Connector.Data[this._parsedPostData["uri"]] = "<html>"+this._parsedPostData["html"]+"</html>"; 436 this._browser = Zotero.Browser.createHiddenBrowser(); 437 438 var ioService = Components.classes["@mozilla.org/network/io-service;1"] 439 .getService(Components.interfaces.nsIIOService); 440 var uri = ioService.newURI(this._parsedPostData["uri"], "UTF-8", null); 441 442 var pageShowCalled = false; 443 var me = this; 444 this._translate.setCookieSandbox(new Zotero.CookieSandbox(this._browser, 445 this._parsedPostData["uri"], this._parsedPostData["cookie"], url.userAgent)); 446 this._browser.addEventListener("DOMContentLoaded", function() { 447 try { 448 if(me._browser.contentDocument.location.href == "about:blank") return; 449 if(pageShowCalled) return; 450 pageShowCalled = true; 451 delete Zotero.Server.Connector.Data[me._parsedPostData["uri"]]; 452 453 // get translators 454 me._translate.setDocument(me._browser.contentDocument); 455 me._translate.setLocation(me._parsedPostData["uri"], me._parsedPostData["uri"]); 456 me._translate.getTranslators(); 457 } catch(e) { 458 sendResponseCallback(500); 459 throw e; 460 } 461 }, false); 462 463 me._browser.loadURI("zotero://connector/"+encodeURIComponent(this._parsedPostData["uri"])); 464 }, 465 466 /** 467 * Callback to be executed when list of translators becomes available. Sends standard 468 * translator passing properties with proxies where available for translators. 469 * @param {Zotero.Translate} translate 470 * @param {Zotero.Translator[]} translators 471 */ 472 _translatorsAvailable: function(translate, translators) { 473 translators = translators.map(function(translator) { 474 translator = translator.serialize(TRANSLATOR_PASSING_PROPERTIES.concat('proxy')); 475 translator.proxy = translator.proxy ? translator.proxy.toJSON() : null; 476 return translator; 477 }); 478 this.sendResponse(200, "application/json", JSON.stringify(translators)); 479 480 Zotero.Browser.deleteHiddenBrowser(this._browser); 481 } 482 } 483 484 /** 485 * Performs translation of a given page 486 * 487 * Accepts: 488 * uri - The URI of the page to be saved 489 * html - document.innerHTML or equivalent 490 * cookie - document.cookie or equivalent 491 * translatorID [optional] - a translator ID as returned by /connector/detect 492 * 493 * Returns: 494 * If a single item, sends response code 201 with item in body. 495 * If multiple items, sends response code 300 with the following content: 496 * items - list of items in the format typically passed to the selectItems handler 497 * instanceID - an ID that must be maintained for the subsequent Zotero.Connector.Select call 498 * uri - the URI of the page for which multiple items are available 499 */ 500 Zotero.Server.Connector.SavePage = function() {}; 501 Zotero.Server.Endpoints["/connector/savePage"] = Zotero.Server.Connector.SavePage; 502 Zotero.Server.Connector.SavePage.prototype = { 503 supportedMethods: ["POST"], 504 supportedDataTypes: ["application/json"], 505 permitBookmarklet: true, 506 507 /** 508 * Either loads HTML into a hidden browser and initiates translation, or saves items directly 509 * to the database 510 * @param {Object} data POST data or GET query string 511 * @param {Function} sendResponseCallback function to send HTTP response 512 */ 513 init: function(url, data, sendResponseCallback) { 514 var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget(); 515 516 // Shouldn't happen as long as My Library exists 517 if (!library.editable) { 518 Zotero.logError("Can't add item to read-only library " + library.name); 519 return sendResponseCallback(500, "application/json", JSON.stringify({ libraryEditable: false })); 520 } 521 522 this.sendResponse = sendResponseCallback; 523 Zotero.Server.Connector.Detect.prototype.init.apply(this, [url, data, sendResponseCallback]) 524 }, 525 526 /** 527 * Callback to be executed when items must be selected 528 * @param {Zotero.Translate} translate 529 * @param {Object} itemList ID=>text pairs representing available items 530 */ 531 _selectItems: function(translate, itemList, callback) { 532 var instanceID = Zotero.randomString(); 533 Zotero.Server.Connector._waitingForSelection[instanceID] = this; 534 535 // Fix for translators that don't create item lists as objects 536 if(itemList.push && typeof itemList.push === "function") { 537 var newItemList = {}; 538 for(var item in itemList) { 539 newItemList[item] = itemList[item]; 540 } 541 itemList = newItemList; 542 } 543 544 // Send "Multiple Choices" HTTP response 545 this.sendResponse(300, "application/json", JSON.stringify({selectItems: itemList, instanceID: instanceID, uri: this._parsedPostData.uri})); 546 this.selectedItemsCallback = callback; 547 }, 548 549 /** 550 * Callback to be executed when list of translators becomes available. Opens progress window, 551 * selects specified translator, and initiates translation. 552 * @param {Zotero.Translate} translate 553 * @param {Zotero.Translator[]} translators 554 */ 555 _translatorsAvailable: function(translate, translators) { 556 // make sure translatorsAvailable succeded 557 if(!translators.length) { 558 Zotero.Browser.deleteHiddenBrowser(this._browser); 559 this.sendResponse(500); 560 return; 561 } 562 563 var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget(); 564 var libraryID = library.libraryID; 565 566 // set handlers for translation 567 var me = this; 568 var jsonItems = []; 569 translate.setHandler("select", function(obj, item, callback) { return me._selectItems(obj, item, callback) }); 570 translate.setHandler("itemDone", function(obj, item, jsonItem) { 571 Zotero.Server.Connector.AttachmentProgressManager.add(jsonItem.attachments); 572 jsonItems.push(jsonItem); 573 }); 574 translate.setHandler("attachmentProgress", function(obj, attachment, progress, error) { 575 Zotero.Server.Connector.AttachmentProgressManager.onProgress(attachment, progress, error); 576 }); 577 translate.setHandler("done", function(obj, item) { 578 Zotero.Browser.deleteHiddenBrowser(me._browser); 579 if(jsonItems.length || me.selectedItems === false) { 580 me.sendResponse(201, "application/json", JSON.stringify({items: jsonItems})); 581 } else { 582 me.sendResponse(500); 583 } 584 }); 585 586 if (this._parsedPostData.translatorID) { 587 translate.setTranslator(this._parsedPostData.translatorID); 588 } else { 589 translate.setTranslator(translators[0]); 590 } 591 translate.translate({libraryID, collections: collection ? [collection.id] : false}); 592 } 593 } 594 595 /** 596 * Saves items to DB 597 * 598 * Accepts: 599 * items - an array of JSON format items 600 * Returns: 601 * 201 response code with item in body. 602 */ 603 Zotero.Server.Connector.SaveItems = function() {}; 604 Zotero.Server.Endpoints["/connector/saveItems"] = Zotero.Server.Connector.SaveItems; 605 Zotero.Server.Connector.SaveItems.prototype = { 606 supportedMethods: ["POST"], 607 supportedDataTypes: ["application/json"], 608 permitBookmarklet: true, 609 610 /** 611 * Either loads HTML into a hidden browser and initiates translation, or saves items directly 612 * to the database 613 */ 614 init: Zotero.Promise.coroutine(function* (requestData) { 615 var data = requestData.data; 616 617 var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget(); 618 var libraryID = library.libraryID; 619 var targetID = collection ? collection.treeViewID : library.treeViewID; 620 621 try { 622 var session = Zotero.Server.Connector.SessionManager.create( 623 data.sessionID, 624 'saveItems', 625 requestData 626 ); 627 } 628 catch (e) { 629 return [409, "application/json", JSON.stringify({ error: "SESSION_EXISTS" })]; 630 } 631 yield session.update(targetID); 632 633 // Shouldn't happen as long as My Library exists 634 if (!library.editable) { 635 Zotero.logError("Can't add item to read-only library " + library.name); 636 return [500, "application/json", JSON.stringify({ libraryEditable: false })]; 637 } 638 639 return new Zotero.Promise((resolve) => { 640 try { 641 this.saveItems( 642 targetID, 643 requestData, 644 function (topLevelItems) { 645 resolve([201, "application/json", JSON.stringify({items: topLevelItems})]); 646 } 647 ) 648 // Add items to session once all attachments have been saved 649 .then(function (items) { 650 session.addItems(items); 651 }); 652 } 653 catch (e) { 654 Zotero.logError(e); 655 resolve(500); 656 } 657 }); 658 }), 659 660 saveItems: async function (target, requestData, onTopLevelItemsDone) { 661 var { library, collection, editable } = Zotero.Server.Connector.resolveTarget(target); 662 663 var data = requestData.data; 664 var cookieSandbox = data.uri 665 ? new Zotero.CookieSandbox( 666 null, 667 data.uri, 668 data.detailedCookies ? "" : data.cookie || "", 669 requestData.headers["User-Agent"] 670 ) 671 : null; 672 if (cookieSandbox && data.detailedCookies) { 673 cookieSandbox.addCookiesFromHeader(data.detailedCookies); 674 } 675 676 for (let item of data.items) { 677 Zotero.Server.Connector.AttachmentProgressManager.add(item.attachments); 678 } 679 680 var proxy = data.proxy && new Zotero.Proxy(data.proxy); 681 682 // Save items 683 var itemSaver = new Zotero.Translate.ItemSaver({ 684 libraryID: library.libraryID, 685 collections: collection ? [collection.id] : undefined, 686 attachmentMode: Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD, 687 forceTagType: 1, 688 referrer: data.uri, 689 cookieSandbox, 690 proxy 691 }); 692 return itemSaver.saveItems( 693 data.items, 694 Zotero.Server.Connector.AttachmentProgressManager.onProgress, 695 function () { 696 // Remove attachments from item.attachments that aren't being saved. We have to 697 // clone the items so that we don't mutate the data stored in the session. 698 var savedItems = [...data.items.map(item => Object.assign({}, item))]; 699 for (let item of savedItems) { 700 item.attachments = item.attachments 701 .filter(attachment => { 702 return Zotero.Server.Connector.AttachmentProgressManager.has(attachment); 703 }); 704 } 705 if (onTopLevelItemsDone) { 706 onTopLevelItemsDone(savedItems); 707 } 708 } 709 ); 710 } 711 } 712 713 /** 714 * Saves a snapshot to the DB 715 * 716 * Accepts: 717 * uri - The URI of the page to be saved 718 * html - document.innerHTML or equivalent 719 * cookie - document.cookie or equivalent 720 * Returns: 721 * Nothing (200 OK response) 722 */ 723 Zotero.Server.Connector.SaveSnapshot = function() {}; 724 Zotero.Server.Endpoints["/connector/saveSnapshot"] = Zotero.Server.Connector.SaveSnapshot; 725 Zotero.Server.Connector.SaveSnapshot.prototype = { 726 supportedMethods: ["POST"], 727 supportedDataTypes: ["application/json"], 728 permitBookmarklet: true, 729 730 /** 731 * Save snapshot 732 */ 733 init: async function (requestData) { 734 var data = requestData.data; 735 736 var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget(); 737 var targetID = collection ? collection.treeViewID : library.treeViewID; 738 739 try { 740 var session = Zotero.Server.Connector.SessionManager.create( 741 data.sessionID, 742 'saveSnapshot', 743 requestData 744 ); 745 } 746 catch (e) { 747 return [409, "application/json", JSON.stringify({ error: "SESSION_EXISTS" })]; 748 } 749 await session.update(collection ? collection.treeViewID : library.treeViewID); 750 751 // Shouldn't happen as long as My Library exists 752 if (!library.editable) { 753 Zotero.logError("Can't add item to read-only library " + library.name); 754 return [500, "application/json", JSON.stringify({ libraryEditable: false })]; 755 } 756 757 try { 758 let item = await this.saveSnapshot(targetID, requestData); 759 await session.addItem(item); 760 } 761 catch (e) { 762 Zotero.logError(e); 763 return 500; 764 } 765 766 return 201; 767 }, 768 769 saveSnapshot: async function (target, requestData) { 770 var { library, collection, editable } = Zotero.Server.Connector.resolveTarget(target); 771 var libraryID = library.libraryID; 772 var data = requestData.data; 773 774 var cookieSandbox = data.url 775 ? new Zotero.CookieSandbox( 776 null, 777 data.url, 778 data.detailedCookies ? "" : data.cookie || "", 779 requestData.headers["User-Agent"] 780 ) 781 : null; 782 if (cookieSandbox && data.detailedCookies) { 783 cookieSandbox.addCookiesFromHeader(data.detailedCookies); 784 } 785 786 if (data.pdf && library.filesEditable) { 787 let item = await Zotero.Attachments.importFromURL({ 788 libraryID, 789 url: data.url, 790 collections: collection ? [collection.id] : undefined, 791 contentType: "application/pdf", 792 cookieSandbox 793 }); 794 795 // Automatically recognize PDF 796 Zotero.RecognizePDF.autoRecognizeItems([item]); 797 798 return item; 799 } 800 801 return new Zotero.Promise((resolve, reject) => { 802 Zotero.Server.Connector.Data[data.url] = "<html>" + data.html + "</html>"; 803 Zotero.HTTP.loadDocuments( 804 ["zotero://connector/" + encodeURIComponent(data.url)], 805 async function (doc) { 806 delete Zotero.Server.Connector.Data[data.url]; 807 808 try { 809 // Create new webpage item 810 let item = new Zotero.Item("webpage"); 811 item.libraryID = libraryID; 812 item.setField("title", doc.title); 813 item.setField("url", data.url); 814 item.setField("accessDate", "CURRENT_TIMESTAMP"); 815 if (collection) { 816 item.setCollections([collection.id]); 817 } 818 var itemID = await item.saveTx(); 819 820 // Save snapshot 821 if (library.filesEditable && !data.skipSnapshot) { 822 await Zotero.Attachments.importFromDocument({ 823 document: doc, 824 parentItemID: itemID 825 }); 826 } 827 828 resolve(item); 829 } 830 catch (e) { 831 reject(e); 832 } 833 }, 834 null, 835 null, 836 false, 837 cookieSandbox 838 ); 839 }); 840 } 841 } 842 843 /** 844 * Handle item selection 845 * 846 * Accepts: 847 * selectedItems - a list of items to translate in ID => text format as returned by a selectItems handler 848 * instanceID - as returned by savePage call 849 * Returns: 850 * 201 response code with empty body 851 */ 852 Zotero.Server.Connector.SelectItems = function() {}; 853 Zotero.Server.Endpoints["/connector/selectItems"] = Zotero.Server.Connector.SelectItems; 854 Zotero.Server.Connector.SelectItems.prototype = { 855 supportedMethods: ["POST"], 856 supportedDataTypes: ["application/json"], 857 permitBookmarklet: true, 858 859 /** 860 * Finishes up translation when item selection is complete 861 * @param {String} data POST data or GET query string 862 * @param {Function} sendResponseCallback function to send HTTP response 863 */ 864 init: function(data, sendResponseCallback) { 865 var saveInstance = Zotero.Server.Connector._waitingForSelection[data.instanceID]; 866 saveInstance.sendResponse = sendResponseCallback; 867 868 var selectedItems = false; 869 for(var i in data.selectedItems) { 870 selectedItems = data.selectedItems; 871 break; 872 } 873 saveInstance.selectedItemsCallback(selectedItems); 874 } 875 } 876 877 /** 878 * 879 * 880 * Accepts: 881 * sessionID - A session ID previously passed to /saveItems 882 * target - A treeViewID (L1, C23, etc.) for the library or collection to save to 883 * tags - A string of tags separated by commas 884 * 885 * Returns: 886 * 200 response on successful change 887 * 400 on error with 'error' property in JSON 888 */ 889 Zotero.Server.Connector.UpdateSession = function() {}; 890 Zotero.Server.Endpoints["/connector/updateSession"] = Zotero.Server.Connector.UpdateSession; 891 Zotero.Server.Connector.UpdateSession.prototype = { 892 supportedMethods: ["POST"], 893 supportedDataTypes: ["application/json"], 894 permitBookmarklet: true, 895 896 init: async function (requestData) { 897 var data = requestData.data 898 899 if (!data.sessionID) { 900 return [400, "application/json", JSON.stringify({ error: "SESSION_ID_NOT_PROVIDED" })]; 901 } 902 903 var session = Zotero.Server.Connector.SessionManager.get(data.sessionID); 904 if (!session) { 905 Zotero.debug("Can't find session " + data.sessionID, 1); 906 return [400, "application/json", JSON.stringify({ error: "SESSION_NOT_FOUND" })]; 907 } 908 909 // Parse treeViewID 910 var [type, id] = [data.target[0], parseInt(data.target.substr(1))]; 911 var tags = data.tags; 912 913 if (type == 'C') { 914 let collection = await Zotero.Collections.getAsync(id); 915 if (!collection) { 916 return [400, "application/json", JSON.stringify({ error: "COLLECTION_NOT_FOUND" })]; 917 } 918 } 919 920 await session.update(data.target, tags); 921 922 return [200, "application/json", JSON.stringify({})]; 923 } 924 }; 925 926 Zotero.Server.Connector.DelaySync = function () {}; 927 Zotero.Server.Endpoints["/connector/delaySync"] = Zotero.Server.Connector.DelaySync; 928 Zotero.Server.Connector.DelaySync.prototype = { 929 supportedMethods: ["POST"], 930 931 init: async function (requestData) { 932 Zotero.Sync.Runner.delaySync(10000); 933 return [204]; 934 } 935 }; 936 937 /** 938 * Gets progress for an attachment that is currently being saved 939 * 940 * Accepts: 941 * Array of attachment IDs returned by savePage, saveItems, or saveSnapshot 942 * Returns: 943 * 200 response code with current progress in body. Progress is either a number 944 * between 0 and 100 or "false" to indicate that saving failed. 945 */ 946 Zotero.Server.Connector.Progress = function() {}; 947 Zotero.Server.Endpoints["/connector/attachmentProgress"] = Zotero.Server.Connector.Progress; 948 Zotero.Server.Connector.Progress.prototype = { 949 supportedMethods: ["POST"], 950 supportedDataTypes: ["application/json"], 951 permitBookmarklet: true, 952 953 /** 954 * @param {String} data POST data or GET query string 955 * @param {Function} sendResponseCallback function to send HTTP response 956 */ 957 init: function(data, sendResponseCallback) { 958 sendResponseCallback(200, "application/json", 959 JSON.stringify(data.map(id => Zotero.Server.Connector.AttachmentProgressManager.getProgressForID(id)))); 960 } 961 }; 962 963 /** 964 * Translates resources using import translators 965 * 966 * Returns: 967 * - Object[Item] an array of imported items 968 */ 969 970 Zotero.Server.Connector.Import = function() {}; 971 Zotero.Server.Endpoints["/connector/import"] = Zotero.Server.Connector.Import; 972 Zotero.Server.Connector.Import.prototype = { 973 supportedMethods: ["POST"], 974 supportedDataTypes: '*', 975 permitBookmarklet: false, 976 977 init: async function (requestData) { 978 let translate = new Zotero.Translate.Import(); 979 translate.setString(requestData.data); 980 let translators = await translate.getTranslators(); 981 if (!translators || !translators.length) { 982 return 400; 983 } 984 translate.setTranslator(translators[0]); 985 var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget(); 986 var libraryID = library.libraryID; 987 988 // Shouldn't happen as long as My Library exists 989 if (!library.editable) { 990 Zotero.logError("Can't import into read-only library " + library.name); 991 return [500, "application/json", JSON.stringify({ libraryEditable: false })]; 992 } 993 994 try { 995 var session = Zotero.Server.Connector.SessionManager.create(requestData.query.session); 996 } 997 catch (e) { 998 return [409, "application/json", JSON.stringify({ error: "SESSION_EXISTS" })]; 999 } 1000 await session.update(collection ? collection.treeViewID : library.treeViewID); 1001 1002 let items = await translate.translate({ 1003 libraryID, 1004 collections: collection ? [collection.id] : null, 1005 forceTagType: 1, 1006 // Import translation skips selection by default, so force it to occur 1007 saveOptions: { 1008 skipSelect: false 1009 } 1010 }); 1011 session.addItems(items); 1012 1013 return [201, "application/json", JSON.stringify(items)]; 1014 } 1015 } 1016 1017 /** 1018 * Install CSL styles 1019 * 1020 * Returns: 1021 * - {name: styleName} 1022 */ 1023 1024 Zotero.Server.Connector.InstallStyle = function() {}; 1025 Zotero.Server.Endpoints["/connector/installStyle"] = Zotero.Server.Connector.InstallStyle; 1026 Zotero.Server.Connector.InstallStyle.prototype = { 1027 supportedMethods: ["POST"], 1028 supportedDataTypes: '*', 1029 permitBookmarklet: false, 1030 1031 init: Zotero.Promise.coroutine(function* (requestData) { 1032 try { 1033 var styleName = yield Zotero.Styles.install( 1034 requestData.data, requestData.query.origin || null, true 1035 ); 1036 } catch (e) { 1037 return [400, "text/plain", e.message]; 1038 } 1039 return [201, "application/json", JSON.stringify({name: styleName})]; 1040 }) 1041 }; 1042 1043 /** 1044 * Get code for a translator 1045 * 1046 * Accepts: 1047 * translatorID 1048 * Returns: 1049 * code - translator code 1050 */ 1051 Zotero.Server.Connector.GetTranslatorCode = function() {}; 1052 Zotero.Server.Endpoints["/connector/getTranslatorCode"] = Zotero.Server.Connector.GetTranslatorCode; 1053 Zotero.Server.Connector.GetTranslatorCode.prototype = { 1054 supportedMethods: ["POST"], 1055 supportedDataTypes: ["application/json"], 1056 permitBookmarklet: true, 1057 1058 /** 1059 * Returns a 200 response to say the server is alive 1060 * @param {String} data POST data or GET query string 1061 * @param {Function} sendResponseCallback function to send HTTP response 1062 */ 1063 init: function(postData, sendResponseCallback) { 1064 var translator = Zotero.Translators.get(postData.translatorID); 1065 translator.getCode().then(function(code) { 1066 sendResponseCallback(200, "application/javascript", code); 1067 }); 1068 } 1069 } 1070 1071 /** 1072 * Get selected collection 1073 * 1074 * Accepts: 1075 * Nothing 1076 * Returns: 1077 * libraryID 1078 * libraryName 1079 * collectionID 1080 * collectionName 1081 */ 1082 Zotero.Server.Connector.GetSelectedCollection = function() {}; 1083 Zotero.Server.Endpoints["/connector/getSelectedCollection"] = Zotero.Server.Connector.GetSelectedCollection; 1084 Zotero.Server.Connector.GetSelectedCollection.prototype = { 1085 supportedMethods: ["POST"], 1086 supportedDataTypes: ["application/json"], 1087 permitBookmarklet: true, 1088 1089 /** 1090 * Returns a 200 response to say the server is alive 1091 * @param {String} data POST data or GET query string 1092 * @param {Function} sendResponseCallback function to send HTTP response 1093 */ 1094 init: function(postData, sendResponseCallback) { 1095 var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget(true); 1096 var response = { 1097 libraryID: library.libraryID, 1098 libraryName: library.name, 1099 libraryEditable: library.editable, 1100 editable 1101 }; 1102 1103 if(collection && collection.id) { 1104 response.id = collection.id; 1105 response.name = collection.name; 1106 } else { 1107 response.id = null; 1108 response.name = response.libraryName; 1109 } 1110 1111 // Get list of editable libraries and collections 1112 var collections = []; 1113 var originalLibraryID = library.libraryID; 1114 for (let library of Zotero.Libraries.getAll()) { 1115 if (!library.editable) continue; 1116 1117 // Add recent: true for recent targets 1118 1119 collections.push( 1120 { 1121 id: library.treeViewID, 1122 name: library.name, 1123 level: 0 1124 }, 1125 ...Zotero.Collections.getByLibrary(library.libraryID, true).map(c => ({ 1126 id: c.treeViewID, 1127 name: c.name, 1128 level: c.level + 1 || 1 // Added by Zotero.Collections._getByContainer() 1129 })) 1130 ); 1131 } 1132 response.targets = collections; 1133 1134 // Mark recent targets 1135 try { 1136 let recents = Zotero.Prefs.get('recentSaveTargets'); 1137 if (recents) { 1138 recents = new Set(JSON.parse(recents).map(o => o.id)); 1139 for (let target of response.targets) { 1140 if (recents.has(target.id)) { 1141 target.recent = true; 1142 } 1143 } 1144 } 1145 } 1146 catch (e) { 1147 Zotero.logError(e); 1148 Zotero.Prefs.clear('recentSaveTargets'); 1149 } 1150 1151 sendResponseCallback( 1152 200, 1153 "application/json", 1154 JSON.stringify(response), 1155 { 1156 // Filter out collection names in debug output 1157 logFilter: function (str) { 1158 try { 1159 let json = JSON.parse(str.match(/^{"libraryID"[^]+/m)[0]); 1160 json.targets.forEach(t => t.name = "\u2026"); 1161 return JSON.stringify(json); 1162 } 1163 catch (e) { 1164 return str; 1165 } 1166 } 1167 } 1168 ); 1169 } 1170 } 1171 1172 /** 1173 * Get a list of client hostnames (reverse local IP DNS) 1174 * 1175 * Accepts: 1176 * Nothing 1177 * Returns: 1178 * {Array} hostnames 1179 */ 1180 Zotero.Server.Connector.GetClientHostnames = {}; 1181 Zotero.Server.Connector.GetClientHostnames = function() {}; 1182 Zotero.Server.Endpoints["/connector/getClientHostnames"] = Zotero.Server.Connector.GetClientHostnames; 1183 Zotero.Server.Connector.GetClientHostnames.prototype = { 1184 supportedMethods: ["POST"], 1185 supportedDataTypes: ["application/json"], 1186 permitBookmarklet: false, 1187 1188 /** 1189 * Returns a 200 response to say the server is alive 1190 */ 1191 init: Zotero.Promise.coroutine(function* (requestData) { 1192 try { 1193 var hostnames = yield Zotero.Proxies.DNS.getHostnames(); 1194 } catch(e) { 1195 return 500; 1196 } 1197 return [200, "application/json", JSON.stringify(hostnames)]; 1198 }) 1199 }; 1200 1201 /** 1202 * Get a list of stored proxies 1203 * 1204 * Accepts: 1205 * Nothing 1206 * Returns: 1207 * {Array} hostnames 1208 */ 1209 Zotero.Server.Connector.Proxies = {}; 1210 Zotero.Server.Connector.Proxies = function() {}; 1211 Zotero.Server.Endpoints["/connector/proxies"] = Zotero.Server.Connector.Proxies; 1212 Zotero.Server.Connector.Proxies.prototype = { 1213 supportedMethods: ["POST"], 1214 supportedDataTypes: ["application/json"], 1215 permitBookmarklet: false, 1216 1217 /** 1218 * Returns a 200 response to say the server is alive 1219 */ 1220 init: Zotero.Promise.coroutine(function* () { 1221 let proxies = Zotero.Proxies.proxies.map((p) => Object.assign(p.toJSON(), {hosts: p.hosts})); 1222 return [200, "application/json", JSON.stringify(proxies)]; 1223 }) 1224 }; 1225 1226 1227 /** 1228 * Test connection 1229 * 1230 * Accepts: 1231 * Nothing 1232 * Returns: 1233 * Nothing (200 OK response) 1234 */ 1235 Zotero.Server.Connector.Ping = function() {}; 1236 Zotero.Server.Endpoints["/connector/ping"] = Zotero.Server.Connector.Ping; 1237 Zotero.Server.Connector.Ping.prototype = { 1238 supportedMethods: ["GET", "POST"], 1239 supportedDataTypes: ["application/json", "text/plain"], 1240 permitBookmarklet: true, 1241 1242 /** 1243 * Sends 200 and HTML status on GET requests 1244 * @param data {Object} request information defined in connector.js 1245 */ 1246 init: function (req) { 1247 if (req.method == 'GET') { 1248 return [200, "text/html", '<!DOCTYPE html><html><head>' + 1249 '<title>Zotero Connector Server is Available</title></head>' + 1250 '<body>Zotero Connector Server is Available</body></html>']; 1251 } else { 1252 // Store the active URL so it can be used for site-specific Quick Copy 1253 if (req.data.activeURL) { 1254 //Zotero.debug("Setting active URL to " + req.data.activeURL); 1255 Zotero.QuickCopy.lastActiveURL = req.data.activeURL; 1256 } 1257 1258 let response = { 1259 prefs: { 1260 automaticSnapshots: Zotero.Prefs.get('automaticSnapshots') 1261 } 1262 }; 1263 if (Zotero.QuickCopy.hasSiteSettings()) { 1264 response.prefs.reportActiveURL = true; 1265 } 1266 1267 this.versionWarning(req); 1268 1269 return [200, 'application/json', JSON.stringify(response)]; 1270 } 1271 }, 1272 1273 1274 /** 1275 * Warn on outdated connector version 1276 * 1277 * We can remove this once the connector checks and warns on its own and most people are on 1278 * a version that does that. 1279 */ 1280 versionWarning: function (req) { 1281 try { 1282 if (!Zotero.Prefs.get('showConnectorVersionWarning')) return; 1283 if (!req.headers) return; 1284 1285 var minVersion = ZOTERO_CONFIG.CONNECTOR_MIN_VERSION; 1286 var appName = ZOTERO_CONFIG.CLIENT_NAME; 1287 var domain = ZOTERO_CONFIG.DOMAIN_NAME; 1288 var origin = req.headers.Origin; 1289 1290 var browser; 1291 var message; 1292 var showDownloadButton = false; 1293 if (origin && origin.startsWith('safari-extension')) { 1294 browser = 'safari'; 1295 message = `An update is available for the ${appName} Connector for Safari.\n\n` 1296 + 'You can upgrade from the Extensions pane of the Safari preferences.'; 1297 } 1298 else if (origin && origin.startsWith('chrome-extension')) { 1299 browser = 'chrome'; 1300 message = `An update is available for the ${appName} Connector for Chrome.\n\n` 1301 + `You can upgrade to the latest version from ${domain}.`; 1302 showDownloadButton = true; 1303 } 1304 else if (req.headers['User-Agent'] && req.headers['User-Agent'].includes('Firefox/')) { 1305 browser = 'firefox'; 1306 message = `An update is available for the ${appName} Connector for Firefox.\n\n` 1307 + `You can upgrade to the latest version from ${domain}.`; 1308 showDownloadButton = true; 1309 } 1310 else { 1311 Zotero.debug("Unknown browser"); 1312 return; 1313 } 1314 1315 if (Zotero.Server.Connector['skipVersionWarning-' + browser]) return; 1316 1317 var version = req.headers['X-Zotero-Version']; 1318 if (!version || version == '4.999.0') return; 1319 1320 // If connector is up to date, bail 1321 if (Services.vc.compare(version, minVersion) >= 0) return; 1322 1323 var showNextPref = `nextConnectorVersionWarning.${browser}`; 1324 var showNext = Zotero.Prefs.get(showNextPref); 1325 if (showNext && new Date() < new Date(showNext * 1000)) return; 1326 1327 // Don't show again for this browser until restart 1328 Zotero.Server.Connector['skipVersionWarning-' + browser] = true; 1329 var ps = Services.prompt; 1330 var buttonFlags; 1331 if (showDownloadButton) { 1332 buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING 1333 + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING; 1334 } 1335 else { 1336 buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_OK; 1337 } 1338 setTimeout(function () { 1339 var dontShow = {}; 1340 var index = ps.confirmEx(null, 1341 Zotero.getString('general.updateAvailable'), 1342 message, 1343 buttonFlags, 1344 showDownloadButton ? Zotero.getString('general.upgrade') : null, 1345 showDownloadButton ? Zotero.getString('general.notNow') : null, 1346 null, 1347 "Don\u0027t show again for a month", 1348 dontShow 1349 ); 1350 1351 var nextShowDays; 1352 if (dontShow.value) { 1353 nextShowDays = 30; 1354 } 1355 // Don't show again for at least a day, even after a restart 1356 else { 1357 nextShowDays = 1; 1358 } 1359 Zotero.Prefs.set(showNextPref, Math.round(Date.now() / 1000) + 86400 * nextShowDays); 1360 1361 if (showDownloadButton && index == 0) { 1362 Zotero.launchURL(ZOTERO_CONFIG.CONNECTORS_URL); 1363 } 1364 }, 500); 1365 } 1366 catch (e) { 1367 Zotero.debug(e, 2); 1368 } 1369 } 1370 } 1371 1372 /** 1373 * IE messaging hack 1374 * 1375 * Accepts: 1376 * Nothing 1377 * Returns: 1378 * Static Response 1379 */ 1380 Zotero.Server.Connector.IEHack = function() {}; 1381 Zotero.Server.Endpoints["/connector/ieHack"] = Zotero.Server.Connector.IEHack; 1382 Zotero.Server.Connector.IEHack.prototype = { 1383 supportedMethods: ["GET"], 1384 permitBookmarklet: true, 1385 1386 /** 1387 * Sends a fixed webpage 1388 * @param {String} data POST data or GET query string 1389 * @param {Function} sendResponseCallback function to send HTTP response 1390 */ 1391 init: function(postData, sendResponseCallback) { 1392 sendResponseCallback(200, "text/html", 1393 '<!DOCTYPE html><html><head>'+ 1394 '<script src="'+ZOTERO_CONFIG.BOOKMARKLET_URL+'common_ie.js"></script>'+ 1395 '<script src="'+ZOTERO_CONFIG.BOOKMARKLET_URL+'ie_hack.js"></script>'+ 1396 '</head><body></body></html>'); 1397 } 1398 } 1399 1400 // XXX For compatibility with older connectors; to be removed 1401 Zotero.Server.Connector.IncompatibleVersion = function() {}; 1402 Zotero.Server.Connector.IncompatibleVersion._errorShown = false 1403 Zotero.Server.Endpoints["/translate/list"] = Zotero.Server.Connector.IncompatibleVersion; 1404 Zotero.Server.Endpoints["/translate/detect"] = Zotero.Server.Connector.IncompatibleVersion; 1405 Zotero.Server.Endpoints["/translate/save"] = Zotero.Server.Connector.IncompatibleVersion; 1406 Zotero.Server.Endpoints["/translate/select"] = Zotero.Server.Connector.IncompatibleVersion; 1407 Zotero.Server.Connector.IncompatibleVersion.prototype = { 1408 supportedMethods: ["POST"], 1409 supportedDataTypes: ["application/json"], 1410 permitBookmarklet: true, 1411 1412 init: function(postData, sendResponseCallback) { 1413 sendResponseCallback(404); 1414 if(Zotero.Server.Connector.IncompatibleVersion._errorShown) return; 1415 1416 Zotero.Utilities.Internal.activate(); 1417 var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]. 1418 createInstance(Components.interfaces.nsIPromptService); 1419 ps.alert(null, 1420 Zotero.getString("connector.error.title"), 1421 Zotero.getString("integration.error.incompatibleVersion2", 1422 ["Standalone "+Zotero.version, "Connector", "2.999.1"])); 1423 Zotero.Server.Connector.IncompatibleVersion._errorShown = true; 1424 } 1425 };