Querying Places

Revision Information
  • Revision slug: Querying_Places
  • Revision title: Querying Places
  • Revision id: 131213
  • Created:
  • Creator: Gemal
  • Is reviewed? Yes
  • Reviewed:
  • Reviewed by: Jay
  • Is approved? Yes
  • Is current revision? No
  • Comment executeQuery instead of executeQueries

Revision Source

Revision Content

Most information from the Firefox history and bookmarks system ("Places") comes from executing queries on the history system (nsINavHistory.executeQuery/executeQueries). This will give you a nsINavHistoryResult object which will contain a tree structure of the results. The definitions of these structures is in {{template.Source("browser/components/places/public/nsINavHistoryService.idl")}}

Executing a query

There are two parts of a query: a list of one or more nsINavHistoryQuery objects, and an nsINavHistoryQueryOptions object. You first need to fill out these structures with the parameters you want. Use nsINavHistoryService.getNewQuery and nsINavHistoryService.getNewQueryOptions to retrieve empty objects. These objects typically have empty values for each possible field, so using them in their initial state will give you all of history in a flat list:

var historyService = Components.classes["@mozilla.org/browser/nav-history-service;1"]

// get all history sorded by ascending visit date
var options = historyService.getNewQueryOptions();
options.sortingMode = options.SORT_BY_DATE_ASCENDING;

// no query parameters will return everything
var query = historyService.getNewQuery();

var result = historyService.executeQuery(query, options)

Result types

The query options structure nsINavHistoryQueryOptions has an attribute resultType for the type of the results that you want. It is important to understand the differences between these:

  • RESULTS_AS_URI: This is the default, and means that you get one result node of type RESULT_TYPE_URI for each URI that matches the query. The visit date for each node will be the last visit date for that URL. You will definitely want this when showing bookmark folders.
  • RESULTS_AS_VISIT: This means that you want one entry for each time a page was visited matching the given query. Therefore, you may get duplicate entries for URLs, each with a different date. The nodes will be of type RESULT_TYPE_VISIT which will give you access to a session ID for each visit. This session ID is the same for all pages that were reached by clicking links. A new session starts when the user types a new URL or follows a bookmark. It is used to compute the dividing lines in the places history view.
  • RESULTS_AS_FULL_VISIT: This is the same as visits but the results will be of type RESULT_TYPE_FULL_VISIT and will have additional information about the visit, such as the referring visit, and how the transition happened (typed, redirect, link, etc). This information is typically not needed and causes the result nodes (of which there may be many) to be larger, which is why it is a separate option.

The named constants are properties of nsINavHistoryQueryOptions, for example Components.interfaces.nsINavHistoryQueryOptions.RESULTS_AS_VISIT.

Query parameters

Note: The query object interface is subject to change. In its current form it is not very expressive. Implementing a better interface is {{template.Bug(317830)}}.

You can pass one or more nsINavHistoryQuery objects to nsINavHistoryService.executeQuery/executeQueries. Within one query object, all parameters are ANDed together. The conditions for different query objects are then ORed together. This allows for a simpler implementation and interface than a full logical operation with nested clauses while still being expressive.

Example of querying for any pages I've visited that contain the word "firefox" in the title/URL or that I've visited today from mozilla.org.

// first query object searches for "firefox" in title/URL
var query1 = historyService.getNewQuery();
query1.searchTerms = "firefox";

// second query object searches for visited in past 24 hours AND from mozilla.org
var query2 = historyService.getNewQuery();
query2.beginTimeReference = query2.TIME_RELATIVE_NOW;
query2.beginTime = -24 * 60 * 60 * 1000000; // 24 hours ago in microseconds
query2.endTimeReference = query2.TIME_RELATIVE_NOW;
query2.endTime = 0; // now
query2.domain = "mozilla.org";

var result = historyService.executeQueries([query1, query2], 2, options);

Note: Keyword searching doesn't work correctly across OR queries. The current behavior does the normal query and then selects keywords from the first query and filters all the results. (In other words, the keywords from the first query are ANDed with all queries.) Keywords from subsequent query objects are ignored. This is {{template.Bug(320332)}}.

Bookmark queries

The contents of bookmark folders can be retrieved by setting the "folders" member in the query object. This item is an array of folder IDs from the bookmark service. Typically, you will only have one folder ID in this list, which will given you the contents of that folder. You can set multiple folders and the result will be the intersection of all the folders.

