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:
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;