blob: 240d8062099acf94ca6c7049978fc27e1e85a799 [file] [log] [blame]
/* -*- 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]);
}