www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit 186561f3202e31cb22aabefe85363a7667732653
parent f52e93dd165ba8a7970df6622859164b6a438043
Author: Dan Stillman <dstillman@zotero.org>
Date:   Thu, 28 Apr 2016 01:09:47 -0400

Merge pull request #950 from adomasven/feature/import-feeds-from-opml

Close adomasven/zotero#11. Add support for feed imports from OPML files
Diffstat:
Mchrome/content/zotero/xpcom/data/feed.js | 2++
Mchrome/content/zotero/xpcom/data/feeds.js | 38++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/zoteroPane.js | 21+++++++++++++++++++++
Mchrome/content/zotero/zoteroPane.xul | 2++
Mchrome/locale/en-US/zotero/zotero.dtd | 3++-
Mchrome/locale/en-US/zotero/zotero.properties | 2++
Atest/tests/data/feeds.opml | 18++++++++++++++++++
Mtest/tests/feedsTest.js | 33+++++++++++++++++++++++++++++++++
8 files changed, 118 insertions(+), 1 deletion(-)

diff --git 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 @@ -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 @@ -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 @@ -117,6 +117,8 @@ <menupopup oncommand="ZoteroPane_Local.newFeedFromPage(event)" onpopupshowing="FeedHandler.buildFeedList(event.target)"/> </menu> + <menuitem id="zotero-tb-feed-add-fromOPML" label="&zotero.toolbar.feeds.new.fromOPML;" + oncommand="ZoteroPane_Local.importFeedsFromOPML()"/> </menupopup> </menu> </menupopup> diff --git a/chrome/locale/en-US/zotero/zotero.dtd b/chrome/locale/en-US/zotero/zotero.dtd @@ -127,7 +127,8 @@ <!ENTITY zotero.toolbar.feeds.new "New Feed…"> <!ENTITY zotero.toolbar.feeds.new.fromURL "From URL…"> -<!ENTITY zotero.toolbar.feeds.new.fromPage "From Page…"> +<!ENTITY zotero.toolbar.feeds.new.fromPage "From Page…"> +<!ENTITY zotero.toolbar.feeds.new.fromOPML "From OPML…"> <!ENTITY zotero.toolbar.feeds.refresh "Refresh Feed"> <!ENTITY zotero.toolbar.feeds.edit "Edit Feed…"> diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties @@ -638,6 +638,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 @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<opml version="1.0"> + <head> + <title>An OPML file with a list of rss/atom feeds</title> + <docs>The OPML format is fairly poorly spec'ed out here http://dev.opml.org/spec2.html</docs> + </head> + <body> + <outline text="Standard format"> + <outline type="rss" text="A title 1" title="A title 1" xmlUrl="http://example.com/feed1.rss"/> + <outline type="rss" text="A title 2" title="A title 2" xmlUrl="http://example.com/feed2.rss"/> + </outline> + <outline text="Non-standard format"> + <outline type="rss" text="A title 3" title="A title 3" url="http://example.com/feed3.rss"/> + <outline type="rss" text="A title 4" title="A title 4" url="http://example.com/feed4.rss"/> + </outline> + </body> +</opml> diff --git 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;