| /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- |
| * ***** BEGIN LICENSE BLOCK ***** |
| * Version: MPL 1.1/GPL 2.0/LGPL 2.1 |
| * |
| * The contents of this file are subject to the Mozilla Public License |
| * Version 1.1 (the "License"); you may not use this file except in |
| * compliance with the License. You may obtain a copy of the License |
| * at http://www.mozilla.org/MPL/ |
| * |
| * Software distributed under the License is distributed on an "AS IS" |
| * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See |
| * the License for the specific language governing rights and |
| * limitations under the License. |
| * |
| * The Original Code is the Places Tagging Service. |
| * |
| * The Initial Developer of the Original Code is |
| * Mozilla Corporation. |
| * Portions created by the Initial Developer are Copyright (C) 2007 |
| * the Initial Developer. All Rights Reserved. |
| * |
| * Contributor(s): |
| * Asaf Romano <mano@mozilla.com> (Original Author) |
| * |
| * Alternatively, the contents of this file may be used under the terms of |
| * either the GNU General Public License Version 2 or later (the "GPL"), or |
| * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), |
| * in which case the provisions of the GPL or the LGPL are applicable instead |
| * of those above. If you wish to allow use of your version of this file only |
| * under the terms of either the GPL or the LGPL, and not to allow others to |
| * use your version of this file under the terms of the MPL, indicate your |
| * decision by deleting the provisions above and replace them with the notice |
| * and other provisions required by the GPL or the LGPL. If you do not delete |
| * the provisions above, a recipient may use your version of this file under |
| * the terms of any one of the MPL, the GPL or the LGPL. |
| * |
| * ***** END LICENSE BLOCK ***** */ |
| |
| const Cc = Components.classes; |
| const Ci = Components.interfaces; |
| const Cr = Components.results; |
| |
| Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); |
| |
| const NH_CONTRACTID = "@mozilla.org/browser/nav-history-service;1"; |
| const BMS_CONTRACTID = "@mozilla.org/browser/nav-bookmarks-service;1"; |
| const IO_CONTRACTID = "@mozilla.org/network/io-service;1"; |
| const ANNO_CONTRACTID = "@mozilla.org/browser/annotation-service;1"; |
| const FAV_CONTRACTID = "@mozilla.org/browser/favicon-service;1"; |
| const OBSS_CONTRACTID = "@mozilla.org/observer-service;1"; |
| |
| var gIoService = Cc[IO_CONTRACTID].getService(Ci.nsIIOService); |
| |
| /** |
| * The Places Tagging Service |
| */ |
| function TaggingService() { |
| } |
| |
| TaggingService.prototype = { |
| get _bms() { |
| if (!this.__bms) |
| this.__bms = Cc[BMS_CONTRACTID].getService(Ci.nsINavBookmarksService); |
| return this.__bms; |
| }, |
| |
| get _history() { |
| if (!this.__history) |
| this.__history = Cc[NH_CONTRACTID].getService(Ci.nsINavHistoryService); |
| return this.__history; |
| }, |
| |
| get _annos() { |
| if (!this.__annos) |
| this.__annos = Cc[ANNO_CONTRACTID].getService(Ci.nsIAnnotationService); |
| return this.__annos; |
| }, |
| |
| get _tagsResult() { |
| if (!this.__tagsResult) { |
| var options = this._history.getNewQueryOptions(); |
| var query = this._history.getNewQuery(); |
| query.setFolders([this._bms.tagsFolder], 1); |
| this.__tagsResult = this._history.executeQuery(query, options); |
| this.__tagsResult.root.containerOpen = true; |
| |
| // we need to null out the result on shutdown |
| var observerSvc = Cc[OBSS_CONTRACTID].getService(Ci.nsIObserverService); |
| observerSvc.addObserver(this, "xpcom-shutdown", false); |
| } |
| return this.__tagsResult; |
| }, |
| |
| // Feed XPCOMUtils |
| classDescription: "Places Tagging Service", |
| contractID: "@mozilla.org/browser/tagging-service;1", |
| classID: Components.ID("{bbc23860-2553-479d-8b78-94d9038334f7}"), |
| |
| // nsISupports |
| QueryInterface: XPCOMUtils.generateQI([Ci.nsITaggingService, |
| Ci.nsIObserver]), |
| |
| /** |
| * If there's no tag with the given name, null is returned; |
| */ |
| _getTagNode: function TS__getTagIndex(aTagNameOrId) { |
| if (!aTagNameOrId) |
| throw Cr.NS_ERROR_INVALID_ARG; |
| |
| var nameLower = null; |
| if (typeof(aTagNameOrId) == "string") |
| nameLower = aTagNameOrId.toLowerCase(); |
| |
| var root = this._tagsResult.root; |
| var cc = root.childCount; |
| for (var i=0; i < cc; i++) { |
| var child = root.getChild(i); |
| if ((nameLower && child.title.toLowerCase() == nameLower) || |
| child.itemId === aTagNameOrId) |
| return child; |
| } |
| |
| return null; |
| }, |
| |
| /** |
| * Creates a tag container under the tags-root with the given name. |
| * |
| * @param aName |
| * the name for the new container. |
| * @returns the id of the new container. |
| */ |
| _createTag: function TS__createTag(aName) { |
| return this._bms.createFolder(this._bms.tagsFolder, aName, |
| this._bms.DEFAULT_INDEX); |
| }, |
| |
| /** |
| * Checks whether the given uri is tagged with the given tag. |
| * |
| * @param [in] aURI |
| * url to check for |
| * @param [in] aTagId |
| * id of the folder representing the tag to check |
| * @param [out] aItemId |
| * the id of the item found under the tag container |
| * @returns true if the given uri is tagged with the given tag, false |
| * otherwise. |
| */ |
| _isURITaggedInternal: function TS__uriTagged(aURI, aTagId, aItemId) { |
| var bookmarkIds = this._bms.getBookmarkIdsForURI(aURI, {}); |
| for (var i=0; i < bookmarkIds.length; i++) { |
| var parent = this._bms.getFolderIdForItem(bookmarkIds[i]); |
| if (parent == aTagId) { |
| aItemId.value = bookmarkIds[i]; |
| return true; |
| } |
| } |
| return false; |
| }, |
| |
| // nsITaggingService |
| tagURI: function TS_tagURI(aURI, aTags) { |
| if (!aURI || !aTags) |
| throw Cr.NS_ERROR_INVALID_ARG; |
| |
| for (var i=0; i < aTags.length; i++) { |
| var tagNode = this._getTagNode(aTags[i]); |
| if (!tagNode) { |
| if (typeof(aTags[i]) == "number") |
| throw Cr.NS_ERROR_INVALID_ARG; |
| |
| var tagId = this._createTag(aTags[i]); |
| this._bms.insertBookmark(tagId, aURI, this._bms.DEFAULT_INDEX, null); |
| } |
| else { |
| var tagId = tagNode.itemId; |
| if (!this._isURITaggedInternal(aURI, tagNode.itemId, {})) |
| this._bms.insertBookmark(tagId, aURI, this._bms.DEFAULT_INDEX, null); |
| |
| // _getTagNode ignores case sensitivity |
| // rename the tag container so the places view would match the |
| // user-typed values |
| if (typeof(aTags[i]) == "string" && tagNode.title != aTags[i]) |
| this._bms.setItemTitle(tagNode.itemId, aTags[i]); |
| } |
| } |
| }, |
| |
| /** |
| * Removes the tag container from the tags-root if the given tag is empty. |
| * |
| * @param aTagId |
| * the item-id of the tag element under the tags root |
| */ |
| _removeTagIfEmpty: function TS__removeTagIfEmpty(aTagId) { |
| var node = this._getTagNode(aTagId).QueryInterface(Ci.nsINavHistoryContainerResultNode); |
| var wasOpen = node.containerOpen; |
| if (!wasOpen) |
| node.containerOpen = true; |
| var cc = node.childCount; |
| if (wasOpen) |
| node.containerOpen = false; |
| if (cc == 0) { |
| this._bms.removeFolder(node.itemId); |
| } |
| }, |
| |
| // nsITaggingService |
| untagURI: function TS_untagURI(aURI, aTags) { |
| if (!aURI) |
| throw Cr.NS_ERROR_INVALID_ARG; |
| |
| if (!aTags) { |
| // see IDL. |
| // XXXmano: write a perf-sensitive version of this code path... |
| aTags = this.getTagsForURI(aURI, { }); |
| } |
| |
| for (var i=0; i < aTags.length; i++) { |
| var tagNode = this._getTagNode(aTags[i]); |
| if (tagNode) { |
| var itemId = { }; |
| if (this._isURITaggedInternal(aURI, tagNode.itemId, itemId)) { |
| this._bms.removeItem(itemId.value); |
| this._removeTagIfEmpty(tagNode.itemId); |
| } |
| } |
| else if (typeof(aTags[i]) == "number") |
| throw Cr.NS_ERROR_INVALID_ARG; |
| } |
| }, |
| |
| // nsITaggingService |
| getURIsForTag: function TS_getURIsForTag(aTag) { |
| if (aTag.length == 0) |
| throw Cr.NS_ERROR_INVALID_ARG; |
| |
| var uris = []; |
| var tagNode = this._getTagNode(aTag); |
| if (tagNode) { |
| tagNode.QueryInterface(Ci.nsINavHistoryContainerResultNode); |
| tagNode.containerOpen = true; |
| var cc = tagNode.childCount; |
| for (var i = 0; i < cc; i++) { |
| try { |
| uris.push(gIoService.newURI(tagNode.getChild(i).uri, null, null)); |
| } catch (ex) { |
| // This is an invalid node, tags should only contain valid uri nodes. |
| // continue to next node. |
| } |
| } |
| tagNode.containerOpen = false; |
| } |
| return uris; |
| }, |
| |
| // nsITaggingService |
| getTagsForURI: function TS_getTagsForURI(aURI, aCount) { |
| if (!aURI) |
| throw Cr.NS_ERROR_INVALID_ARG; |
| |
| var tags = []; |
| var bookmarkIds = this._bms.getBookmarkIdsForURI(aURI, {}); |
| var root = this._tagsResult.root; |
| var cc = root.childCount; |
| for (var i=0; i < bookmarkIds.length; i++) { |
| var parent = this._bms.getFolderIdForItem(bookmarkIds[i]); |
| for (var j=0; j < cc; j++) { |
| var child = root.getChild(j); |
| if (child.itemId == parent) |
| tags.push(child.title); |
| } |
| } |
| |
| // sort the tag list |
| tags.sort(); |
| aCount.value = tags.length; |
| return tags; |
| }, |
| |
| // nsITaggingService |
| get allTags() { |
| var tags = []; |
| var root = this._tagsResult.root; |
| var cc = root.childCount; |
| for (var j=0; j < cc; j++) { |
| var child = root.getChild(j); |
| tags.push(child.title); |
| } |
| |
| // sort the tag list |
| tags.sort(); |
| return tags; |
| }, |
| |
| // nsIObserver |
| observe: function TS_observe(subject, topic, data) { |
| if (topic == "xpcom-shutdown") { |
| this.__tagsResult.root.containerOpen = false; |
| this.__tagsResult = null; |
| var observerSvc = Cc[OBSS_CONTRACTID].getService(Ci.nsIObserverService); |
| observerSvc.removeObserver(this, "xpcom-shutdown"); |
| } |
| } |
| }; |
| |
| function NSGetModule(compMgr, fileSpec) { |
| return XPCOMUtils.generateModule([TaggingService]); |
| } |