Generally, you want to set the grouping mode in the options to GROUP_BY_FOLDER for bookmarks queries. This will result in a hierarchical structure containing the bookmark folder hierarchy rooted at where you requested.

Note: This doesn't actually have any effect in the current implementation. The query system always assumes GROUP_BY_FOLDER for bookmarks queries. See {{template.Bug(324579)}}.

For sorting, you will generally want to use SORT_BY_NONE (the default) since this will return items in their "natural" order as specified by the user in the bookmarks manager. Other sortings will work, however.

For bookmark queries you will generally want no query parameters to retrieve all items from the requested folder(s). When you specify exactly one folder, GROUP_BY_FOLDER, and no query parameters, the system will be more efficient querying and keeping the results up-to-date since this maps to exactly one bookmark folder.

var bookmarkSvc = Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"]
// |options| is an object created in the previous section
options.setFolders([bookmarkService.toolbarRoot], 1);
options.setGroupingMode([options.GROUP_BY_FOLDER], 1);
var result = historyService.executeQuery(query, options);

Serializing queries

Normally, query and options objects are represented in terms of objects. However, it is possible to serialize these objects using queriesToQueryString, and deserialize them using queryStringToQueries. The result will be a string starting with "place:" which you can store or bookmark. When a "place:" URI is bookmarked, it will expand to the results of the query when it is opened by the user.

You should never manually construct "place:" URI strings. The format is subject to change. If you need one, create the proper objects and call the history service to serialize it for you.

Be careful, queryStringToQueries may not return any query objects if the string was empty. Your code should handle this case. There will always be an options structure returned. If no options were specified, it will have the default values. If there were no query parameters specified but the input string was not empty (there were options) you may get one query object returned, containing the default query values.

Example of serializing and deserializing two queries and an options object:

var queryString = historyService.queriesToQueryStrings([query1, query2], 2, options);

var queriesRef = { };
var queryCountRef = { };
var optionsRef = { };
historyService.queryStringToQueries(queryString, queriesRef, queryCountRef, optionsRef);
// now use queriesRef.value, optionsRef.value

Using the results

Note: Be careful when accessing nodes and do not keep references to them around. Notifications sent to the result from the history and bookmarks system, as well as commands executed by the programmer such as sorting may cause the structure to change and nodes may be inserted, removed, or replaced.

The nsINavHistoryResult object returned by executeQuery/executeQueries contains the list of matches to the given history or bookmarks query. These results are contained in a tree structure made up of nodes. A node's type can be retrieved using its type attribute. This type tells you what interface you can QueryInterface the node to in order to get at more detailed information:

  • nsINavHistoryResultNode: Base class for all nodes. Contains URI, title, and other general info.
  • nsINavHistoryVisitResultNode: Derived from nsINavHistoryResultNode, contains session information.
  • nsINavHistoryFullVisitResultNode: Derived from nsINavHistoryVisitResultNode, contains information about how the user navigated to this page. Note: currently unimplemented, see {{template.Bug(320831)}}.
  • nsINavHistoryContainerResultNode: General container node giving access to its children. Derived from nsINavHistoryResultNode.
  • nsINavHistoryQueryResultNode: A type of container representing a query of the history system. It allows you to get the query options and parameters.
  • nsINavHistoryFolderResultNode: Derived from nsINavHistoryQueryResultNode, this represents a special type of query mapping to the exact contents of one bookmarks folder. It gives easy access to its folder ID, and also updates itself more efficiently than a general query.

Example of detecting the type of a node

var Ci = Components.interfaces;
  case node.RESULT_TYPE_URI:
    dump("URI result " + node.uri + "\n");
  case node.RESULT_TYPE_VISIT:
    var visit = node.QueryInterface(Ci.nsINavHistoryVisitResultNode);
    dump("Visit result " + node.uri + " session = " + visit.sessionId + "\n");
    var fullVisit = node.QueryInterface(Ci.nsINavHistoryFullVisitResultNode);
    dump("Full visit result " + node.uri + " session = " + fullVisit.sessionId + " transitionType = " +
         fullVisit.transitionType + "\n");
  case node.RESULT_TYPE_HOST:
    var container = node.QueryInterface(Ci.nsINavHistoryContainerResultNode);
    dump("Host " + container.title + "\n");
    var container = node.QueryInterface(Ci.nsINavHistoryContainerResultNode);
    dump("Remote container " + container.title + " type = " + container.remoteContainerType + "\n");
  case node.RESULT_TYPE_QUERY:
    var query = node.QueryInterface(Ci.nsINavHistoryQueryResultNode);
    dump("Query, place URI = " + query.uri + "\n");
    // note that the folder is also a query and so has a query and an options structure
    var folder = node.QueryInterface(Ci.nsINavHistoryFolderResultNode);
    dump("Folder " + folder.title + " id = " + folder.folderId + "\n");


