Bug 383803 - Places Tagging Back-end. r=dietrich.
authormozilla.mano@sent.com
Mon, 11 Jun 2007 22:18:50 -0700
changeset 2279 d305170d490e89d506c617e04afab401a11e7913
parent 2278 36dd65edea4242634a8e07bb700ad75a1d33dc99
child 2280 88da73a1c03a97e7cb3c8d581c3753bccdf039b8
push idunknown
push userunknown
push dateunknown
reviewersdietrich
bugs383803
milestone1.9a6pre
Bug 383803 - Places Tagging Back-end. r=dietrich.
browser/installer/unix/packages-static
browser/installer/windows/packages-static
toolkit/components/places/public/Makefile.in
toolkit/components/places/public/nsITaggingService.idl
toolkit/components/places/src/Makefile.in
toolkit/components/places/src/nsNavHistory.cpp
toolkit/components/places/src/nsNavHistory.h
toolkit/components/places/src/nsTaggingService.js
toolkit/components/places/tests/unit/test_tagging.js
toolkit/themes/pinstripe/mozapps/jar.mn
toolkit/themes/pinstripe/mozapps/places/tagContainerIcon.png
toolkit/themes/winstripe/mozapps/jar.mn
toolkit/themes/winstripe/mozapps/places/tagContainerIcon.png
--- a/browser/installer/unix/packages-static
+++ b/browser/installer/unix/packages-static
@@ -222,16 +222,17 @@ bin/components/nsSessionStartup.js
 bin/components/nsSessionStore.js
 bin/components/sessionstore.xpt
 bin/components/nsURLFormatter.js
 bin/components/urlformatter.xpt
 bin/components/libbrowserdirprovider.so
 bin/components/libbrowsercomps.so
 bin/components/txEXSLTRegExFunctions.js
 bin/components/nsLivemarkService.js
+bin/components/nsTaggingService.js
 bin/components/nsDefaultCLH.js
 
 ; Safe Browsing
 bin/components/nsSafebrowsingApplication.js
 bin/components/safebrowsing.xpt
 bin/components/nsUrlClassifierListManager.js
 bin/components/nsUrlClassifierLib.js
 bin/components/nsUrlClassifierTable.js
--- a/browser/installer/windows/packages-static
+++ b/browser/installer/windows/packages-static
@@ -222,16 +222,17 @@ bin\components\nsSessionStartup.js
 bin\components\nsSessionStore.js
 bin\components\sessionstore.xpt
 bin\components\nsURLFormatter.js
 bin\components\urlformatter.xpt
 bin\components\browserdirprovider.dll
 bin\components\brwsrcmp.dll
 bin\components\txEXSLTRegExFunctions.js
 bin\components\nsLivemarkService.js
+bin\components\nsTaggingService.js
 bin\components\nsDefaultCLH.js
 
 ; Safe Browsing
 bin\components\nsSafebrowsingApplication.js
 bin\components\safebrowsing.xpt
 bin\components\nsUrlClassifierListManager.js
 bin\components\nsUrlClassifierLib.js
 bin\components\nsUrlClassifierTable.js
--- a/toolkit/components/places/public/Makefile.in
+++ b/toolkit/components/places/public/Makefile.in
@@ -48,11 +48,12 @@ XPIDL_MODULE = places
 
 XPIDLSRCS  = \
   nsIAnnotationService.idl \
   nsIFaviconService.idl \
   nsINavHistoryService.idl \
   nsINavBookmarksService.idl \
   nsILivemarkService.idl \
   nsIRemoteContainer.idl \
