diff --git a/chrome/content/zotero/xpcom/data/feed.js b/chrome/content/zotero/xpcom/data/feed.js index c591c46b0..131b52d32 100644 --- a/chrome/content/zotero/xpcom/data/feed.js +++ b/chrome/content/zotero/xpcom/data/feed.js @@ -236,6 +236,8 @@ Zotero.Feed.prototype._initSave = Zotero.Promise.coroutine(function* (env) { if (!this._feedName) throw new Error("Feed name not set"); if (!this._feedUrl) throw new Error("Feed URL not set"); + if (!this.refreshInterval) this.refreshInterval = Zotero.Prefs.get('feeds.defaultTTL') * 60; + if (!this.cleanupAfter) this.cleanupAfter = Zotero.Prefs.get('feeds.defaultCleanupAfter'); if (env.isNew) { // Make sure URL is unique diff --git a/chrome/content/zotero/xpcom/data/feeds.js b/chrome/content/zotero/xpcom/data/feeds.js index b8f2028cc..763adaf3e 100644 --- a/chrome/content/zotero/xpcom/data/feeds.js +++ b/chrome/content/zotero/xpcom/data/feeds.js @@ -73,6 +73,44 @@ Zotero.Feeds = new function() { return this.scheduleNextFeedCheck(); } + this.importFromOPML = Zotero.Promise.coroutine(function* (opmlString) { + var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] + .createInstance(Components.interfaces.nsIDOMParser); + var doc = parser.parseFromString(opmlString, "application/xml"); + // Per some random spec (https://developer.mozilla.org/en-US/docs/Web/API/DOMParser), + // DOMParser returns a special type of xml document on error, so we do some magic checking here. + if (doc.documentElement.tagName == 'parseerror') { + return false; + } + var body = doc.getElementsByTagName('body')[0]; + var feedElems = doc.querySelectorAll('[type=rss][url], [xmlUrl]'); + var newFeeds = []; + var registeredUrls = new Set(); + for (let feedElem of feedElems) { + let url = feedElem.getAttribute('xmlUrl'); + if (!url) url = feedElem.getAttribute('url'); + let name = feedElem.getAttribute('title'); + if (!name) name = feedElem.getAttribute('text'); + if (Zotero.Feeds.existsByURL(url) || registeredUrls.has(url)) { + Zotero.debug("Feed Import from OPML: Feed " + name + " : " + url + " already exists. Skipping"); + continue; + } + // Prevent duplicates from the same OPML file + registeredUrls.add(url); + let feed = new Zotero.Feed({url, name}); + newFeeds.push(feed); + } + // This could potentially be a massive list, so we save in a transaction. + yield Zotero.DB.executeTransaction(function* () { + for (let feed of newFeeds) { + yield feed.save(); + } + }); + // Finally, update + yield Zotero.Feeds.updateFeeds(); + return true; + }); + this.restoreFromJSON = Zotero.Promise.coroutine(function* (json, merge=false) { Zotero.debug("Restoring feeds from remote JSON"); Zotero.debug(json); diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index e70b919ed..0111a7839 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -870,6 +870,27 @@ var ZoteroPane = new function() return collection.saveTx(); }); + this.importFeedsFromOPML = Zotero.Promise.coroutine(function* (event) { + var nsIFilePicker = Components.interfaces.nsIFilePicker; + while (true) { + var fp = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); + fp.init(window, Zotero.getString('fileInterface.importOPML'), nsIFilePicker.modeOpen); + fp.appendFilter(Zotero.getString('fileInterface.OPMLFeedFilter'), '*.opml; *.xml'); + fp.appendFilters(nsIFilePicker.filterAll); + if (fp.show() == nsIFilePicker.returnOK) { + var contents = yield Zotero.File.getContentsAsync(fp.file.path); + var success = yield Zotero.Feeds.importFromOPML(contents); + if (success) { + return true; + } + // Try again + Zotero.alert(window, Zotero.getString('general.error'), Zotero.getString('fileInterface.unsupportedFormat')); + } else { + return false; + } + } + }); + this.newFeedFromPage = Zotero.Promise.coroutine(function* (event) { let data = {unsaved: true}; if (event) { diff --git a/chrome/content/zotero/zoteroPane.xul b/chrome/content/zotero/zoteroPane.xul index 053686737..20b95a94b 100644 --- a/chrome/content/zotero/zoteroPane.xul +++ b/chrome/content/zotero/zoteroPane.xul @@ -117,6 +117,8 @@ + diff --git a/chrome/locale/en-US/zotero/zotero.dtd b/chrome/locale/en-US/zotero/zotero.dtd index 356d9b843..8340118e9 100644 --- a/chrome/locale/en-US/zotero/zotero.dtd +++ b/chrome/locale/en-US/zotero/zotero.dtd @@ -127,7 +127,8 @@ - + + diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties index d8308c868..bd6617782 100644 --- a/chrome/locale/en-US/zotero/zotero.properties +++ b/chrome/locale/en-US/zotero/zotero.properties @@ -637,6 +637,8 @@ fileInterface.importClipboardNoDataError = No importable data could be read from fileInterface.noReferencesError = The items you have selected contain no references. Please select one or more references and try again. fileInterface.bibliographyGenerationError = An error occurred generating your bibliography. Please try again. fileInterface.exportError = An error occurred while trying to export the selected file. +fileInterface.importOPML = Import Feeds from OPML +fileInterface.OPMLFeedFilter = OPML Feed List quickSearch.mode.titleCreatorYear = Title, Creator, Year quickSearch.mode.fieldsAndTags = All Fields & Tags diff --git a/test/tests/data/feeds.opml b/test/tests/data/feeds.opml new file mode 100644 index 000000000..922ba235f --- /dev/null +++ b/test/tests/data/feeds.opml @@ -0,0 +1,18 @@ + + + + + An OPML file with a list of rss/atom feeds + The OPML format is fairly poorly spec'ed out here http://dev.opml.org/spec2.html + + + + + + + + + + + + diff --git a/test/tests/feedsTest.js b/test/tests/feedsTest.js index a0243b571..da1a5f4fa 100644 --- a/test/tests/feedsTest.js +++ b/test/tests/feedsTest.js @@ -4,6 +4,39 @@ describe("Zotero.Feeds", function () { yield clearFeeds(); }); + describe('#importFromOPML', function() { + var opmlUrl = getTestDataUrl("feeds.opml"); + var opmlString; + + before(function* (){ + opmlString = yield Zotero.File.getContentsFromURLAsync(opmlUrl) + }); + + beforeEach(function* () { + yield clearFeeds(); + }); + + it('imports feeds correctly', function* (){ + let shouldExist = { + "http://example.com/feed1.rss": "A title 1", + "http://example.com/feed2.rss": "A title 2", + "http://example.com/feed3.rss": "A title 3", + "http://example.com/feed4.rss": "A title 4" + }; yield Zotero.Feeds.importFromOPML(opmlString); + let feeds = Zotero.Feeds.getAll(); + for (let feed of feeds) { + assert.equal(shouldExist[feed.url], feed.name, "Feed exists and title matches"); + delete shouldExist[feed.url]; + } + assert.equal(Object.keys(shouldExist).length, 0, "All feeds from opml have been created"); + }); + + it("doesn't fail if some feeds already exist", function* (){ + yield createFeed({url: "http://example.com/feed1.rss"}); + yield Zotero.Feeds.importFromOPML(opmlString) + }); + }); + describe("#restoreFromJSON", function() { var json = {}; var expiredFeedURL, existingFeedURL; diff --git a/translators b/translators index c834d8478..f0aa00219 160000 --- a/translators +++ b/translators @@ -1 +1 @@ -Subproject commit c834d847805f030afe3c38c4ad92f135faf01a4d +Subproject commit f0aa00219aeb75f04654a59339aa94e1accbc956