Containers hold lists of other containers and result nodes. Each result has a container representing the root of the query. It can be retrieved using the root attribute of the result. For general queries, this root container is a nsINavHistoryQueryResultNode with the query parameters and options that you supplied in the original query. For queries mapping to one bookmark folder, this will be a nsINavHistoryFolderResultNode.

Containers can be open or closed. This corresponds to the open and closed state in a tree view, and can also be mapped to showing and hiding menus. To get at a container's contents, you must first open the container. Most container types populate themselves lazily, so opening a container actually corresponds to executing the given query. While a container is open, it will listen to the history and bookmarks systems' notifications and modify their contents to keep themselves up-to-date. For this reason, it is best to close a container as soon as you are done with it, since it will give better performance. If you close a container and re-open it before any history or bookmark change notifications come, the results will generally still be there and this operation will be fast.

Example of traversing a container:

var cont = result.root;
cont.containerOpen = true;
for (var i = 0; i < cont.childCount; i ++) {
  var node = cont.getChild(i);
  dump(node.title + "\n");
cont.containerOpen = false;

The result view interface

Note: this interface is not in the Firefox 2.0 alpha1 release. It is {{template.Bug(329546)}}. If you are using the alpha release, you can QueryInterface the result object directly to nsITreeView to attach to a tree.

If you are mapping a result into UI, you can implement the nsINavHistoryResultViewer interface and attach it to the result with the nsINavHistoryResult.viewer attribute. This viewer will be called when the result tree changes, either as a result of user action or as a result of notifications from the bookmarks and history systems. Your implementation would then reflect these changes in the UI.

A prepackaged view interface for a nsITreeBoxObject is provided that manages the complex view requirements of a tree. This object's interface is nsINavHistoryResultTreeViewer (a descendent of nsINavHistoryResultViewer) and can be created using the contract @mozilla.org/browser/nav-history/result-tree-viewer;1.

var treeviewer =
result.viewer = treeviewer;
mytree.view = treeviewer.QueryInterface(Components.interfaces.nsITreeView);

Both the result and the tree will register themselves with the viewer object using result attribute and the setTree method respectively. Do not set these explicitly.

Remote containers

Remote containers are a way for extension authors and others to provide content for containers in a places query result. First, you should create a component that implements nsIRemoteContainer. Then you create a bookmark folder associated with your component by using nsINavBookmarksService.createContainer. The type parameter is a string containing the contract ID of your component. The bookmark service and other components will call CreateService using this contract ID to get your nsIRemoteContainer implementation.

Once your service is associated with a folder, it will get notifications when the folder is moved or deleted, or when a container result node representing your container is opened or closed. In response to these operations, your service should update any bookkeeping information associated with the folder. Your service can also declare whether your service's containers have read-only children or whether they can be modified like normal bookmarks.

There are two modes of operation that remote container implementations can use. First, it can act like a bookmarks provider and create real bookmarks inside a regular bookmarks folder. An example of this is the {{wiki.template('Named-source', [ "browser/components/places/src/nsLivemarkService.cpp", "livemark service" ])}}. The livemark service reads a feed and creates bookmarks in a folder corresponding to the items in that stream. These bookmarks are managed by the bookmarks service so the livemarks service does not care when containers are opened and closed. It only needs to handle the cases when a folder is moved or deleted (to update the information that associates a feed with the folder) and declares that livemarks are read-only.

The second mode of operation is more active. Such services can respond to container open and close operations and populate the results at runtime. They can therefore generate more dynamic content at the time it is shown. When a folder with a container type is opened, the service will be notified and given the container result node. The service can then create children in that container using appendURINode, appendFolderNode, etc. Of special note is appendContainerNode that can be used to create more remote containers. These remote containers are not associated with any bookmark folder (use appendFolderNode for that). For example, a file browser could be created that dynamically creates containers associated with subfolders. Keep in mind that each container has a property bag that can be used to associate random information with the node such a path.

Revert to this revision