+  nsITaggingService.idl  \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/public/nsITaggingService.idl
@@ -0,0 +1,90 @@
+/* -*- Mode: C++; tab-width: 2; 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 Places Tagging Service code.
+ *
+ * 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>
+ *
+ * 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 ***** */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+interface nsIVariant;
+
+[scriptable, uuid(962bf94f-e719-4cf6-8967-74ff5d2020df)]
+interface nsITaggingService : nsISupports
+{
+  /**
+   * Tags a URL with the given set of tags. Current tags set for the URL
+   * persist. Tags in aTags which are already set for the given URL are
+   * ignored.
+   *
+   * @param aURI
+   *        the URL to tag.
+   * @param aTags
+   *        Array of tags to set for the given URL.
+   */
+  void tagURI(in nsIURI aURI,
+              [const, array, size_is(aCount)] in wstring aTags,
+              in unsigned long aCount);
+
+  /**
+   * Removes tags from a URL. Tags from aTags which are not set for the
+   * given URL are ignored.
+   *
+   * @param aURI
+   *        the URL to un-tag.
+   * @param aTags
+   *        Array of tags to unset.
+   */
+  void untagURI(in nsIURI aURI,
+                [const, array, size_is(aCount)] in wstring aTags,
+                in unsigned long aCount);
+
+  /**
+   * Retrieves all URLs tagged with the given tag.
+   *
+   * @param aTag
+   *        Array of uris tagged with aTag.
+   */
+  nsIVariant getURIsForTag(in AString aTag);
+
+  /**
+   * Retrieves all tags set for the given URL.
+   *
+   * @param aURI
+   *        a URL.
+   * @returns array of tags.
+   */
+  nsIVariant getTagsForURI(in nsIURI aURI);
+};
--- a/toolkit/components/places/src/Makefile.in
+++ b/toolkit/components/places/src/Makefile.in
@@ -97,12 +97,14 @@ EXTRA_DSO_LDOPTS += \
 	$(DEPTH)/db/morkreader/$(LIB_PREFIX)morkreader_s.$(LIB_SUFFIX) \
 	$(MOZ_UNICHARUTIL_LIBS) \
 	$(MOZ_COMPONENT_LIBS) \
 	$(NULL)
 
 LOCAL_INCLUDES += -I$(srcdir)/../../build
 
 ifdef MOZ_PLACES_BOOKMARKS
-EXTRA_PP_COMPONENTS = nsLivemarkService.js
+EXTRA_PP_COMPONENTS = nsLivemarkService.js \
+                      nsTaggingService.js \
+                      $(NULL)
 endif
 
 include $(topsrcdir)/config/rules.mk
