/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ Components.utils.import("resource:///modules/FeedUtils.jsm"); Components.utils.import("resource:///modules/gloda/log4moz.js"); Components.utils.import("resource:///modules/mailServices.js"); Components.utils.import("resource://gre/modules/FileUtils.jsm"); Components.utils.import("resource://gre/modules/PluralForm.jsm"); Components.utils.import("resource://gre/modules/Services.jsm"); var {classes: Cc, interfaces: Ci} = Components; var FeedSubscriptions = { get mMainWin() { return Services.wm.getMostRecentWindow("mail:3pane"); }, get mTree() { return document.getElementById("rssSubscriptionsList"); }, mFeedContainers: [], mRSSServer : null, mActionMode : null, kSubscribeMode : 1, kUpdateMode : 2, kMoveMode : 3, kCopyMode : 4, kImportingOPML : 5, kVerifyUrlMode : 6, get FOLDER_ACTIONS() { return Ci.nsIMsgFolderNotificationService.folderAdded | Ci.nsIMsgFolderNotificationService.folderDeleted | Ci.nsIMsgFolderNotificationService.folderRenamed | Ci.nsIMsgFolderNotificationService.folderMoveCopyCompleted; }, onLoad: function () { // Extract the folder argument. let folder; if (window.arguments && window.arguments[0].folder) folder = window.arguments[0].folder; // Ensure dialog is fully loaded before selecting, to get visible row. setTimeout(function() { FeedSubscriptions.refreshSubscriptionView(folder) }, 100); let message = FeedUtils.strings.GetStringFromName("subscribe-loading"); this.updateStatusItem("statusText", message); FeedUtils.CANCEL_REQUESTED = false; if (this.mMainWin) { this.mMainWin.FeedFolderNotificationService = MailServices.mfn; this.mMainWin.FeedFolderNotificationService .addListener(this.FolderListener, this.FOLDER_ACTIONS); } }, onClose: function () { let dismissDialog = true; // If we are in the middle of subscribing to a feed, inform the user that // dismissing the dialog right now will abort the feed subscription. if (this.mActionMode == this.kSubscribeMode) { let pTitle = FeedUtils.strings.GetStringFromName( "subscribe-cancelSubscriptionTitle"); let pMessage = FeedUtils.strings.GetStringFromName( "subscribe-cancelSubscription"); dismissDialog = !(Services.prompt.confirmEx(window, pTitle, pMessage, Ci.nsIPromptService.STD_YES_NO_BUTTONS, null, null, null, null, { })); } if (dismissDialog) { FeedUtils.CANCEL_REQUESTED = this.mActionMode == this.kSubscribeMode; if (this.mMainWin) { this.mMainWin.FeedFolderNotificationService .removeListener(this.FolderListener, this.FOLDER_ACTIONS); delete this.mMainWin.FeedFolderNotificationService; } } return dismissDialog; }, refreshSubscriptionView: function(aSelectFolder, aSelectFeedUrl) { let item = this.mView.currentItem; this.loadSubscriptions(); this.mTree.view = this.mView; if (aSelectFolder && !aSelectFeedUrl) this.selectFolder(aSelectFolder); else { // If no folder to select, try to select the pre rebuild selection, in // an existing window. For folderpane changes in a feed account. if (item) { let rootFolder = item.container ? item.folder.rootFolder : item.parentFolder.rootFolder; if (item.container) { if (!this.selectFolder(item.folder, { open: item.open })) // The item no longer exists, an ancestor folder was deleted or // renamed/moved. this.selectFolder(rootFolder); } else { let url = item.parentFolder == aSelectFolder ? aSelectFeedUrl : item.url; this.selectFeed({ folder: rootFolder, url: url }, null); } } } this.mView.treeBox.ensureRowIsVisible(this.mView.selection.currentIndex); this.clearStatusInfo(); }, mView: { kRowIndexUndefined: -1, get currentItem() { // Get the current selection, if any. let seln = this.selection; let currentSelectionIndex = seln ? seln.currentIndex : null; let item; if (currentSelectionIndex != null) item = this.getItemAtIndex(currentSelectionIndex); return item; }, /* nsITreeView */ treeBox: null, mRowCount: 0, get rowCount() { return this.mRowCount; }, _selection: null, get selection () { return this._selection; }, set selection (val) { return this._selection = val; }, setTree: function(aTreebox) { this.treeBox = aTreebox; }, isSeparator: function(aRow) { return false; }, isSorted: function() { return false; }, isSelectable: function(aRow, aColumn) { return false; }, isEditable: function (aRow, aColumn) { return false; }, getProgressMode : function(aRow, aCol) {}, cycleHeader: function(aCol) {}, cycleCell: function(aRow, aCol) {}, selectionChanged: function() {}, performAction: function(aAction) {}, performActionOnRow: function (aAction, aRow) {}, performActionOnCell: function(aAction, aRow, aCol) {}, getRowProperties: function(aRow) { return ""; }, getColumnProperties: function(aCol) { return ""; }, getCellValue: function (aRow, aColumn) {}, setCellValue: function (aRow, aColumn, aValue) {}, setCellText: function (aRow, aColumn, aValue) {}, getCellProperties: function (aRow, aColumn) { let item = this.getItemAtIndex(aRow); let folder = item && item.folder ? item.folder : null; #ifdef MOZ_THUNDERBIRD let properties = ["folderNameCol"]; let hasFeeds = folder ? FeedUtils.getFeedUrlsInFolder(folder) : false; let prop = !folder ? "isFeed-true" : hasFeeds ? "isFeedFolder-true" : folder.isServer ? "serverType-rss isServer-true" : null; if (prop) properties.push(prop); return properties.join(" "); #else return !folder ? "serverType-rss" : folder.isServer ? "serverType-rss isServer-true" : "livemark"; #endif }, isContainer: function (aRow) { let item = this.getItemAtIndex(aRow); return item ? item.container : false; }, isContainerOpen: function (aRow) { let item = this.getItemAtIndex(aRow); return item ? item.open : false; }, isContainerEmpty: function (aRow) { let item = this.getItemAtIndex(aRow); if (!item) return false; return item.children.length == 0; }, getItemAtIndex: function (aRow) { if (aRow < 0 || aRow >= FeedSubscriptions.mFeedContainers.length) return null; return FeedSubscriptions.mFeedContainers[aRow]; }, getItemInViewIndex: function(aFolder) { if (!aFolder || !(aFolder instanceof Ci.nsIMsgFolder)) return null; for (let index = 0; index < this.rowCount; index++) { // Find the visible folder in the view. let item = this.getItemAtIndex(index); if (item && item.container && item.url == aFolder.URI) return index; } return null; }, removeItemAtIndex: function (aRow, aNoSelect) { let itemToRemove = this.getItemAtIndex(aRow); if (!itemToRemove) return; if (itemToRemove.container && itemToRemove.open) // Close it, if open container. this.toggleOpenState(aRow); let parentIndex = this.getParentIndex(aRow); let hasNextSibling = this.hasNextSibling(aRow, aRow); if (parentIndex != this.kRowIndexUndefined) { let parent = this.getItemAtIndex(parentIndex); if (parent) { for (let index = 0; index < parent.children.length; index++) if (parent.children[index] == itemToRemove) { parent.children.splice(index, 1); break; } } } // Now remove it from our view. FeedSubscriptions.mFeedContainers.splice(aRow, 1); // Now invalidate the correct tree rows. this.mRowCount--; this.treeBox.rowCountChanged(aRow, -1); // Now update the selection position, unless noSelect (selection is // done later or not at all). If the item is the last child, select the // parent. Otherwise select the next sibling. if (!aNoSelect) { if (aRow <= FeedSubscriptions.mFeedContainers.length) this.selection.select(hasNextSibling ? aRow : aRow - 1); else this.selection.clearSelection(); } // Now refocus the tree. FeedSubscriptions.mTree.focus(); }, getCellText: function (aRow, aColumn) { let item = this.getItemAtIndex(aRow); return (item && aColumn.id == "folderNameCol") ? item.name : ""; }, getImageSrc: function(aRow, aCol) { let item = this.getItemAtIndex(aRow); if ((item.folder && item.folder.isServer) || item.open) return ""; if (item.favicon != null) return item.favicon; if (item.folder && FeedSubscriptions.mMainWin && "gFolderTreeView" in FeedSubscriptions.mMainWin) { let favicon = FeedSubscriptions.mMainWin.gFolderTreeView .getFolderCacheProperty(item.folder, "favicon"); if (favicon != null) return item.favicon = favicon; } let callback = (iconUrl => { item.favicon = iconUrl; if (item.folder) { for (let child of item.children) if (!child.container) { child.favicon = iconUrl; break; } } this.selection.tree.invalidateRow(aRow); }); // A closed non server folder. if (item.folder) { for (let child of item.children) { if (!child.container) { if (child.favicon != null) return child.favicon; setTimeout(() => { FeedUtils.getFavicon(child.parentFolder, child.url, null, window, callback); }, 0); break; } } } else { // A feed. setTimeout(() => { FeedUtils.getFavicon(item.parentFolder, item.url, null, window, callback); }, 0); } // Store empty string to return default while favicons are retrieved. return item.favicon = ""; }, canDrop: function (aRow, aOrientation) { let dropResult = this.extractDragData(aRow); return aOrientation == Ci.nsITreeView.DROP_ON && dropResult.canDrop && (dropResult.dropUrl || dropResult.dropOnIndex != this.kRowIndexUndefined); }, drop: function (aRow, aOrientation) { let win = FeedSubscriptions; let results = this.extractDragData(aRow); if (!results.canDrop) return; // Preselect the drop folder. this.selection.select(aRow); if (results.dropUrl) { // Don't freeze the app that initiated the drop just because we are // in a loop waiting for the user to dimisss the add feed dialog. setTimeout(function() { win.addFeed(results.dropUrl, null, true, null, win.kSubscribeMode); }, 0); let folderItem = this.getItemAtIndex(aRow); FeedUtils.log.debug("drop: folder, url - " + folderItem.folder.name + ", " + results.dropUrl); } else if (results.dropOnIndex != this.kRowIndexUndefined) { win.moveCopyFeed(results.dropOnIndex, aRow, results.dropEffect); } }, // Helper function for drag and drop. extractDragData: function(aRow) { let dt = this._currentDataTransfer; let dragDataResults = { canDrop: false, dropUrl: null, dropOnIndex: this.kRowIndexUndefined, dropEffect: dt.dropEffect }; if (dt.getData("text/x-moz-feed-index")) { // Dragging a feed in the tree. if (this.selection) { dragDataResults.dropOnIndex = this.selection.currentIndex; let curItem = this.getItemAtIndex(this.selection.currentIndex); let newItem = this.getItemAtIndex(aRow); let curServer = curItem && curItem.parentFolder ? curItem.parentFolder.server : null; let newServer = newItem && newItem.folder ? newItem.folder.server : null; // No copying within the same account and no moving to the account // folder in the same account. if (!(curServer == newServer && (dragDataResults.dropEffect == "copy" || newItem.folder == curItem.parentFolder || newItem.folder.isServer))) dragDataResults.canDrop = true; } } else { // Try to get a feed url. let validUri = FeedUtils.getFeedUriFromDataTransfer(dt); if (validUri) { dragDataResults.canDrop = true; dragDataResults.dropUrl = validUri.spec; } } return dragDataResults; }, getParentIndex: function (aRow) { let item = this.getItemAtIndex(aRow); if (item) { for (let index = aRow; index >= 0; index--) if (FeedSubscriptions.mFeedContainers[index].level < item.level) return index; } return this.kRowIndexUndefined; }, isIndexChildOfParentIndex: function (aRow, aChildRow) { // For visible tree rows, not if items are children of closed folders. let item = this.getItemAtIndex(aRow); if (!item || aChildRow <= aRow) return false; let targetLevel = this.getItemAtIndex(aRow).level; let rows = FeedSubscriptions.mFeedContainers; for (let i = aRow + 1; i < rows.length; i++) { if (this.getItemAtIndex(i).level <= targetLevel) break; if (aChildRow == i) return true; } return false; }, hasNextSibling: function(aRow, aAfterIndex) { let targetLevel = this.getItemAtIndex(aRow).level; let rows = FeedSubscriptions.mFeedContainers; for (let i = aAfterIndex + 1; i < rows.length; i++) { if (this.getItemAtIndex(i).level == targetLevel) return true; if (this.getItemAtIndex(i).level < targetLevel) return false; } return false; }, hasPreviousSibling: function (aRow) { let item = this.getItemAtIndex(aRow); if (item && aRow) return this.getItemAtIndex(aRow - 1).level == item.level; else return false; }, getLevel: function (aRow) { let item = this.getItemAtIndex(aRow); if (!item) return 0; return item.level; }, toggleOpenState: function (aRow) { let item = this.getItemAtIndex(aRow); if (!item) return; // Save off the current selection item. let seln = this.selection; let currentSelectionIndex = seln.currentIndex; let rowsChanged = this.toggle(aRow) // Now restore selection, ensuring selection is maintained on toggles. if (currentSelectionIndex > aRow) seln.currentIndex = currentSelectionIndex + rowsChanged; else seln.select(currentSelectionIndex); seln.selectEventsSuppressed = false; }, toggle: function (aRow) { // Collapse the row, or build sub rows based on open states in the map. let item = this.getItemAtIndex(aRow); if (!item) return null; let rows = FeedSubscriptions.mFeedContainers; let rowCount = 0; let multiplier; function addDescendants(aItem) { for (let i = 0; i < aItem.children.length; i++) { rowCount++; let child = aItem.children[i]; rows.splice(aRow + rowCount, 0, child); if (child.open) addDescendants(child); } } if (item.open) { // Close the container. Add up all subfolders and their descendants // who may be open. multiplier = -1; let nextRow = aRow + 1; let nextItem = rows[nextRow]; while (nextItem && nextItem.level > item.level) { rowCount++; nextItem = rows[++nextRow]; } rows.splice(aRow + 1, rowCount); } else { // Open the container. Restore the open state of all subfolder and // their descendants. multiplier = 1; addDescendants(item); } let delta = multiplier * rowCount; this.mRowCount += delta; item.open = !item.open; // Suppress the select event caused by rowCountChanged. this.selection.selectEventsSuppressed = true; // Add or remove the children from our view. this.treeBox.rowCountChanged(aRow, delta); return delta; } }, makeFolderObject: function (aFolder, aCurrentLevel) { let defaultQuickMode = aFolder.server.getBoolValue("quickMode"); let optionsAcct = aFolder.isServer ? FeedUtils.getOptionsAcct(aFolder.server) : null; let open = !aFolder.isServer && aFolder.server == this.mRSSServer && this.mActionMode == this.kImportingOPML ? true : false let folderObject = { children : [], folder : aFolder, name : aFolder.prettyName, level : aCurrentLevel, url : aFolder.URI, quickMode: defaultQuickMode, options : optionsAcct, open : open, container: true, favicon : null }; // If a feed has any sub folders, add them to the list of children. let folderEnumerator = aFolder.subFolders; while (folderEnumerator.hasMoreElements()) { let folder = folderEnumerator.getNext(); if ((folder instanceof Ci.nsIMsgFolder) && !folder.getFlag(Ci.nsMsgFolderFlags.Trash) && !folder.getFlag(Ci.nsMsgFolderFlags.Virtual)) { folderObject.children .push(this.makeFolderObject(folder, aCurrentLevel + 1)); } } let feeds = this.getFeedsInFolder(aFolder); for (let feed of feeds) { // Now add any feed urls for the folder. folderObject.children.push(this.makeFeedObject(feed, aFolder, aCurrentLevel + 1)); } // Finally, set the folder's quickMode based on the its first feed's // quickMode, since that is how the view determines summary mode, and now // quickMode is updated to be the same for all feeds in a folder. if (feeds && feeds[0]) folderObject.quickMode = feeds[0].quickMode; folderObject.children = this.folderItemSorter(folderObject.children); return folderObject; }, folderItemSorter: function (aArray) { return aArray.sort(function(a, b) { return a.name.toLowerCase() > b.name.toLowerCase() }). sort(function(a, b) { return a.container < b.container }); }, getFeedsInFolder: function (aFolder) { let feeds = new Array(); let feedUrlArray = FeedUtils.getFeedUrlsInFolder(aFolder); if (!feedUrlArray) // No feedUrls in this folder. return feeds; for (let url of feedUrlArray) { let feedResource = FeedUtils.rdf.GetResource(url); let feed = new Feed(feedResource, aFolder.server); feeds.push(feed); } return feeds; }, makeFeedObject: function (aFeed, aFolder, aLevel) { // Look inside the data source for the feed properties. let feed = { children : [], parentFolder: aFolder, name : aFeed.title || aFeed.description || aFeed.url, url : aFeed.url, quickMode : aFeed.quickMode, options : aFeed.options || FeedUtils.optionsTemplate, level : aLevel, open : false, container : false, favicon : null }; return feed; }, loadSubscriptions: function () { // Put together an array of folders. Each feed account level folder is // included as the root. let numFolders = 0; let feedContainers = []; // Get all the feed account folders. let feedRootFolders = FeedUtils.getAllRssServerRootFolders(); feedRootFolders.forEach(function(rootFolder) { feedContainers.push(this.makeFolderObject(rootFolder, 0)); numFolders++; }, this); this.mFeedContainers = feedContainers; this.mView.mRowCount = numFolders; FeedSubscriptions.mTree.focus(); }, /** * Find the folder in the tree. The search may be limited to subfolders of * a known folder, or expanded to include the entire tree. This function is * also used to insert/remove folders without rebuilding the tree view cache * (to avoid position/toggle state loss). * * @param aFolder nsIMsgFolder - the folder to find. * @param [aParams] object - params object, containing: * * [parentIndex] int - index of folder to start the search; if * null (default), the index of the folder's * rootFolder will be used. * [select] boolean - if true (default) the folder's ancestors * will be opened and the folder selected. * [open] boolean - if true (default) the folder is opened. * [remove] boolean - delete the item from tree row cache if true, * false (default) otherwise. * [newFolder] nsIMsgFolder - if not null (default) the new folder, * for add or rename. * * @return bool found - true if found, false if not. */ selectFolder: function(aFolder, aParms) { let folderURI = aFolder.URI; let parentIndex = aParms && ("parentIndex" in aParms) ? aParms.parentIndex : null; let selectIt = aParms && ("select" in aParms) ? aParms.select : true; let openIt = aParms && ("open" in aParms) ? aParms.open : true; let removeIt = aParms && ("remove" in aParms) ? aParms.remove : false; let newFolder = aParms && ("newFolder" in aParms) ? aParms.newFolder : null; let startIndex, startItem; let found = false; let firstVisRow, curFirstVisRow, curLastVisRow; if (this.mView.treeBox) firstVisRow = this.mView.treeBox.getFirstVisibleRow(); if (parentIndex != null) { // Use the parentIndex if given. startIndex = parentIndex; if (aFolder.isServer) // Fake item for account root folder. startItem = { name: "AccountRoot", children: [this.mView.getItemAtIndex(startIndex)], container: true, open: false, url: null, level: -1}; else startItem = this.mView.getItemAtIndex(startIndex); } else { // Get the folder's root parent index. let index = 0; for (index; index < this.mView.rowCount; index++) { let item = this.mView.getItemAtIndex(index); if (item.url == aFolder.server.rootFolder.URI) break; } startIndex = index; if (aFolder.isServer) // Fake item for account root folder. startItem = { name: "AccountRoot", children: [this.mView.getItemAtIndex(startIndex)], container: true, open: false, url: null, level: -1}; else startItem = this.mView.getItemAtIndex(startIndex); } function containsFolder(aItem) { // Search for the folder. If it's found, set the open state on all // ancestor folders. A toggle() rebuilds the view rows to match the map. if (aItem.url == folderURI) return found = true; for (let i = 0; i < aItem.children.length; i++) { if (aItem.children[i].container && containsFolder(aItem.children[i])) { if (removeIt && aItem.children[i].url == folderURI) { // Get all occurences in the tree cache arrays. FeedUtils.log.debug("selectFolder: delete in cache, " + "parent:children:item:index - "+ aItem.name + ":" + aItem.children.length + ":" + aItem.children[i].name + ":" + i); aItem.children.splice(i, 1); FeedUtils.log.debug("selectFolder: deleted in cache, " + "parent:children - " + aItem.name + ":" + aItem.children.length); removeIt = false; return true; } if (newFolder) { let newItem = FeedSubscriptions.makeFolderObject(newFolder, aItem.level + 1); newItem.open = aItem.children[i].open; if (newFolder.isServer) FeedSubscriptions.mFeedContainers[startIndex] = newItem; else { aItem.children[i] = newItem; aItem.children = FeedSubscriptions.folderItemSorter(aItem.children); } FeedUtils.log.trace("selectFolder: parentName:newFolderName:newFolderItem - " + aItem.name + ":" + newItem.name + ":" + newItem.toSource()); newFolder = null; return true; } if (!found) { // For the folder to find. found = true; aItem.children[i].open = openIt; } else { // For ancestor folders. if (selectIt || openIt) aItem.children[i].open = true; } return true; } } return false; } if (startItem) { // Find a folder with a specific parent. containsFolder(startItem); if (!found) return false; if (!selectIt) return true; if (startItem.open) this.mView.toggle(startIndex); this.mView.toggleOpenState(startIndex); } for (let index = 0; index < this.mView.rowCount && selectIt; index++) { // The desired folder is now in the view. let item = this.mView.getItemAtIndex(index); if (!item.container) continue; if (item.url == folderURI) { if (item.children.length && ((!item.open && openIt) || (item.open && !openIt))) this.mView.toggleOpenState(index); this.mView.selection.select(index); found = true; break; } } // Ensure tree position does not jump unnecessarily. curFirstVisRow = this.mView.treeBox.getFirstVisibleRow(); curLastVisRow = this.mView.treeBox.getLastVisibleRow(); if (firstVisRow >= 0 && this.mView.rowCount - curLastVisRow > firstVisRow - curFirstVisRow) this.mView.treeBox.scrollToRow(firstVisRow); else this.mView.treeBox.ensureRowIsVisible(this.mView.rowCount - 1); FeedUtils.log.debug("selectFolder: curIndex:firstVisRow:" + "curFirstVisRow:curLastVisRow:rowCount - " + this.mView.selection.currentIndex + ":" + firstVisRow + ":" + curFirstVisRow + ":" + curLastVisRow + ":" + this.mView.rowCount); return found; }, /** * Find the feed in the tree. The search first gets the feed's folder, * then selects the child feed. * * @param aFeed {Feed object} - the feed to find. * @param [aParentIndex] integer - index to start the folder search. * * @return found bool - true if found, false if not. */ selectFeed: function(aFeed, aParentIndex) { let folder = aFeed.folder; let found = false; if (aFeed.folder.isServer) { // If passed the root folder, the caller wants to get the feed's folder // from the db (for cases of an ancestor folder rename/move). let itemResource = FeedUtils.rdf.GetResource(aFeed.url); let ds = FeedUtils.getSubscriptionsDS(aFeed.folder.server); folder = ds.GetTarget(itemResource, FeedUtils.FZ_DESTFOLDER, true); } if (this.selectFolder(folder, { parentIndex: aParentIndex })) { let seln = this.mView.selection; let item = this.mView.currentItem; if (item) { for (let i = seln.currentIndex + 1; i < this.mView.rowCount; i++) { if (this.mView.getItemAtIndex(i).url == aFeed.url) { this.mView.selection.select(i); this.mView.treeBox.ensureRowIsVisible(i); found = true; break; } } } } return found; }, updateFeedData: function (aItem) { if (!aItem) return; let nameValue = document.getElementById("nameValue"); let locationValue = document.getElementById("locationValue"); let locationValidate = document.getElementById("locationValidate"); let selectFolder = document.getElementById("selectFolder"); let selectFolderValue = document.getElementById("selectFolderValue"); let isServer = aItem.folder && aItem.folder.isServer; let isFolder = aItem.folder && !aItem.folder.isServer; let isFeed = !aItem.container; let server, displayFolder; if (isFeed) { // A feed item. Set the feed location and title info. nameValue.value = aItem.name; locationValue.value = aItem.url; locationValidate.removeAttribute("collapsed"); // Root the location picker to the news & blogs server. server = aItem.parentFolder.server; displayFolder = aItem.parentFolder; } else { // A folder/container item. nameValue.value = ""; nameValue.disabled = true; locationValue.value = ""; locationValidate.setAttribute("collapsed", true); server = aItem.folder.server; displayFolder = aItem.folder; } // Common to both folder and feed items. nameValue.disabled = aItem.container; this.setFolderPicker(displayFolder, isFeed); // Set quick mode value. document.getElementById("quickMode").checked = aItem.quickMode; // Autotag items. let autotagEnable = document.getElementById("autotagEnable"); let autotagUsePrefix = document.getElementById("autotagUsePrefix"); let autotagPrefix = document.getElementById("autotagPrefix"); let categoryPrefsAcct = FeedUtils.getOptionsAcct(server).category; if (isServer) aItem.options = FeedUtils.getOptionsAcct(server); let categoryPrefs = aItem.options ? aItem.options.category : null; autotagEnable.checked = categoryPrefs && categoryPrefs.enabled; autotagUsePrefix.checked = categoryPrefs && categoryPrefs.prefixEnabled; autotagUsePrefix.disabled = !autotagEnable.checked; autotagPrefix.disabled = autotagUsePrefix.disabled || !autotagUsePrefix.checked; autotagPrefix.value = categoryPrefs && categoryPrefs.prefix ? categoryPrefs.prefix : ""; }, setFolderPicker: function(aFolder, aIsFeed) { let editFeed = document.getElementById("editFeed"); let folderPrettyPath = FeedUtils.getFolderPrettyPath(aFolder); if (!folderPrettyPath) return editFeed.disabled = true; let selectFolder = document.getElementById("selectFolder"); let selectFolderPopup = document.getElementById("selectFolderPopup"); let selectFolderValue = document.getElementById("selectFolderValue"); selectFolder.setAttribute("hidden", !aIsFeed); selectFolder._folder = aFolder; selectFolderValue.setAttribute("hidden", aIsFeed); selectFolderValue.setAttribute("showfilepath", false); if (aIsFeed) { selectFolderPopup._ensureInitialized(); selectFolderPopup.selectFolder(aFolder); selectFolder.setAttribute("label", folderPrettyPath); selectFolder.setAttribute("uri", aFolder.URI); } else { selectFolderValue.value = folderPrettyPath; selectFolderValue.setAttribute("prettypath", folderPrettyPath); selectFolderValue.setAttribute("filepath", aFolder.filePath.path); } return editFeed.disabled = false; }, onClickSelectFolderValue: function(aEvent) { let target = aEvent.target; if ((("button" in aEvent) && (aEvent.button != 0 || aEvent.originalTarget.localName != "div" || target.selectionStart != target.selectionEnd)) || (aEvent.keyCode && aEvent.keyCode != aEvent.DOM_VK_RETURN)) return; // Toggle between showing prettyPath and absolute filePath. if (target.getAttribute("showfilepath") == "true") { target.setAttribute("showfilepath", false); target.value = target.getAttribute("prettypath"); } else { target.setAttribute("showfilepath", true); target.value = target.getAttribute("filepath"); } }, setNewFolder: function(aEvent) { aEvent.stopPropagation(); this.setFolderPicker(aEvent.target._folder, true); this.editFeed(); }, setSummary: function(aChecked) { let item = this.mView.currentItem; if (!item || !item.folder) // Not a folder. return; if (item.folder.isServer) { if (document.getElementById("locationValue").value) // Intent is to add a feed/folder to the account, so return. return; // An account folder. If it changes, all non feed containing subfolders // need to be updated with the new default. item.folder.server.setBoolValue("quickMode", aChecked); this.FolderListener.folderAdded(item.folder); } else if (!FeedUtils.getFeedUrlsInFolder(item.folder)) // Not a folder with feeds. return; else { let feedsInFolder = this.getFeedsInFolder(item.folder); // Update the feeds database, for each feed in the folder. feedsInFolder.forEach(function(feed) { feed.quickMode = aChecked; }); // Update the folder's feeds properties in the tree map. item.children.forEach(function(feed) { feed.quickMode = aChecked; }); let ds = FeedUtils.getSubscriptionsDS(item.folder.server); ds.Flush(); } // Update the folder in the tree map. item.quickMode = aChecked; let message = FeedUtils.strings.GetStringFromName("subscribe-feedUpdated"); this.updateStatusItem("statusText", message); }, setCategoryPrefs: function(aNode) { let item = this.mView.currentItem; if (!item) return; let isServer = item.folder && item.folder.isServer; let isFolder = item.folder && !item.folder.isServer; let autotagEnable = document.getElementById("autotagEnable"); let autotagUsePrefix = document.getElementById("autotagUsePrefix"); let autotagPrefix = document.getElementById("autotagPrefix"); if (isFolder || (isServer && document.getElementById("locationValue").value)) { // Intend to subscribe a feed to a folder, a value must be in the url // field. Update states for addFeed() and return. autotagUsePrefix.disabled = !autotagEnable.checked; autotagPrefix.disabled = autotagUsePrefix.disabled || !autotagUsePrefix.checked; return; } switch (aNode.id) { case "autotagEnable": item.options.category.enabled = aNode.checked; break; case "autotagUsePrefix": item.options.category.prefixEnabled = aNode.checked; item.options.category.prefix = autotagPrefix.value; break; } if (isServer) { FeedUtils.setOptionsAcct(item.folder.server, item.options) } else { let feedResource = FeedUtils.rdf.GetResource(item.url); let feed = new Feed(feedResource, item.parentFolder.server); feed.options = item.options; let ds = FeedUtils.getSubscriptionsDS(item.parentFolder.server); ds.Flush(); } this.updateFeedData(item); let message = FeedUtils.strings.GetStringFromName("subscribe-feedUpdated"); this.updateStatusItem("statusText", message); }, onKeyPress: function(aEvent) { if (aEvent.keyCode == aEvent.DOM_VK_DELETE && aEvent.target.id == "rssSubscriptionsList") this.removeFeed(true); this.clearStatusInfo(); }, onSelect: function () { let item = this.mView.currentItem; this.updateFeedData(item); this.setSummaryFocus(); this.updateButtons(item); }, updateButtons: function (aSelectedItem) { let item = aSelectedItem; let isServer = item && item.folder && item.folder.isServer; let disable = !item || !item.container || isServer || this.mActionMode == this.kImportingOPML; document.getElementById("addFeed").disabled = disable; disable = !item || (item.container && !isServer) || this.mActionMode == this.kImportingOPML; document.getElementById("editFeed").disabled = disable; disable = !item || item.container || this.mActionMode == this.kImportingOPML; document.getElementById("removeFeed").disabled = disable; disable = !item || !isServer || this.mActionMode == this.kImportingOPML; document.getElementById("importOPML").disabled = disable; document.getElementById("exportOPML").disabled = disable; }, onMouseDown: function (aEvent) { if (aEvent.button != 0 || aEvent.target.id == "validationText" || aEvent.target.id == "addCertException") return; this.clearStatusInfo(); }, setSummaryFocus: function () { let item = this.mView.currentItem; if (!item) return; let locationValue = document.getElementById("locationValue"); let quickMode = document.getElementById("quickMode"); let autotagEnable = document.getElementById("autotagEnable"); let autotagUsePrefix = document.getElementById("autotagUsePrefix"); let autotagPrefix = document.getElementById("autotagPrefix"); let isServer = item.folder && item.folder.isServer; let isFolder = item.folder && !item.folder.isServer; let isFeed = !item.container; // Enable summary/autotag by default. quickMode.disabled = autotagEnable.disabled = false; autotagUsePrefix.disabled = !autotagEnable.checked; autotagPrefix.disabled = autotagUsePrefix.disabled || !autotagUsePrefix.checked; if (isServer) { let disable = locationValue.hasAttribute("focused") || locationValue.value; document.getElementById("addFeed").disabled = !disable; document.getElementById("editFeed").disabled = disable; } else if (isFolder) { if (!locationValue.hasAttribute("focused") && !locationValue.value) { // Enabled for a folder with feeds. Autotag disabled unless intent is // to add a feed. quickMode.disabled = !FeedUtils.getFeedUrlsInFolder(item.folder); autotagEnable.disabled = autotagUsePrefix.disabled = autotagPrefix.disabled = true; } } else { // Summary is per folder. quickMode.disabled = true; } }, removeFeed: function (aPrompt) { let seln = this.mView.selection; if (seln.count != 1) return; let itemToRemove = this.mView.getItemAtIndex(seln.currentIndex); if (!itemToRemove || itemToRemove.container) return; if (aPrompt) { // Confirm unsubscribe prompt. let pTitle = FeedUtils.strings.GetStringFromName( "subscribe-confirmFeedDeletionTitle"); let pMessage = FeedUtils.strings.formatStringFromName( "subscribe-confirmFeedDeletion", [itemToRemove.name], 1); if (Services.prompt.confirmEx(window, pTitle, pMessage, Ci.nsIPromptService.STD_YES_NO_BUTTONS, null, null, null, null, { })) return; } FeedUtils.deleteFeed(FeedUtils.rdf.GetResource(itemToRemove.url), itemToRemove.parentFolder.server, itemToRemove.parentFolder); // Now that we have removed the feed from the datasource, it is time to // update our view layer. Update parent folder's quickMode if necessary // and remove the child from its parent folder object. let parentIndex = this.mView.getParentIndex(seln.currentIndex); let parentItem = this.mView.getItemAtIndex(parentIndex); this.updateFolderQuickModeInView(itemToRemove, parentItem, true); this.mView.removeItemAtIndex(seln.currentIndex, false); let message = FeedUtils.strings.GetStringFromName("subscribe-feedRemoved"); this.updateStatusItem("statusText", message); }, /** * This addFeed is used by 1) Add button, 1) Update button, 3) Drop of a * feed url on a folder (which can be an add or move). If Update, the new * url is added and the old removed; thus aParse is false and no new messages * are downloaded, the feed is only validated and stored in the db. If dnd, * the drop folder is selected and the url is prefilled, so proceed just as * though the url were entered manually. This allows a user to see the dnd * url better in case of errors. * * @param [aFeedLocation] string - the feed url; get the url from the * input field if null. * @param [aFolder] nsIMsgFolder - folder to subscribe, current selected * folder if null. * @param [aParse] boolean - if true (default) parse and download * the feed's articles. * @param [aParams] object - additional params. * @param [aMode] integer - action mode (default is kSubscribeMode) * of the add. * * @return success boolean - true if edit checks passed and an * async download has been initiated. */ addFeed: function(aFeedLocation, aFolder, aParse, aParams, aMode) { let message; let parse = aParse == null ? true : aParse; let mode = aMode == null ? this.kSubscribeMode : aMode; let locationValue = document.getElementById("locationValue"); let quickMode = aParams && ("quickMode" in aParams) ? aParams.quickMode : document.getElementById("quickMode").checked; let name = aParams && ("name" in aParams) ? aParams.name : document.getElementById("nameValue").value; let options = aParams && ("options" in aParams) ? aParams.options : null; if (aFeedLocation) locationValue.value = aFeedLocation; let feedLocation = locationValue.value.trim(); if (!feedLocation) { message = locationValue.getAttribute("placeholder"); this.updateStatusItem("statusText", message); return false; } if (!FeedUtils.isValidScheme(feedLocation)) { message = FeedUtils.strings.GetStringFromName("subscribe-feedNotValid"); this.updateStatusItem("statusText", message); return false; } let addFolder; if (aFolder) { // For Update or if passed a folder. if (aFolder instanceof Ci.nsIMsgFolder) addFolder = aFolder; } else { // A folder must be selected for Add and Drop. let index = this.mView.selection.currentIndex; let item = this.mView.getItemAtIndex(index); if (item && item.container) addFolder = item.folder; } // Shouldn't happen. Or else not passed an nsIMsgFolder. if (!addFolder) return false; // Before we go any further, make sure the user is not already subscribed // to this feed. if (FeedUtils.feedAlreadyExists(feedLocation, addFolder.server)) { message = FeedUtils.strings.GetStringFromName( "subscribe-feedAlreadySubscribed"); this.updateStatusItem("statusText", message); return false; } if (!options) { // Not passed a param, get values from the ui. options = FeedUtils.optionsTemplate; options.category.enabled = document.getElementById("autotagEnable").checked; options.category.prefixEnabled = document.getElementById("autotagUsePrefix").checked; options.category.prefix = document.getElementById("autotagPrefix").value; } let folderURI = addFolder.isServer ? null : addFolder.URI; let feedProperties = { feedName : name, feedLocation : feedLocation, folderURI : folderURI, server : addFolder.server, quickMode : quickMode, options : options }; let feed = this.storeFeed(feedProperties); if (!feed) return false; // Now validate and start downloading the feed. message = FeedUtils.strings.GetStringFromName("subscribe-validating-feed"); this.updateStatusItem("statusText", message); this.updateStatusItem("progressMeter", 0); document.getElementById("addFeed").setAttribute("disabled", true); this.mActionMode = mode; feed.download(parse, this.mFeedDownloadCallback); return true; }, // Helper routine used by addFeed and importOPMLFile. storeFeed: function(feedProperties) { let itemResource = FeedUtils.rdf.GetResource(feedProperties.feedLocation); let feed = new Feed(itemResource, feedProperties.server); // If the user specified a folder to add the feed to, then set it here. if (feedProperties.folderURI) { let folderResource = FeedUtils.rdf.GetResource(feedProperties.folderURI); if (folderResource) { let folder = folderResource.QueryInterface(Ci.nsIMsgFolder); if (folder && !folder.isServer) feed.folder = folder; } } feed.title = feedProperties.feedName; feed.quickMode = feedProperties.quickMode; feed.options = feedProperties.options; return feed; }, updateAccount: function(aItem) { // Check to see if the categoryPrefs custom prefix string value changed. let editAutotagPrefix = document.getElementById("autotagPrefix").value; if (aItem.options.category.prefix != editAutotagPrefix) { aItem.options.category.prefix = editAutotagPrefix; FeedUtils.setOptionsAcct(aItem.folder.server, aItem.options) let message = FeedUtils.strings.GetStringFromName("subscribe-feedUpdated"); this.updateStatusItem("statusText", message); } }, editFeed: function() { let seln = this.mView.selection; if (seln.count != 1) return; let itemToEdit = this.mView.getItemAtIndex(seln.currentIndex); if (itemToEdit.folder && itemToEdit.folder.isServer) { this.updateAccount(itemToEdit) return; } if (!itemToEdit || itemToEdit.container || !itemToEdit.parentFolder) return; let resource = FeedUtils.rdf.GetResource(itemToEdit.url); let currentFolderServer = itemToEdit.parentFolder.server; let ds = FeedUtils.getSubscriptionsDS(currentFolderServer); let currentFolderURI = itemToEdit.parentFolder.URI; let feed = new Feed(resource, currentFolderServer); feed.folder = itemToEdit.parentFolder; let editNameValue = document.getElementById("nameValue").value; let editFeedLocation = document.getElementById("locationValue").value.trim(); let selectFolder = document.getElementById("selectFolder"); let editQuickMode = document.getElementById("quickMode").checked; let editAutotagPrefix = document.getElementById("autotagPrefix").value; if (feed.url != editFeedLocation) { // Updating a url. We need to add the new url and delete the old, to // ensure everything is cleaned up correctly. this.addFeed(null, itemToEdit.parentFolder, false, null, this.kUpdateMode) return; } // Did the user change the folder URI for storing the feed? let editFolderURI = selectFolder.getAttribute("uri"); if (currentFolderURI != editFolderURI) { // Make sure the new folderpicked folder is visible. this.selectFolder(selectFolder._folder); // Now go back to the feed item. this.selectFeed(feed, null); // We need to find the index of the new parent folder. let newParentIndex = this.mView.kRowIndexUndefined; for (let index = 0; index < this.mView.rowCount; index++) { let item = this.mView.getItemAtIndex(index); if (item && item.container && item.url == editFolderURI) { newParentIndex = index; break; } } if (newParentIndex != this.mView.kRowIndexUndefined) this.moveCopyFeed(seln.currentIndex, newParentIndex, "move"); return; } let updated = false; let message = ""; // Disable the button until the update completes and we process the async // verify response. document.getElementById("editFeed").setAttribute("disabled", true); // Check to see if the title value changed, no blank title allowed. if (feed.title != editNameValue) { if (!editNameValue) { document.getElementById("nameValue").value = feed.title; } else { feed.title = editNameValue; itemToEdit.name = editNameValue; seln.tree.invalidateRow(seln.currentIndex); updated = true; } } // Check to see if the quickMode value changed. if (feed.quickMode != editQuickMode) { feed.quickMode = editQuickMode; itemToEdit.quickMode = editQuickMode; updated = true; } // Check to see if the categoryPrefs custom prefix string value changed. if (itemToEdit.options.category.prefix != editAutotagPrefix && itemToEdit.options.category.prefix != null && editAutotagPrefix != "") { itemToEdit.options.category.prefix = editAutotagPrefix; feed.options = itemToEdit.options; updated = true; } let verifyDelay = 0; if (updated) { ds.Flush(); message = FeedUtils.strings.GetStringFromName("subscribe-feedUpdated"); this.updateStatusItem("statusText", message); verifyDelay = 1500; } // Now we want to verify if the stored feed url still works. If it // doesn't, show the error. Delay a bit to leave Updated message visible. message = FeedUtils.strings.GetStringFromName("subscribe-validating-feed"); this.mActionMode = this.kVerifyUrlMode; setTimeout(() => { this.updateStatusItem("statusText", message); this.updateStatusItem("progressMeter", "?"); feed.download(false, this.mFeedDownloadCallback); }, verifyDelay); }, /** * Moves or copies a feed to another folder or account. * * @param int aOldFeedIndex - index in tree of target feed item. * @param int aNewParentIndex - index in tree of target parent folder item. * @param string aMoveCopy - either "move" or "copy". */ moveCopyFeed: function(aOldFeedIndex, aNewParentIndex, aMoveCopy) { let moveFeed = aMoveCopy == "move"; let currentItem = this.mView.getItemAtIndex(aOldFeedIndex); if (!currentItem || this.mView.getParentIndex(aOldFeedIndex) == aNewParentIndex) // If the new parent is the same as the current parent, then do nothing. return; let currentParentIndex = this.mView.getParentIndex(aOldFeedIndex); let currentParentItem = this.mView.getItemAtIndex(currentParentIndex); let currentParentResource = FeedUtils.rdf.GetResource(currentParentItem.url); let currentFolder = currentParentResource.QueryInterface(Ci.nsIMsgFolder); let newParentItem = this.mView.getItemAtIndex(aNewParentIndex); let newParentResource = FeedUtils.rdf.GetResource(newParentItem.url); let newFolder = newParentResource.QueryInterface(Ci.nsIMsgFolder); let ds = FeedUtils.getSubscriptionsDS(currentItem.parentFolder.server); let resource = FeedUtils.rdf.GetResource(currentItem.url); let accountMoveCopy = false; if (currentFolder.rootFolder.URI == newFolder.rootFolder.URI) { // Moving within the same account/feeds db. if (newFolder.isServer || !moveFeed) // No moving to account folder if already in the account; can only move, // not copy, to folder in the same account. return; // Unassert the older URI, add an assertion for the new parent URI. ds.Change(resource, FeedUtils.FZ_DESTFOLDER, currentParentResource, newParentResource); ds.Flush(); // Update folderpane favicons. FeedUtils.setFolderPaneProperty(currentFolder, "favicon", null, "row"); FeedUtils.setFolderPaneProperty(newFolder, "favicon", null, "row"); } else { // Moving/copying to a new account. If dropping on the account folder, // a new subfolder is created if necessary. accountMoveCopy = true; let mode = moveFeed ? this.kMoveMode : this.kCopyMode; let params = {quickMode: currentItem.quickMode, name: currentItem.name, options: currentItem.options}; // Subscribe to the new folder first. If it already exists in the // account or on error, return. if (!this.addFeed(currentItem.url, newFolder, false, params, mode)) return; // Unsubscribe the feed from the old folder, if add to the new folder // is successfull, and doing a move. if (moveFeed) FeedUtils.deleteFeed(FeedUtils.rdf.GetResource(currentItem.url), currentItem.parentFolder.server, currentItem.parentFolder); } // Update local favicons. currentParentItem.favicon = newParentItem.favicon = null; // Finally, update our view layer. Update old parent folder's quickMode // and remove the old row, if move. Otherwise no change to the view. if (moveFeed) { this.updateFolderQuickModeInView(currentItem, currentParentItem, true); this.mView.removeItemAtIndex(aOldFeedIndex, true); if (aNewParentIndex > aOldFeedIndex) aNewParentIndex--; } if (accountMoveCopy) { // If a cross account move/copy, download callback will update the view // with the new location. Preselect folder/mode for callback. this.selectFolder(newFolder, { parentIndex: aNewParentIndex }); return; } // Add the new row location to the view. currentItem.level = newParentItem.level + 1; currentItem.parentFolder = newFolder; this.updateFolderQuickModeInView(currentItem, newParentItem, false); newParentItem.children.push(currentItem); if (newParentItem.open) // Close the container, selecting the feed will rebuild the view rows. this.mView.toggle(aNewParentIndex); this.selectFeed({folder: newParentItem.folder, url: currentItem.url}, aNewParentIndex); let message = FeedUtils.strings.GetStringFromName("subscribe-feedMoved"); this.updateStatusItem("statusText", message); }, updateFolderQuickModeInView: function (aFeedItem, aParentItem, aRemove) { let feedItem = aFeedItem; let parentItem = aParentItem; let feedUrlArray = FeedUtils.getFeedUrlsInFolder(feedItem.parentFolder); let feedsInFolder = feedUrlArray ? feedUrlArray.length : 0; if (aRemove && feedsInFolder < 1) // Removed only feed in folder; set quickMode to server default. parentItem.quickMode = parentItem.folder.server.getBoolValue("quickMode"); if (!aRemove) { // Just added a feed to a folder. If there are already feeds in the // folder, the feed must reflect the parent's quickMode. If it is the // only feed, update the parent folder to the feed's quickMode. if (feedsInFolder > 1) { let feedResource = FeedUtils.rdf.GetResource(feedItem.url); let feed = new Feed(feedResource, feedItem.parentFolder.server); feed.quickMode = parentItem.quickMode; feedItem.quickMode = parentItem.quickMode; } else parentItem.quickMode = feedItem.quickMode; } }, onDragStart: function (aEvent) { // Get the selected feed article (if there is one). let seln = this.mView.selection; if (seln.count != 1) return; // Only initiate a drag if the item is a feed (ignore folders/containers). let item = this.mView.getItemAtIndex(seln.currentIndex); if (!item || item.container) return; aEvent.dataTransfer.setData("text/x-moz-feed-index", seln.currentIndex); aEvent.dataTransfer.effectAllowed = "copyMove"; }, onDragOver: function (aEvent) { this.mView._currentDataTransfer = aEvent.dataTransfer; }, mFeedDownloadCallback: { mSubscribeMode: true, downloaded: function(feed, aErrorCode) { // Offline check is done in the context of 3pane, return to the subscribe // window once the modal prompt is dispatched. window.focus(); // Feed is null if our attempt to parse the feed failed. let message = ""; let win = FeedSubscriptions; if (aErrorCode == FeedUtils.kNewsBlogSuccess || aErrorCode == FeedUtils.kNewsBlogNoNewItems) { win.updateStatusItem("progressMeter", 100); if (win.mActionMode == win.kVerifyUrlMode) { // Just checking for errors, if none bye. The (non error) code // kNewsBlogNoNewItems can only happen in verify mode. win.mActionMode = null; win.clearStatusInfo(); message = FeedUtils.strings.GetStringFromName("subscribe-feedVerified"); win.updateStatusItem("statusText", message); document.getElementById("editFeed").removeAttribute("disabled"); return; } // Add the feed to the databases. FeedUtils.addFeed(feed); // Now add the feed to our view. If adding, the current selection will // be a folder; if updating it will be a feed. No need to rebuild the // entire view, that is too jarring. let curIndex = win.mView.selection.currentIndex; let curItem = win.mView.getItemAtIndex(curIndex); if (curItem) { let parentIndex, parentItem, newItem, level; let rows = win.mFeedContainers; if (curItem.container) { // Open the container, if it exists. let folderExists = win.selectFolder(feed.folder, { parentIndex: curIndex }); if (!folderExists) { // This means a new folder was created. parentIndex = curIndex; parentItem = curItem; level = curItem.level + 1; newItem = win.makeFolderObject(feed.folder, level); } else { // If a folder happens to exist which matches one that would // have been created, the feed system reuses it. Get the // current item again if reusing a previously unselected folder. curIndex = win.mView.selection.currentIndex; curItem = win.mView.getItemAtIndex(curIndex); parentIndex = curIndex; parentItem = curItem; level = curItem.level + 1; newItem = win.makeFeedObject(feed, feed.folder, level); } } else { // Adding a feed. parentIndex = win.mView.getParentIndex(curIndex); parentItem = win.mView.getItemAtIndex(parentIndex); level = curItem.level; newItem = win.makeFeedObject(feed, feed.folder, level); } if (!newItem.container) win.updateFolderQuickModeInView(newItem, parentItem, false); parentItem.children.push(newItem); parentItem.children = win.folderItemSorter(parentItem.children); parentItem.favicon = null; if (win.mActionMode == win.kSubscribeMode) message = FeedUtils.strings.GetStringFromName( "subscribe-feedAdded"); if (win.mActionMode == win.kUpdateMode) { win.removeFeed(false); message = FeedUtils.strings.GetStringFromName( "subscribe-feedUpdated"); } if (win.mActionMode == win.kMoveMode) message = FeedUtils.strings.GetStringFromName( "subscribe-feedMoved"); if (win.mActionMode == win.kCopyMode) message = FeedUtils.strings.GetStringFromName( "subscribe-feedCopied"); win.selectFeed(feed, parentIndex); } } else { // Non success. Remove intermediate traces from the feeds database. // But only if we're not in verify mode. if (win.mActionMode != win.kVerifyUrlMode && feed && feed.url && feed.server) FeedUtils.deleteFeed(FeedUtils.rdf.GetResource(feed.url), feed.server, feed.server.rootFolder); if (aErrorCode == FeedUtils.kNewsBlogInvalidFeed) message = FeedUtils.strings.GetStringFromName( "subscribe-feedNotValid"); if (aErrorCode == FeedUtils.kNewsBlogRequestFailure) message = FeedUtils.strings.GetStringFromName( "subscribe-networkError"); if (aErrorCode == FeedUtils.kNewsBlogFileError) message = FeedUtils.strings.GetStringFromName( "subscribe-errorOpeningFile"); if (aErrorCode == FeedUtils.kNewsBlogBadCertError) { let host = Services.io.newURI(feed.url, null, null).host; message = FeedUtils.strings.formatStringFromName( "newsblog-badCertError", [host], 1); } if (aErrorCode == FeedUtils.kNewsBlogNoAuthError) message = FeedUtils.strings.GetStringFromName( "subscribe-noAuthError"); if (win.mActionMode != win.kUpdateMode && win.mActionMode != win.kVerifyUrlMode) // Re-enable the add button if subscribe failed. document.getElementById("addFeed").removeAttribute("disabled"); if (win.mActionMode == win.kVerifyUrlMode) // Re-enable the update button if verify failed. document.getElementById("editFeed").removeAttribute("disabled"); } win.mActionMode = null; win.clearStatusInfo(); let code = feed.url.startsWith("http") ? aErrorCode : null; win.updateStatusItem("statusText", message, code); }, // This gets called after the RSS parser finishes storing a feed item to // disk. aCurrentFeedItems is an integer corresponding to how many feed // items have been downloaded so far. aMaxFeedItems is an integer // corresponding to the total number of feed items to download. onFeedItemStored: function (feed, aCurrentFeedItems, aMaxFeedItems) { window.focus(); let message = FeedUtils.strings.formatStringFromName( "subscribe-gettingFeedItems", [aCurrentFeedItems, aMaxFeedItems], 2); FeedSubscriptions.updateStatusItem("statusText", message); this.onProgress(feed, aCurrentFeedItems, aMaxFeedItems); }, onProgress: function(feed, aProgress, aProgressMax, aLengthComputable) { FeedSubscriptions.updateStatusItem("progressMeter", (aProgress * 100) / aProgressMax); } }, // Status routines. updateStatusItem: function(aID, aValue, aErrorCode) { let el = document.getElementById(aID); if (el.getAttribute("collapsed")) el.removeAttribute("collapsed"); if (aID == "progressMeter") el.setAttribute("mode", aValue == "?" ? "undetermined" : "determined"); if (aID == "statusText") el.textContent = aValue; else el.value = aValue; el = document.getElementById("validationText"); if (aErrorCode == FeedUtils.kNewsBlogInvalidFeed) el.removeAttribute("collapsed"); else el.setAttribute("collapsed", true); el = document.getElementById("addCertException"); if (aErrorCode == FeedUtils.kNewsBlogBadCertError) el.removeAttribute("collapsed"); else el.setAttribute("collapsed", true); }, clearStatusInfo: function() { document.getElementById("statusText").textContent = ""; document.getElementById("progressMeter").collapsed = true; document.getElementById("validationText").collapsed = true; document.getElementById("addCertException").collapsed = true; }, checkValidation: function(aEvent) { if (aEvent.button != 0) return; let validationSite = "http://validator.w3.org"; let validationQuery = "http://validator.w3.org/feed/check.cgi?url="; if (this.mMainWin) { let tabmail = this.mMainWin.document.getElementById("tabmail"); if (tabmail) { let feedLocation = document.getElementById("locationValue").value; let url = validationQuery + encodeURIComponent(feedLocation); this.mMainWin.focus(); this.mMainWin.openContentTab(url, "tab", "^" + validationSite); FeedUtils.log.debug("checkValidation: query url - " + url); } } aEvent.stopPropagation(); }, addCertExceptionDialog: function() { let feedURL = document.getElementById("locationValue").value.trim(); let params = { exceptionAdded : false, location: feedURL, prefetchCert: true }; window.openDialog("chrome://pippki/content/exceptionDialog.xul", "", "chrome,centerscreen,modal", params); if (params.exceptionAdded) this.clearStatusInfo(); }, // Listener for folder pane changes. FolderListener: { get feedWindow() { let subscriptionsWindow = Services.wm.getMostRecentWindow("Mail:News-BlogSubscriptions"); return subscriptionsWindow ? subscriptionsWindow.FeedSubscriptions : null; }, get currentSelectedIndex() { return this.feedWindow ? this.feedWindow.mView.selection.currentIndex : -1; }, get currentSelectedItem() { return this.feedWindow ? this.feedWindow.mView.currentItem : null; }, folderAdded: function(aFolder) { if (aFolder.server.type != "rss" || FeedUtils.isInTrash(aFolder)) return; let parentFolder = aFolder.isServer ? aFolder : aFolder.parent; FeedUtils.log.debug("folderAdded: folder:parent - " + aFolder.name + ":" + (parentFolder ? parentFolder.filePath.path : "(null)")); if (!parentFolder || !this.feedWindow) return; let feedWindow = this.feedWindow; let curSelIndex = this.currentSelectedIndex; let curSelItem = this.currentSelectedItem; let firstVisRow = feedWindow.mView.treeBox.getFirstVisibleRow(); let indexInView = feedWindow.mView.getItemInViewIndex(parentFolder); let open = indexInView != null; if (aFolder.isServer) { if (indexInView != null) // Existing account root folder in the view. open = feedWindow.mView.getItemAtIndex(indexInView).open; else { // Add the account root folder to the view. feedWindow.mFeedContainers.push(feedWindow.makeFolderObject(parentFolder, 0)); feedWindow.mView.mRowCount++; feedWindow.mTree.view = feedWindow.mView; feedWindow.mView.treeBox.scrollToRow(firstVisRow); return; } } // Rebuild the added folder's parent item in the tree row cache. feedWindow.selectFolder(parentFolder, { select: false, open: open, newFolder: parentFolder }); if (indexInView == null || !curSelItem) // Folder isn't in the tree view, no need to update the view. return; let parentIndex = feedWindow.mView.getParentIndex(indexInView); if (parentIndex == feedWindow.mView.kRowIndexUndefined) // Root folder is its own parent. parentIndex = indexInView; if (open) { // Close an open parent (or root) folder. feedWindow.mView.toggle(parentIndex); feedWindow.mView.toggleOpenState(parentIndex); } feedWindow.mView.treeBox.scrollToRow(firstVisRow); if (curSelItem.container) feedWindow.selectFolder(curSelItem.folder, { open: curSelItem.open }); else feedWindow.selectFeed({ folder: curSelItem.parentFolder, url: curSelItem.url }, parentIndex); }, folderDeleted: function(aFolder) { if (aFolder.server.type != "rss" || FeedUtils.isInTrash(aFolder)) return; FeedUtils.log.debug("folderDeleted: folder - " + aFolder.name); if (!this.feedWindow) return; let feedWindow = this.feedWindow; let curSelIndex = this.currentSelectedIndex; let indexInView = feedWindow.mView.getItemInViewIndex(aFolder); let open = indexInView != null; // Delete the folder from the tree row cache. feedWindow.selectFolder(aFolder, { select: false, open: false, remove: true }); if (!open || curSelIndex < 0) // Folder isn't in the tree view, no need to update the view. return; let select = indexInView == curSelIndex || feedWindow.mView.isIndexChildOfParentIndex(indexInView, curSelIndex); feedWindow.mView.removeItemAtIndex(indexInView, !select); }, folderRenamed: function(aOrigFolder, aNewFolder) { if (aNewFolder.server.type != "rss" || FeedUtils.isInTrash(aNewFolder)) return; FeedUtils.log.debug("folderRenamed: old:new - " + aOrigFolder.name + ":" + aNewFolder.name); if (!this.feedWindow) return; let feedWindow = this.feedWindow; let curSelIndex = this.currentSelectedIndex; let curSelItem = this.currentSelectedItem; let firstVisRow = feedWindow.mView.treeBox.getFirstVisibleRow(); let indexInView = feedWindow.mView.getItemInViewIndex(aOrigFolder); let open = indexInView != null; // Rebuild the renamed folder's item in the tree row cache. feedWindow.selectFolder(aOrigFolder, { select: false, open: open, newFolder: aNewFolder }); if (!open || !curSelItem) // Folder isn't in the tree view, no need to update the view. return; let select = indexInView == curSelIndex || feedWindow.mView.isIndexChildOfParentIndex(indexInView, curSelIndex); let parentIndex = feedWindow.mView.getParentIndex(indexInView); if (parentIndex == feedWindow.mView.kRowIndexUndefined) // Root folder is its own parent. parentIndex = indexInView; feedWindow.mView.toggle(parentIndex); feedWindow.mView.toggleOpenState(parentIndex); feedWindow.mView.treeBox.scrollToRow(firstVisRow); if (curSelItem.container) { if (curSelItem.folder == aOrigFolder) feedWindow.selectFolder(aNewFolder, { open: curSelItem.open }); else if (select) feedWindow.mView.selection.select(indexInView); else feedWindow.selectFolder(curSelItem.folder, { open: curSelItem.open }); } else feedWindow.selectFeed({ folder: curSelItem.parentFolder.rootFolder, url: curSelItem.url }, parentIndex); }, folderMoveCopyCompleted: function(aMove, aSrcFolder, aDestFolder) { if (aDestFolder.server.type != "rss") return; FeedUtils.log.debug("folderMoveCopyCompleted: move:src:dest - " + aMove + ":" + aSrcFolder.name + ":" + aDestFolder.name); if (!this.feedWindow) return; let feedWindow = this.feedWindow; let curSelIndex = this.currentSelectedIndex; let curSelItem = this.currentSelectedItem; let firstVisRow = feedWindow.mView.treeBox.getFirstVisibleRow(); let indexInView = feedWindow.mView.getItemInViewIndex(aSrcFolder); let destIndexInView = feedWindow.mView.getItemInViewIndex(aDestFolder); let open = indexInView != null || destIndexInView != null; let parentIndex = feedWindow.mView.getItemInViewIndex(aDestFolder.parent || aDestFolder); let select = indexInView == curSelIndex || feedWindow.mView.isIndexChildOfParentIndex(indexInView, curSelIndex); if (aMove) { this.folderDeleted(aSrcFolder); if (aDestFolder.getFlag(Ci.nsMsgFolderFlags.Trash)) return; } setTimeout(function() { // State on disk needs to settle before a folder object can be rebuilt. feedWindow.selectFolder(aDestFolder, { select: false, open: open || select, newFolder: aDestFolder }); if (!open || !curSelItem) // Folder isn't in the tree view, no need to update the view. return; feedWindow.mView.toggle(parentIndex); feedWindow.mView.toggleOpenState(parentIndex); feedWindow.mView.treeBox.scrollToRow(firstVisRow); if (curSelItem.container) { if (curSelItem.folder == aSrcFolder || select) feedWindow.selectFolder(aDestFolder, { open: true }); else feedWindow.selectFolder(curSelItem.folder, { open: curSelItem.open }); } else feedWindow.selectFeed({ folder: curSelItem.parentFolder.rootFolder, url: curSelItem.url }, null); }, 50); } }, /* *************************************************************** */ /* OPML Functions */ /* *************************************************************** */ get brandShortName() { let brandBundle = document.getElementById("bundle_brand"); return brandBundle ? brandBundle.getString("brandShortName") : ""; }, /** * Export feeds as opml file Save As filepicker function. * * @param bool aList - if true, exporting as list; if false (default) * exporting feeds in folder structure - used for title. * @return nsILocalFile or null. */ opmlPickSaveAsFile: function(aList) { let accountName = this.mRSSServer.rootFolder.prettyName; let fileName = FeedUtils.strings.formatStringFromName( "subscribe-OPMLExportDefaultFileName", [this.brandShortName, accountName], 2); let title = aList ? FeedUtils.strings.formatStringFromName( "subscribe-OPMLExportTitleList", [accountName], 1) : FeedUtils.strings.formatStringFromName( "subscribe-OPMLExportTitleStruct", [accountName], 1); let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); fp.defaultString = fileName; fp.defaultExtension = "opml"; if (this.opmlLastSaveAsDir && (this.opmlLastSaveAsDir instanceof Ci.nsILocalFile)) fp.displayDirectory = this.opmlLastSaveAsDir; let opmlFilterText = FeedUtils.strings.GetStringFromName( "subscribe-OPMLExportOPMLFilesFilterText"); fp.appendFilter(opmlFilterText, "*.opml"); fp.appendFilters(Ci.nsIFilePicker.filterAll); fp.filterIndex = 0; fp.init(window, title, Ci.nsIFilePicker.modeSave); if (fp.show() != Ci.nsIFilePicker.returnCancel && fp.file) { this.opmlLastSaveAsDir = fp.file.parent; return fp.file; } return null; }, /** * Import feeds opml file Open filepicker function. * * @return nsILocalFile or null. */ opmlPickOpenFile: function() { let title = FeedUtils.strings.GetStringFromName("subscribe-OPMLImportTitle"); let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); fp.defaultString = ""; if (this.opmlLastOpenDir && (this.opmlLastOpenDir instanceof Ci.nsILocalFile)) fp.displayDirectory = this.opmlLastOpenDir; let opmlFilterText = FeedUtils.strings.GetStringFromName( "subscribe-OPMLExportOPMLFilesFilterText"); fp.appendFilter(opmlFilterText, "*.opml"); fp.appendFilters(Ci.nsIFilePicker.filterXML); fp.appendFilters(Ci.nsIFilePicker.filterAll); fp.init(window, title, Ci.nsIFilePicker.modeOpen); if (fp.show() != Ci.nsIFilePicker.returnCancel && fp.file) { this.opmlLastOpenDir = fp.file.parent; return fp.file; } return null; }, exportOPML: function(aEvent) { // Account folder must be selected. let item = this.mView.currentItem; if (!item || !item.folder || !item.folder.isServer) return; this.mRSSServer = item.folder.server; let rootFolder = this.mRSSServer.rootFolder; let exportAsList = aEvent.ctrlKey; let SPACES2 = " "; let SPACES4 = " "; if (this.mRSSServer.rootFolder.hasSubFolders) { let opmlDoc = document.implementation.createDocument("", "opml", null); let opmlRoot = opmlDoc.documentElement; opmlRoot.setAttribute("version", "1.0"); opmlRoot.setAttribute("xmlns:fz", "urn:forumzilla:"); this.generatePPSpace(opmlRoot, SPACES2); // Make the element. let head = opmlDoc.createElement("head"); this.generatePPSpace(head, SPACES4); let titleText = FeedUtils.strings.formatStringFromName( "subscribe-OPMLExportFileDialogTitle", [this.brandShortName, rootFolder.prettyName], 2); let title = opmlDoc.createElement("title"); title.appendChild(opmlDoc.createTextNode(titleText)); head.appendChild(title); this.generatePPSpace(head, SPACES4); let dt = opmlDoc.createElement("dateCreated"); dt.appendChild(opmlDoc.createTextNode((new Date()).toUTCString())); head.appendChild(dt); this.generatePPSpace(head, SPACES2); opmlRoot.appendChild(head); this.generatePPSpace(opmlRoot, SPACES2); // Add s to the . let body = opmlDoc.createElement("body"); if (exportAsList) this.generateOutlineList(rootFolder, body, SPACES4.length + 2); else this.generateOutlineStruct(rootFolder, body, SPACES4.length); this.generatePPSpace(body, SPACES2); if (!body.childElementCount) // No folders/feeds. return; opmlRoot.appendChild(body); this.generatePPSpace(opmlRoot, ""); let serializer = new XMLSerializer(); if (FeedUtils.log.level <= Log4Moz.Level.Debug) FeedUtils.log.debug("exportOPML: opmlDoc -\n" + serializer.serializeToString(opmlDoc) + "\n"); // Get file to save from filepicker. let saveAsFile = this.opmlPickSaveAsFile(exportAsList); if (!saveAsFile) return; let fos = FileUtils.openSafeFileOutputStream(saveAsFile); serializer.serializeToStream(opmlDoc, fos, "utf-8"); FileUtils.closeSafeFileOutputStream(fos); let statusReport = FeedUtils.strings.formatStringFromName( "subscribe-OPMLExportDone", [saveAsFile.path], 1); this.updateStatusItem("statusText", statusReport); } }, generatePPSpace: function(aNode, indentString) { aNode.appendChild(aNode.ownerDocument.createTextNode("\n")); aNode.appendChild(aNode.ownerDocument.createTextNode(indentString)); }, generateOutlineList: function(baseFolder, parent, indentLevel) { // Pretty printing. let indentString = " ".repeat(indentLevel - 2); let feedOutline; let folderEnumerator = baseFolder.subFolders; while (folderEnumerator.hasMoreElements()) { let folder = folderEnumerator.getNext().QueryInterface(Ci.nsIMsgFolder); FeedUtils.log.debug("generateOutlineList: folder - " + folder.filePath.path); if (!(folder instanceof Ci.nsIMsgFolder) || folder.getFlag(Ci.nsMsgFolderFlags.Trash) || folder.getFlag(Ci.nsMsgFolderFlags.Virtual)) continue; FeedUtils.log.debug("generateOutlineList: CONTINUE folderName - " + folder.name); if (folder.hasSubFolders) { FeedUtils.log.debug("generateOutlineList: has subfolders - " + folder.name); // Recurse. this.generateOutlineList(folder, parent, indentLevel); } // Add outline elements with xmlUrls. let feeds = this.getFeedsInFolder(folder); for (let feed of feeds) { FeedUtils.log.debug("generateOutlineList: folder has FEED url - " + folder.name + " : " + feed.url); feedOutline = this.exportOPMLOutline(feed, parent.ownerDocument); this.generatePPSpace(parent, indentString); parent.appendChild(feedOutline); } } }, generateOutlineStruct: function(baseFolder, parent, indentLevel) { // Pretty printing. function indentString(len) { return " ".repeat(len - 2); }; let folderOutline, feedOutline; let folderEnumerator = baseFolder.subFolders; while (folderEnumerator.hasMoreElements()) { let folder = folderEnumerator.getNext().QueryInterface(Ci.nsIMsgFolder); FeedUtils.log.debug("generateOutlineStruct: folder - " + folder.filePath.path); if (!(folder instanceof Ci.nsIMsgFolder) || folder.getFlag(Ci.nsMsgFolderFlags.Trash) || folder.getFlag(Ci.nsMsgFolderFlags.Virtual)) continue; FeedUtils.log.debug("generateOutlineStruct: CONTINUE folderName - " + folder.name); // Make a folder outline element. folderOutline = parent.ownerDocument.createElement("outline"); folderOutline.setAttribute("title", folder.prettyName); this.generatePPSpace(parent, indentString(indentLevel + 2)); if (folder.hasSubFolders) { FeedUtils.log.debug("generateOutlineStruct: has subfolders - " + folder.name); // Recurse. this.generateOutlineStruct(folder, folderOutline, indentLevel + 2); } let feeds = this.getFeedsInFolder(folder); for (let feed of feeds) { // Add feed outline elements with xmlUrls. FeedUtils.log.debug("generateOutlineStruct: folder has FEED url - "+ folder.name + " : " + feed.url); feedOutline = this.exportOPMLOutline(feed, parent.ownerDocument); this.generatePPSpace(folderOutline, indentString(indentLevel + 4)); folderOutline.appendChild(feedOutline); } parent.appendChild(folderOutline); } }, exportOPMLOutline: function(aFeed, aDoc) { let outRv = aDoc.createElement("outline"); outRv.setAttribute("type", "rss"); outRv.setAttribute("title", aFeed.title); outRv.setAttribute("text", aFeed.title); outRv.setAttribute("version", "RSS"); outRv.setAttribute("fz:quickMode", aFeed.quickMode); outRv.setAttribute("fz:options", JSON.stringify(aFeed.options)); outRv.setAttribute("xmlUrl", aFeed.url); outRv.setAttribute("htmlUrl", aFeed.link); return outRv; }, importOPML: function() { // Account folder must be selected in subscribe dialog. let item = this.mView ? this.mView.currentItem : null; if (!item || !item.folder || !item.folder.isServer) return; let server = item.folder.server; // Get file to open from filepicker. let openFile = this.opmlPickOpenFile(); if (!openFile) return; this.mActionMode = this.kImportingOPML; this.updateButtons(null); this.selectFolder(item.folder, { select: false, open: true }); let statusReport = FeedUtils.strings.GetStringFromName("subscribe-loading"); this.updateStatusItem("statusText", statusReport); // If there were a getElementsByAttribute in html, we could go determined... this.updateStatusItem("progressMeter", "?"); if (!this.importOPMLFile(openFile, server, this.importOPMLFinished)) { this.mActionMode = null; this.updateButtons(item); this.clearStatusInfo(); } }, /** * Import opml file into a feed account. Used by the Subscribe dialog and * the Import wizard. * * @param nsILocalFile aFile - the opml file. * @param nsIMsgIncomingServer aServer - the account server. * @param func aCallback - callback function. * * @return bool - false if error. */ importOPMLFile: function(aFile, aServer, aCallback) { if (aServer && (aServer instanceof Ci.nsIMsgIncomingServer)) this.mRSSServer = aServer; if (!aFile || !this.mRSSServer || !aCallback) return false; let opmlDom, statusReport; let stream = Cc["@mozilla.org/network/file-input-stream;1"]. createInstance(Ci.nsIFileInputStream); // Read in file as raw bytes, so Expat can do the decoding for us. try { stream.init(aFile, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0); let parser = new DOMParser(); opmlDom = parser.parseFromStream(stream, null, stream.available(), "application/xml"); } catch(e) { statusReport = FeedUtils.strings.GetStringFromName( "subscribe-errorOpeningFile"); Services.prompt.alert(window, null, statusReport); return false; } finally { stream.close(); } let body = opmlDom ? opmlDom.querySelector("body") : null; // Return if the OPML file is invalid or empty. if (!body || !body.childElementCount || opmlDom.documentElement.tagName != "opml") { statusReport = FeedUtils.strings.formatStringFromName( "subscribe-OPMLImportInvalidFile", [aFile.leafName], 1); Services.prompt.alert(window, null, statusReport); return false; } this.importOPMLOutlines(body, this.mRSSServer, aCallback); return true; }, importOPMLOutlines: function(aBody, aRSSServer, aCallback) { let win = this; let rssServer = aRSSServer; let callback = aCallback; let outline, feedFolder; let badTag = false; let firstFeedInFolderQuickMode = null; let lastFolder; let feedsAdded = 0; let rssOutlines = 0; let folderOutlines = 0; function processor(aParentNode, aParentFolder) { FeedUtils.log.trace("importOPMLOutlines: PROCESSOR tag:name:childs - " + aParentNode.tagName + ":" + aParentNode.getAttribute("text") + ":" + aParentNode.childElementCount); while (true) { if (aParentNode.tagName == "body" && !aParentNode.childElementCount) { // Finished. let statusReport = win.importOPMLStatus(feedsAdded, rssOutlines); callback(statusReport, lastFolder, win); return; } outline = aParentNode.firstElementChild; if (outline.tagName != "outline") { FeedUtils.log.info("importOPMLOutlines: skipping, node is not an " + " - <" + outline.tagName + ">"); badTag = true; break; } let outlineName = outline.getAttribute("text") || outline.getAttribute("title") || outline.getAttribute("xmlUrl"); let feedUrl, folderURI; if (outline.getAttribute("type") == "rss") { // A feed outline. feedUrl = outline.getAttribute("xmlUrl") || outline.getAttribute("url"); if (!feedUrl) { FeedUtils.log.info("importOPMLOutlines: skipping, type=rss " + "has no url - " + outlineName); break; } rssOutlines++; feedFolder = aParentFolder; if (FeedUtils.feedAlreadyExists(feedUrl, rssServer)) { FeedUtils.log.info("importOPMLOutlines: feed already subscribed in account " + rssServer.prettyName + ", url - " + feedUrl); break; } if (aParentNode.tagName == "outline" && aParentNode.getAttribute("type") != "rss") // Parent is a folder, already created. folderURI = feedFolder.URI; else { // Parent is not a folder outline, likely the in a flat list. // Create feed's folder with feed's name and account rootFolder as // parent of feed's folder. // NOTE: Assume a type=rss outline must be a leaf and is not a // direct parent of another type=rss outline; such a structure // may lead to unintended nesting and inaccurate counts. } // Create the feed. let quickMode = outline.hasAttribute("fz:quickMode") ? outline.getAttribute("fz:quickMode") == "true" : rssServer.getBoolValue("quickMode"); let options = outline.getAttribute("fz:options"); options = options ? JSON.parse(options) : null; if (firstFeedInFolderQuickMode === null) // The summary/web page pref applies to all feeds in a folder, // though it is a property of an individual feed. This can be // set (and is obvious) in the subscribe dialog; ensure import // doesn't leave mismatches if mismatched in the opml file. firstFeedInFolderQuickMode = quickMode; else quickMode = firstFeedInFolderQuickMode; let feedProperties = { feedName : outlineName, feedLocation : feedUrl, server : rssServer, folderURI : folderURI, quickMode : quickMode, options : options }; FeedUtils.log.info("importOPMLOutlines: importing feed: name, url - "+ outlineName + ", " + feedUrl); let feed = win.storeFeed(feedProperties); if (outline.hasAttribute("htmlUrl")) feed.link = outline.getAttribute("htmlUrl"); feed.createFolder(); if (!feed.folder) { // Non success. Remove intermediate traces from the feeds database. if (feed && feed.url && feed.server) FeedUtils.deleteFeed(FeedUtils.rdf.GetResource(feed.url), feed.server, feed.server.rootFolder); FeedUtils.log.info("importOPMLOutlines: skipping, error creating folder - '" + feed.folderName + "' from outlineName - '" + outlineName + "' in parent folder " + aParentFolder.filePath.path); badTag = true; break; } // Add the feed to the databases. FeedUtils.addFeed(feed); // Feed correctly added. feedsAdded++; lastFolder = feed.folder; } else { // A folder outline. If a folder exists in the account structure at // the same level as in the opml structure, feeds are placed into the // existing folder. let defaultName = FeedUtils.strings.GetStringFromName("ImportFeedsNew"); let folderName = FeedUtils.getSanitizedFolderName(aParentFolder, outlineName, defaultName, false); try { feedFolder = aParentFolder.getChildNamed(folderName); } catch (ex) { // Folder not found, create it. FeedUtils.log.info("importOPMLOutlines: creating folder - '" + folderName + "' from outlineName - '" + outlineName + "' in parent folder " + aParentFolder.filePath.path); firstFeedInFolderQuickMode = null; try { feedFolder = aParentFolder.QueryInterface(Ci.nsIMsgLocalMailFolder). createLocalSubfolder(folderName); folderOutlines++; } catch (ex) { // An error creating. Skip it. FeedUtils.log.info("importOPMLOutlines: skipping, error creating folder - '" + folderName + "' from outlineName - '" + outlineName + "' in parent folder " + aParentFolder.filePath.path); let xfolder = aParentFolder.getChildNamed(folderName); aParentFolder.propagateDelete(xfolder, true, null); badTag = true; break; } } } break; } if (!outline.childElementCount || badTag) { // Remove leaf nodes that are processed or bad tags from the opml dom, // and go back to reparse. This method lets us use setTimeout to // prevent UI hang, in situations of both deep and shallow trees. // A yield/generator.next() method is fine for shallow trees, but not // the true recursion required for deeper trees; both the shallow loop // and the recurse should give it up. outline.remove(); badTag = false; outline = aBody; feedFolder = rssServer.rootFolder; } setTimeout(function() { processor(outline, feedFolder); }, 0); } processor(aBody, rssServer.rootFolder); }, importOPMLStatus: function(aFeedsAdded, aRssOutlines, aFolderOutlines) { let statusReport; if (aRssOutlines > aFeedsAdded) statusReport = FeedUtils.strings.formatStringFromName("subscribe-OPMLImportStatus", [PluralForm.get(aFeedsAdded, FeedUtils.strings.GetStringFromName("subscribe-OPMLImportUniqueFeeds")) .replace("#1", aFeedsAdded), PluralForm.get(aRssOutlines, FeedUtils.strings.GetStringFromName("subscribe-OPMLImportFoundFeeds")) .replace("#1", aRssOutlines)], 2); else statusReport = PluralForm.get(aFeedsAdded, FeedUtils.strings.GetStringFromName("subscribe-OPMLImportFeedCount")) .replace("#1", aFeedsAdded); return statusReport; }, importOPMLFinished: function(aStatusReport, aLastFolder, aWin) { if (aLastFolder) { aWin.selectFolder(aLastFolder, { select: false, newFolder: aLastFolder }); aWin.selectFolder(aLastFolder.parent); } aWin.mActionMode = null; aWin.updateButtons(aWin.mView.currentItem); aWin.clearStatusInfo(); aWin.updateStatusItem("statusText", aStatusReport); } };