diff options
Diffstat (limited to 'mailnews/extensions/newsblog/content/feed-subscriptions.js')
-rw-r--r-- | mailnews/extensions/newsblog/content/feed-subscriptions.js | 2703 |
1 files changed, 2703 insertions, 0 deletions
diff --git a/mailnews/extensions/newsblog/content/feed-subscriptions.js b/mailnews/extensions/newsblog/content/feed-subscriptions.js new file mode 100644 index 000000000..2b77e60c4 --- /dev/null +++ b/mailnews/extensions/newsblog/content/feed-subscriptions.js @@ -0,0 +1,2703 @@ +/* -*- 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 <head> 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 <outline>s to the <body>. + 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> - <" + 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 <outline> " + + "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 <body> 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); + } + +}; |