--- a/toolkit/components/places/src/nsNavHistory.cpp
+++ b/toolkit/components/places/src/nsNavHistory.cpp
@@ -2221,17 +2221,17 @@ nsNavHistory::GetQueryResults(const nsCO
   rv = mDBConn->CreateStatement(queryString, getter_AddRefs(statement));
   NS_ENSURE_SUCCESS(rv, rv);
 
   // bind parameters
   numParameters = 0;
   for (i = 0; i < aQueries.Count(); i ++) {
     PRInt32 clauseParameters = 0;
     rv = BindQueryClauseParameters(statement, numParameters,
-                                   aQueries[i], &clauseParameters);
+                                   aQueries[i], aOptions, &clauseParameters);
     NS_ENSURE_SUCCESS(rv, rv);
     numParameters += clauseParameters;
   }
 
   PRUint32 groupCount;
   const PRUint16 *groupings = aOptions->GroupingMode(&groupCount);
 
   if (groupCount == 0 && ! hasSearchTerms) {
@@ -3322,19 +3322,31 @@ nsNavHistory::QueryToSelectClause(nsNavH
   if (aQuery->MaxVisits() >= 0) {
     if (! aClause->IsEmpty())
       *aClause += NS_LITERAL_CSTRING(" AND ");
     parameterString(aStartParameter + *aParamCount, paramString);
     *aClause += NS_LITERAL_CSTRING("h.visit_count <= ") + paramString;
     (*aParamCount) ++;
   }
 
-  // only bookmarked, has no affect on bookmarks-only queries
-  if (aOptions->QueryType() != nsINavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS &&
-      aQuery->OnlyBookmarked()) {
+  
+  if (aOptions->QueryType() == nsINavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS) {
+    // Folders only have an affect on bookmark queries
+    // XXX: add multiple folders support
+    if (aQuery->Folders().Length() == 1) {
+      if (!aClause->IsEmpty())
+        *aClause += NS_LITERAL_CSTRING(" AND ");
+
+      nsCAutoString paramString;
+      parameterString(aStartParameter + *aParamCount, paramString);
+      (*aParamCount) ++;
+      *aClause += NS_LITERAL_CSTRING(" b.parent = ") + paramString;
+    }
+  } else if (aQuery->OnlyBookmarked()) {
+    // only bookmarked, has no affect on bookmarks-only queries
     if (!aClause->IsEmpty())
       *aClause += NS_LITERAL_CSTRING(" AND ");
 
     *aClause += NS_LITERAL_CSTRING("EXISTS (SELECT b.fk FROM moz_bookmarks b WHERE b.type = ") +
                 nsPrintfCString("%d", nsNavBookmarks::TYPE_BOOKMARK) +
                 NS_LITERAL_CSTRING(" AND b.fk = h.id)");
   }
 
@@ -3419,16 +3431,17 @@ nsNavHistory::QueryToSelectClause(nsNavH
 // nsNavHistory::BindQueryClauseParameters
 //
 //    THE ORDER AND BEHAVIOR SHOULD BE IN SYNC WITH QueryToSelectClause
 
 nsresult
 nsNavHistory::BindQueryClauseParameters(mozIStorageStatement* statement,
                                         PRInt32 aStartParameter,
                                         nsNavHistoryQuery* aQuery, // const
+                                        nsNavHistoryQueryOptions* aOptions,
                                         PRInt32* aParamCount)
 {
   nsresult rv;
   (*aParamCount) = 0;
 
   PRBool hasIt;
 
   // begin time
@@ -3461,16 +3474,26 @@ nsNavHistory::BindQueryClauseParameters(
 
   visits = aQuery->MaxVisits();
   if (visits >= 0) {
     rv = statement->BindInt32Parameter(aStartParameter + *aParamCount, visits);
     NS_ENSURE_SUCCESS(rv, rv);
     (*aParamCount) ++;
   }
 
+  // folder
+  if (aOptions->QueryType() == nsINavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS) {
+    // XXX: add multiple folders support
+    if (aQuery->Folders().Length() == 1) {
+      rv = statement->BindInt64Parameter(aStartParameter + *aParamCount,
+                                         aQuery->Folders()[0]);
+      NS_ENSURE_SUCCESS(rv, rv);
+      (*aParamCount) ++;
+    }
+  }
   // onlyBookmarked: nothing to bind
 
   // domain (see GetReversedHostname for more info on reversed host names)
   if (NS_SUCCEEDED(aQuery->GetHasDomain(&hasIt)) && hasIt) {
     nsString revDomain;
     GetReversedHostname(NS_ConvertUTF8toUTF16(aQuery->Domain()), revDomain);
 
     if (aQuery->DomainIsHost()) {
--- a/toolkit/components/places/src/nsNavHistory.h
+++ b/toolkit/components/places/src/nsNavHistory.h
@@ -477,16 +477,17 @@ protected:
                                nsNavHistoryQueryOptions* aOptions,
                                PRInt32 aStartParameter,
                                nsCString* aClause,
                                PRInt32* aParamCount,
                                const nsACString& aCommonConditions);
   nsresult BindQueryClauseParameters(mozIStorageStatement* statement,
                                      PRInt32 aStartParameter,
                                      nsNavHistoryQuery* aQuery,
+                                     nsNavHistoryQueryOptions* aOptions,
                                      PRInt32* aParamCount);
 
   nsresult ResultsAsList(mozIStorageStatement* statement,
                          nsNavHistoryQueryOptions* aOptions,
                          nsCOMArray<nsNavHistoryResultNode>* aResults);
 
   void GetAgeInDaysString(PRInt32 aInt, const PRUnichar *aName, 
                           nsACString& aResult);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/src/nsTaggingService.js
@@ -0,0 +1,322 @@
+/* -*- 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>
+ *
+ * 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;
+
+const TAGS_CLASSID = Components.ID("{6a059068-1630-11dc-8314-0800200c9a66}");
+const TAGS_CLASSNAME = "Places Tagging Service";
+const TAGS_CONTRACTID = "@mozilla.org/browser/tagging-service;1";
+
+const TAG_CONTAINER_ICON_URI = "chrome://mozapps/skin/places/tagContainerIcon.png"
+
+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 FAV_CONTRACTID = "@mozilla.org/browser/favicon-service;1";
+
+var gIoService = Cc[IO_CONTRACTID].getService(Ci.nsIIOService);
+
+/**
+ * The Places Tagging Service
+ */
+function TaggingService() {
+  this._tags = [];
+  
+  var options = this._history.getNewQueryOptions();
+  var query = this._history.getNewQuery();
+  query.setFolders([this._bms.tagRoot], 1);
+  var result = this._history.executeQuery(query, options);
+  var rootNode = result.root;
+  rootNode.containerOpen = true;
+
+  var cc = rootNode.childCount;
+  for (var i=0; i < cc; i++) {
+    var child = rootNode.getChild(i);
+    this._tags.push({ itemId: child.itemId, name: child.title });
+  }
+}
+
+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;
+  },
+
+  // nsISupports
+  QueryInterface: function TS_QueryInterface(iid) {
+    if (iid.equals(Ci.nsITaggingService) ||
+        iid.equals(Ci.nsISupports))
+      return this;
+    throw Cr.NO_INTERFACE;
+  },
+
+  /**
+   * If there's no tag with the given name, -1 is returned.
+   */
+  _getTagIndex: function TS__getTagIndex(aName) {
+    for (var i=0; i < this._tags.length; i++) {
+      if (this._tags[i].name == aName)
+        return i;
+    }
+
+    return -1;
+  },
+
+  get _tagContainerIcon() {
+    if (!this.__tagContainerIcon) {
+      this.__tagContainerIcon =
+        gIoService.newURI(TAG_CONTAINER_ICON_URI, null, null);
+    }
+
+    return this.__tagContainerIcon;
+  },
+
+  /**
+   * 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) {
+    var id = this._bms.createFolder(this._bms.tagRoot, aName,
+                                    this._bms.DEFAULT_INDEX);
+    this._tags.push({ itemId: id, name: aName});
+
+    // Set the favicon
+    var faviconService = Cc[FAV_CONTRACTID].getService(Ci.nsIFaviconService);
+    var uri = this._bms.getFolderURI(id);
+    faviconService.setFaviconUrlForPage(uri, this._tagContainerIcon);
+  
+    return id;
+  },
+
+  /**
+   * 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 options = this._history.getNewQueryOptions();
+    options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+    var query = this._history.getNewQuery();
+    query.setFolders([aTagId], 1);
+    query.uri = aURI;
+    var result = this._history.executeQuery(query, options);
+    var rootNode = result.root;
+    rootNode.containerOpen = true;
+    if (rootNode.childCount != 0) {
+      aItemId.value = rootNode.getChild(0).itemId;
+      return true;
+    }
+    return false;
+  },
+
+  // nsITaggingService
+  tagURI: function TS_tagURI(aURI, aTags, aCount) {
+    if (!aURI)
+      throw Cr.NS_ERROR_INVALID_ARG;
+
+    for (var i=0; i < aTags.length; i++) {
+      if (aTags[i].length == 0)
+        throw Cr.NS_ERROR_INVALID_ARG;
+
+      var tagIndex = this._getTagIndex(aTags[i]);
+      if (tagIndex == -1) {
+        var tagId = this._createTag(aTags[i]);
+        this._bms.insertBookmark(tagId, aURI, this._bms.DEFAULT_INDEX, "");
+      }
+      else {
+        var tagId = this._tags[tagIndex].itemId;
+        if (!this._isURITaggedInternal(aURI, tagId, {}))
+          this._bms.insertBookmark(tagId, aURI, this._bms.DEFAULT_INDEX, "");
+      }
+    }
+  },
+
+  /**
+   * Removes the tag container from the tags-root if the given tag is empty.
+   *
+   * @param aTagIndex
+   *        the index of a tag element under the _tags array
+   */
+  _removeTagAtIndexIfEmpty: function TS__removeTagAtIndexIfEmpty(aTagIndex) {
+    var options = this._history.getNewQueryOptions();
+    var query = this._history.getNewQuery();
+    query.setFolders([this._tags[aTagIndex].itemId], 1);
+    var result = this._history.executeQuery(query, options);
+    var rootNode = result.root;
+    rootNode.containerOpen = true;
+    if (rootNode.childCount == 0) {
+      this._bms.removeFolder(this._tags[aTagIndex].itemId);
+      this._tags.splice(aTagIndex, 1);
+    }
+  },
+
+  // nsITaggingService
+  untagURI: function TS_untagURI(aURI, aTags, aCount) {
+    if (!aURI)
+      throw Cr.NS_ERROR_INVALID_ARG;
+
+    for (var i=0; i < aTags.length; i++) {
+      if (aTags[i].length == 0)
+        throw Cr.NS_ERROR_INVALID_ARG;
+
+      var tagIndex = this._getTagIndex(aTags[i]);
+      if (tagIndex != -1) {
+        var itemId = { };
+        if (this._isURITaggedInternal(aURI, this._tags[tagIndex].itemId,
+                                      itemId)) {
+          this._bms.removeItem(itemId.value);
+          this._removeTagAtIndexIfEmpty(tagIndex);
+        }
+      }
+    }
+  },
+
+  // nsITaggingService
+  getURIsForTag: function TS_getURIsForTag(aTag) {
+    if (aTag.length == 0)
+      throw Cr.NS_ERROR_INVALID_ARG;
+
+    var uris = [];
+    var tagIndex = this._getTagIndex(aTag);
+    if (tagIndex != -1) {
+      var tagId = this._tags[tagIndex].itemId;
+      var options = this._history.getNewQueryOptions();
+      var query = this._history.getNewQuery();
+      query.setFolders([tagId], 1);
+      var result = this._history.executeQuery(query, options);
+      var rootNode = result.root;
+      rootNode.containerOpen = true;
+      var cc = rootNode.childCount;
+      for (var i=0; i < cc; i++)
+        uris.push(gIoService.newURI(rootNode.getChild(i).uri, null, null));
+    }
+    return uris;
+  },
+
+  // nsITaggingService
+  getTagsForURI: function TS_getTagsForURI(aURI) {
+    if (!aURI)
+      throw Cr.NS_ERROR_INVALID_ARG;
+
+    var tags = [];
+
+    var bookmarkIds = this._bms.getBookmarkIdsForURI(aURI, {});
+    for (var i=0; i < bookmarkIds.length; i++) {
+      var parent = this._bms.getFolderIdForItem(bookmarkIds[i]);
+      for (var j=0; j < this._tags.length; j++) {
+        if (this._tags[j].itemId == parent)
+          tags.push(this._tags[j].name);
+      }
+    }
+    return tags;
+  }
+};
+
+
+var gModule = {
+  registerSelf: function(componentManager, fileSpec, location, type) {
+    componentManager = componentManager.QueryInterface(Ci.nsIComponentRegistrar);
+    
+    for (var key in this._objects) {
+      var obj = this._objects[key];
+      componentManager.registerFactoryLocation(obj.CID,
+                                               obj.className,
+                                               obj.contractID,
+                                               fileSpec,
+                                               location,
+                                               type);
+    }
+  },
+  
+  unregisterSelf: function(componentManager, fileSpec, location) {},
+
+  getClassObject: function(componentManager, cid, iid) {
+    if (!iid.equals(Components.interfaces.nsIFactory))
+      throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  
+    for (var key in this._objects) {
+      if (cid.equals(this._objects[key].CID))
+        return this._objects[key].factory;
+    }
+
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+  
+  _objects: {
+    service: {
+      CID        : TAGS_CLASSID,
+      contractID : TAGS_CONTRACTID,
+      className  : TAGS_CLASSNAME,
+      factory    : TaggingServiceFactory = {
+                     createInstance: function(aOuter, aIID) {
+                       if (aOuter != null)
+                         throw Cr.NS_ERROR_NO_AGGREGATION;
+                       var svc = new TaggingService();
+                      return svc.QueryInterface(aIID);
+                     }
+                   }
+    }
+  },
+
+  canUnload: function(componentManager) {
+    return true;
+  }
+};
+
+function NSGetModule(compMgr, fileSpec) {
+  return gModule;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_tagging.js
@@ -0,0 +1,121 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** 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 Places Tagging Service unit test code.
+ *
+ * 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>
+ *
+ * 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 ***** */
+
+// Get history service
+try {
+  var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+                getService(Ci.nsINavHistoryService);
+} catch(ex) {
+  do_throw("Could not get history service\n");
+}
+
+// Get bookmark service
+try {
+  var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+              getService(Ci.nsINavBookmarksService);
+}
+catch(ex) {
+  do_throw("Could not get the nav-bookmarks-service\n");
+}
+
+// Get tagging service
+try {
+  var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].
+                getService(Ci.nsITaggingService);
+} catch(ex) {
+  do_throw("Could not get tagging service\n");
+}
+
+// main
+function run_test() {
+  var options = histsvc.getNewQueryOptions();
+  var query = histsvc.getNewQuery();
+
+  query.setFolders([bmsvc.tagRoot], 1);
+  var result = histsvc.executeQuery(query, options);
+  var tagRoot = result.root;
+  tagRoot.containerOpen = true;
+
+  do_check_eq(tagRoot.childCount, 0);
+
+  var uri1 = uri("http://foo.tld/");
+  var uri2 = uri("https://bar.tld/");
+
+  // this also tests that the multiple folders are not created for the same tag
+  tagssvc.tagURI(uri1, ["tag 1"], 1);
+  tagssvc.tagURI(uri2, ["tag 1"], 1);
+  do_check_eq(tagRoot.childCount, 1);
+
+  var tag1node = tagRoot.getChild(0)
+                        .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+  do_check_eq(tag1node.title, "tag 1");
+  tag1node.containerOpen = true;
+  do_check_eq(tag1node.childCount, 2);
+
+  // Tagging the same url twice with the same tag should be a no-op
+  tagssvc.tagURI(uri1, ["tag 1"], 1);
+  do_check_eq(tag1node.childCount, 2);
+
+  // the former should be ignored.
+  do_check_eq(tagRoot.childCount, 1);
+  tagssvc.tagURI(uri1, ["tag 1", "tag 2"], 2);
+  do_check_eq(tagRoot.childCount, 2);
+
+  // test getTagsForURI
+  var uri1tags = tagssvc.getTagsForURI(uri1, {});
+  do_check_eq(uri1tags.length, 2);
+  do_check_eq(uri1tags[0], "tag 1");
+  do_check_eq(uri1tags[1], "tag 2");
+  var uri2tags = tagssvc.getTagsForURI(uri2, {});
+  do_check_eq(uri2tags.length, 1);
+  do_check_eq(uri2tags[0], "tag 1");
+
+  // test getURIsForTag
+  var tag1uris = tagssvc.getURIsForTag("tag 1");
+  do_check_eq(tag1uris.length, 2);
+  do_check_true(tag1uris[0].equals(uri1));
+  do_check_true(tag1uris[1].equals(uri2));
+
+  tagssvc.untagURI(uri1, ["tag 1"], 1);
+  do_check_eq(tag1node.childCount, 1);
+
+  // removing the last uri from a tag should remove the tag-container
+  tagssvc.untagURI(uri2, ["tag 1"], 1);
+  do_check_eq(tagRoot.childCount, 1);
+}
--- a/toolkit/themes/pinstripe/mozapps/jar.mn
+++ b/toolkit/themes/pinstripe/mozapps/jar.mn
@@ -31,10 +31,11 @@ classic.jar:
   skin/classic/mozapps/shared/richview.xml                        (shared/richview.xml)
   skin/classic/mozapps/update/warning.gif                         (update/warning.gif)
   skin/classic/mozapps/update/updates.css                         (update/updates.css)
   skin/classic/mozapps/update/update.png                          (update/update.png)
   skin/classic/mozapps/xpinstall/xpinstallItemGeneric.png         (xpinstall/xpinstallItemGeneric.png)
   skin/classic/mozapps/xpinstall/xpinstallConfirm.css             (xpinstall/xpinstallConfirm.css)
 #ifdef MOZ_PLACES
   skin/classic/mozapps/places/defaultFavicon.png                  (places/defaultFavicon.png)
+  skin/classic/mozapps/places/tagContainerIcon.png                (places/tagContainerIcon.png)
 #endif
 #endif
new file mode 100755
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..05f64eae6c452bbdda1aa4efb4d178314fbe7532
GIT binary patch
literal 271
zc%17D@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|jKx9jP7LeL$-D$|SkfJR9T^xl
z_H+M9WCijSl0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP@>n<
z#WBRfzc%Q$-~j~=?b)R!=F6`Z2r}y)@1M}pK4W+M(KBm${U6L0h(2g%8XEPbtNuhP
z)0WMfBM+6aIx<_`c~~LW9GdHp_=M%9Jg-xMLcnjqN>vW^^7Dp$TwDE~9=T8*pSk=_
z+Gf>@=Uz*H-#7EId<FNODL2o{>eXM*oRhI_f$@pn1@oVoWNY|dRgwSLbu}ng-tq>}
OVGN$GelF{r5}E)#Phdg-
--- a/toolkit/themes/winstripe/mozapps/jar.mn
+++ b/toolkit/themes/winstripe/mozapps/jar.mn
@@ -32,10 +32,11 @@ classic.jar:
         skin/classic/mozapps/update/extensionalert.png                (update/extensionalert.png)
         skin/classic/mozapps/update/update.png                        (update/update.png)
         skin/classic/mozapps/update/warning.gif                       (update/warning.gif)
         skin/classic/mozapps/update/updates.css                       (update/updates.css)
         skin/classic/mozapps/xpinstall/xpinstallConfirm.css           (xpinstall/xpinstallConfirm.css)
         skin/classic/mozapps/xpinstall/xpinstallItemGeneric.png       (xpinstall/xpinstallItemGeneric.png)
 #ifdef MOZ_PLACES
         skin/classic/mozapps/places/defaultFavicon.png                (places/defaultFavicon.png)
+        skin/classic/mozapps/places/tagContainerIcon.png                (places/tagContainerIcon.png)
 #endif
 #endif
new file mode 100755
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..05f64eae6c452bbdda1aa4efb4d178314fbe7532
GIT binary patch
literal 271
zc%17D@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|jKx9jP7LeL$-D$|SkfJR9T^xl
z_H+M9WCijSl0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP@>n<
z#WBRfzc%Q$-~j~=?b)R!=F6`Z2r}y)@1M}pK4W+M(KBm${U6L0h(2g%8XEPbtNuhP
z)0WMfBM+6aIx<_`c~~LW9GdHp_=M%9Jg-xMLcnjqN>vW^^7Dp$TwDE~9=T8*pSk=_
z+Gf>@=Uz*H-#7EId<FNODL2o{>eXM*oRhI_f$@pn1@oVoWNY|dRgwSLbu}ng-tq>}
OVGN$GelF{r5}E)#Phdg-