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