1651 lines
52 KiB
JavaScript
1651 lines
52 KiB
JavaScript
/* ***** 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 Microsummarizer.
|
|
*
|
|
* The Initial Developer of the Original Code is Mozilla.
|
|
* Portions created by the Initial Developer are Copyright (C) 2006
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Myk Melez <myk@mozilla.org> (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 PERMS_FILE = 0644;
|
|
const MODE_WRONLY = 0x02;
|
|
const MODE_TRUNCATE = 0x20;
|
|
|
|
// How often to update microsummaries, in milliseconds.
|
|
// XXX Make this a hidden pref so power users can modify it.
|
|
const UPDATE_INTERVAL = 30 * 60 * 1000; // 30 minutes
|
|
|
|
// How often to check for microsummaries that need updating, in milliseconds.
|
|
const CHECK_INTERVAL = 15 * 1000; // 15 seconds
|
|
|
|
const MICSUM_NS = new Namespace("http://www.mozilla.org/microsummaries/0.1");
|
|
const XSLT_NS = new Namespace("http://www.w3.org/1999/XSL/Transform");
|
|
|
|
#ifdef MOZ_PLACES
|
|
const FIELD_MICSUM_GEN_URI = "microsummary/generatorURI";
|
|
const FIELD_MICSUM_EXPIRATION = "microsummary/expiration";
|
|
const FIELD_GENERATED_TITLE = "bookmarks/generatedTitle";
|
|
const FIELD_CONTENT_TYPE = "bookmarks/contentType";
|
|
#else
|
|
const NC_NS = "http://home.netscape.com/NC-rdf#";
|
|
const FIELD_MICSUM_GEN_URI = NC_NS + "MicsumGenURI";
|
|
const FIELD_MICSUM_EXPIRATION = NC_NS + "MicsumExpiration";
|
|
const FIELD_GENERATED_TITLE = NC_NS + "GeneratedTitle";
|
|
const FIELD_CONTENT_TYPE = NC_NS + "ContentType";
|
|
const FIELD_BOOKMARK_URL = NC_NS + "URL";
|
|
#endif
|
|
|
|
function MicrosummaryService() {}
|
|
|
|
MicrosummaryService.prototype = {
|
|
|
|
#ifdef MOZ_PLACES
|
|
// Bookmarks Service
|
|
__bms: null,
|
|
get _bms() {
|
|
if (!this.__bms)
|
|
this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
|
|
getService(Ci.nsINavBookmarksService);
|
|
return this.__bms;
|
|
},
|
|
|
|
// Annotation Service
|
|
__ans: null,
|
|
get _ans() {
|
|
if (!this.__ans)
|
|
this.__ans = Cc["@mozilla.org/browser/annotation-service;1"].
|
|
getService(Ci.nsIAnnotationService);
|
|
return this.__ans;
|
|
},
|
|
#else
|
|
// RDF Service
|
|
__rdf: null,
|
|
get _rdf() {
|
|
if (!this.__rdf)
|
|
this.__rdf = Cc["@mozilla.org/rdf/rdf-service;1"].
|
|
getService(Ci.nsIRDFService);
|
|
return this.__rdf;
|
|
},
|
|
|
|
// Bookmarks Data Source
|
|
__bmds: null,
|
|
get _bmds() {
|
|
if (!this.__bmds)
|
|
this.__bmds = this._rdf.GetDataSource("rdf:bookmarks");
|
|
return this.__bmds;
|
|
},
|
|
|
|
// Old Bookmarks Service
|
|
__bms: null,
|
|
get _bms() {
|
|
if (!this.__bms)
|
|
this.__bms = this._bmds.QueryInterface(Ci.nsIBookmarksService);
|
|
return this.__bms;
|
|
},
|
|
#endif
|
|
|
|
// IO Service
|
|
__ios: null,
|
|
get _ios() {
|
|
if (!this.__ios)
|
|
this.__ios = Cc["@mozilla.org/network/io-service;1"].
|
|
getService(Ci.nsIIOService);
|
|
return this.__ios;
|
|
},
|
|
|
|
// Observer Service
|
|
__obs: null,
|
|
get _obs() {
|
|
if (!this.__obs)
|
|
this.__obs = Cc["@mozilla.org/observer-service;1"].
|
|
getService(Ci.nsIObserverService);
|
|
return this.__obs;
|
|
},
|
|
|
|
/**
|
|
* Make a URI from a spec.
|
|
* @param spec
|
|
* The string spec of the URI.
|
|
* @returns An nsIURI object.
|
|
*/
|
|
_uri: function MSS__uri(spec) {
|
|
return this._ios.newURI(spec, null, null);
|
|
},
|
|
|
|
#ifndef MOZ_PLACES
|
|
/**
|
|
* Make an RDF resource from a URI spec.
|
|
* @param uriSpec
|
|
* The URI spec to convert into a resource.
|
|
* @returns An nsIRDFResource object.
|
|
*/
|
|
_resource: function MSS__resource(uriSpec) {
|
|
return this._rdf.GetResource(uriSpec);
|
|
},
|
|
|
|
/**
|
|
* Make an RDF literal from a string.
|
|
* @param str
|
|
* The string from which to construct the literal.
|
|
* @returns An nsIRDFLiteral object
|
|
*/
|
|
_literal: function MSS__literal(str) {
|
|
return this._rdf.GetLiteral(str);
|
|
},
|
|
#endif
|
|
|
|
// Directory Locator
|
|
__dirs: null,
|
|
get _dirs() {
|
|
if (!this.__dirs)
|
|
this.__dirs = Cc["@mozilla.org/file/directory_service;1"].
|
|
getService(Ci.nsIProperties);
|
|
return this.__dirs;
|
|
},
|
|
|
|
// A cache of local microsummary generators. This gets built on startup
|
|
// by the _cacheLocalGenerators() method.
|
|
_localGenerators: {},
|
|
|
|
// The timer that periodically checks for microsummaries needing updating.
|
|
_timer: null,
|
|
|
|
// Interfaces this component implements.
|
|
interfaces: [Ci.nsIMicrosummaryService, Ci.nsIObserver, Ci.nsISupports],
|
|
|
|
// nsISupports
|
|
|
|
QueryInterface: function MSS_QueryInterface(iid) {
|
|
//if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
|
|
if (!iid.equals(Ci.nsIMicrosummaryService) &&
|
|
!iid.equals(Ci.nsIObserver) &&
|
|
!iid.equals(Ci.nsISupportsWeakReference) &&
|
|
!iid.equals(Ci.nsISupports))
|
|
throw Components.results.NS_ERROR_NO_INTERFACE;
|
|
return this;
|
|
},
|
|
|
|
// nsIObserver
|
|
|
|
observe: function MSS_observe(subject, topic, data) {
|
|
switch (topic) {
|
|
case "xpcom-shutdown":
|
|
this._destroy();
|
|
break;
|
|
}
|
|
},
|
|
|
|
_init: function MSS__init() {
|
|
this._obs.addObserver(this, "xpcom-shutdown", true);
|
|
|
|
// Periodically update microsummaries that need updating.
|
|
this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
var callback = {
|
|
_svc: this,
|
|
notify: function(timer) { this._svc._updateMicrosummaries() }
|
|
};
|
|
this._timer.initWithCallback(callback,
|
|
CHECK_INTERVAL,
|
|
this._timer.TYPE_REPEATING_SLACK);
|
|
|
|
this._cacheLocalGenerators();
|
|
},
|
|
|
|
_destroy: function MSS__destroy() {
|
|
this._timer.cancel();
|
|
this._timer = null;
|
|
},
|
|
|
|
_updateMicrosummaries: function MSS__updateMicrosummaries() {
|
|
#ifdef MOZ_PLACES
|
|
// This try/catch block is a temporary workaround for bug 336194.
|
|
var bookmarks;
|
|
try {
|
|
bookmarks = this._ans.getPagesWithAnnotation(FIELD_MICSUM_GEN_URI, {});
|
|
}
|
|
catch(e) {
|
|
bookmarks = [];
|
|
}
|
|
#else
|
|
var bookmarks = [];
|
|
var resources = this._bmds.GetAllResources();
|
|
while (resources.hasMoreElements()) {
|
|
var resource = resources.getNext().QueryInterface(Ci.nsIRDFResource);
|
|
if (this._bmds.hasArcOut(resource, this._resource(FIELD_MICSUM_GEN_URI)))
|
|
bookmarks.push(resource);
|
|
}
|
|
#endif
|
|
|
|
var now = new Date().getTime();
|
|
|
|
for ( var i = 0; i < bookmarks.length; i++ ) {
|
|
var bookmarkID = bookmarks[i];
|
|
|
|
// Skip this page if its microsummary hasn't expired yet.
|
|
if (this._hasField(bookmarkID, FIELD_MICSUM_EXPIRATION) &&
|
|
this._getField(bookmarkID, FIELD_MICSUM_EXPIRATION) > now)
|
|
continue;
|
|
|
|
// Create and initialize a new Microsummary instance.
|
|
var pageURI =
|
|
#ifdef MOZ_PLACES
|
|
bookmarkID;
|
|
#else
|
|
this._uri(this._getField(bookmarkID, FIELD_BOOKMARK_URL));
|
|
#endif
|
|
var generatorURI = this._uri(this._getField(bookmarkID, FIELD_MICSUM_GEN_URI));
|
|
var microsummary = new Microsummary(pageURI, generatorURI);
|
|
if (this._localGenerators[generatorURI.spec])
|
|
microsummary.generator = this._localGenerators[generatorURI.spec];
|
|
|
|
// A microsummary observer that calls the microsummary service
|
|
// to update the datastore when the microsummary finishes loading.
|
|
var observer = {
|
|
_svc: this,
|
|
_bookmarkID: bookmarkID,
|
|
onContentLoaded: function(microsummary) {
|
|
this._svc._updateMicrosummary(this._bookmarkID, microsummary);
|
|
|
|
// Prevent reference cycles from leaking memory.
|
|
microsummary.removeObserver(this);
|
|
this._bookmark = null;
|
|
this._svc = null;
|
|
}
|
|
};
|
|
|
|
// Register the observer with the microsummary and trigger
|
|
// the microsummary to update itself.
|
|
microsummary.addObserver(observer);
|
|
microsummary.update();
|
|
}
|
|
},
|
|
|
|
_updateMicrosummary: function MSS__updateMicrosummary(bookmarkID, microsummary) {
|
|
this._setField(bookmarkID, FIELD_GENERATED_TITLE, microsummary.content);
|
|
|
|
var now = new Date().getTime();
|
|
this._setField(bookmarkID, FIELD_MICSUM_EXPIRATION, now + UPDATE_INTERVAL);
|
|
|
|
LOG("updated microsummary for page " + microsummary.pageURI.spec +
|
|
" to " + microsummary.content);
|
|
},
|
|
|
|
/**
|
|
* Load local generators into the cache.
|
|
*
|
|
*/
|
|
_cacheLocalGenerators: function MSS__cacheLocalGenerators() {
|
|
// Load generators from the application directory.
|
|
var appDir = this._dirs.get("MicsumGens", Ci.nsIFile);
|
|
if (appDir.exists())
|
|
this._cacheLocalGeneratorDir(appDir);
|
|
|
|
// Load generators from the user's profile.
|
|
var profileDir = this._dirs.get("UsrMicsumGens", Ci.nsIFile);
|
|
if (profileDir.exists())
|
|
this._cacheLocalGeneratorDir(profileDir);
|
|
},
|
|
|
|
/**
|
|
* Load local generators from a directory into the cache.
|
|
*
|
|
* @param dir
|
|
* nsIFile object pointing to directory containing generator files
|
|
*
|
|
*/
|
|
_cacheLocalGeneratorDir: function MSS__cacheLocalGeneratorDir(dir) {
|
|
var files = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
|
|
var file = files.nextFile;
|
|
|
|
while (file) {
|
|
// Recursively load generators so support packs containing
|
|
// lots of generators can organize them into multiple directories.
|
|
if (file.isDirectory())
|
|
this._cacheLocalGeneratorDir(file);
|
|
else
|
|
this._cacheLocalGeneratorFile(file);
|
|
|
|
file = files.nextFile;
|
|
}
|
|
files.close();
|
|
},
|
|
|
|
/**
|
|
* Load a local generator from a file into the cache.
|
|
*
|
|
* @param file
|
|
* nsIFile object pointing to file from which to load generator
|
|
*
|
|
*/
|
|
_cacheLocalGeneratorFile: function MSS__cacheLocalGeneratorFile(file) {
|
|
var uri = this._ios.newFileURI(file);
|
|
var request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
|
|
createInstance(Ci.nsIDOMEventTarget);
|
|
|
|
var t = this;
|
|
request.addEventListener("load",
|
|
function(e) { t._localGeneratorLoadHandler(e) },
|
|
false);
|
|
request = request.QueryInterface(Ci.nsIXMLHttpRequest);
|
|
request.open("GET", uri.spec, true);
|
|
request.send(null);
|
|
},
|
|
|
|
_localGeneratorLoadHandler: function(event) {
|
|
// XXX Factor out code using getPageFromEvent().
|
|
var request = event.target;
|
|
var uri = request.channel.originalURI;
|
|
var doc = request.responseXML;
|
|
|
|
if (!doc)
|
|
throw(uri.spec + " microsummary generator loaded, but not XML");
|
|
|
|
if (doc.documentElement.nodeName == "parsererror")
|
|
throw(uri.spec + " microsummary generator loaded, but invalid XML");
|
|
|
|
// XXX Right now the code retrieves the first "generator" element
|
|
// in the microsummaries namespace, regardless of whether or not
|
|
// it's the root element. Should it matter?
|
|
// XXX We should assert if the file doesn't contain a generator.
|
|
var node = doc.getElementsByTagNameNS(MICSUM_NS, "generator")[0];
|
|
if (!node)
|
|
return;
|
|
|
|
var generator = new MicrosummaryGenerator();
|
|
generator.localURI = uri;
|
|
generator.initFromXML(node);
|
|
|
|
// Add the generator to the local generators cache.
|
|
// XXX Figure out why Firefox crashes on shutdown if we index generators
|
|
// by uri.spec but doesn't crash if we index by uri.spec.split().join().
|
|
//this._localGenerators[generator.uri.spec] = generator;
|
|
this._localGenerators[generator.uri.spec.split().join()] = generator;
|
|
|
|
LOG("loaded local microsummary generator\n" +
|
|
" local URI: " + generator.localURI.spec + "\n" +
|
|
" source URI: " + generator.uri.spec);
|
|
},
|
|
|
|
// nsIMicrosummaryService
|
|
|
|
/**
|
|
* Install the microsummary generator from the resource at the supplied URI.
|
|
* Callable by content via the addMicrosummaryGenerator() sidebar method.
|
|
*
|
|
* @param generatorURI
|
|
* the URI of the resource providing the generator
|
|
*
|
|
*/
|
|
addGenerator: function MSS_addGenerator(generatorURI) {
|
|
var t = this;
|
|
downloadXMLPage(generatorURI, function(e) { t._addGeneratorLoadHandler(e) });
|
|
},
|
|
|
|
_addGeneratorLoadHandler: function(event) {
|
|
// XXX Factor out code using getPageFromEvent().
|
|
var request = event.target;
|
|
var uri = request.channel.originalURI;
|
|
var doc = request.responseXML;
|
|
|
|
if (!doc)
|
|
throw(uri.spec + " microsummary generator loaded, but not XML");
|
|
|
|
if (doc.documentElement.nodeName == "parsererror")
|
|
throw(uri.spec + " microsummary generator loaded, but invalid XML");
|
|
|
|
// XXX Make sure it's a valid microsummary generator.
|
|
|
|
// Add a reference to the URI from which we got this generator so we have
|
|
// a unique identifier for the generator and also so we can check back later
|
|
// for updates.
|
|
doc.documentElement.setAttribute("sourceURI", uri.spec);
|
|
|
|
var genName = doc.documentElement.getAttribute("name");
|
|
var fileName = sanitizeName(genName) + ".xml";
|
|
var file = this._dirs.get("ProfD", Ci.nsIFile);
|
|
file.append("microsummary-generators");
|
|
file.append(fileName);
|
|
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
|
|
|
|
var fos = Cc["@mozilla.org/network/safe-file-output-stream;1"].
|
|
createInstance(Ci.nsIFileOutputStream);
|
|
var localFile = file.QueryInterface(Ci.nsILocalFile);
|
|
fos.init(localFile, (MODE_WRONLY | MODE_TRUNCATE), PERMS_FILE, 0);
|
|
var serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
|
|
createInstance(Ci.nsIDOMSerializer);
|
|
serializer.serializeToStream(doc.documentElement, fos, null);
|
|
if (fos instanceof Ci.nsISafeOutputStream) {
|
|
try { fos.finish() }
|
|
catch (e) { fos.close() }
|
|
}
|
|
else
|
|
fos.close();
|
|
|
|
// Finally, cache the generator in the local generators cache.
|
|
this._cacheLocalGeneratorFile(file);
|
|
},
|
|
|
|
/**
|
|
* Get the set of microsummaries available for a given page. The set
|
|
* might change after this method returns, since this method will trigger
|
|
* an asynchronous load of the page in question (if it isn't already loaded)
|
|
* to see if it references any page-specific microsummaries.
|
|
*
|
|
* If the caller passes a bookmark ID, and one of the microsummaries
|
|
* is the current one for the bookmark, this method will retrieve content
|
|
* from the datastore for that microsummary, which is useful when callers
|
|
* want to display a list of microsummaries for a page that isn't loaded,
|
|
* and they want to display the actual content of the selected microsummary
|
|
* immediately (rather than after the content is asynchronously loaded).
|
|
*
|
|
* @param pageURI
|
|
* the URI of the page for which to retrieve available microsummaries
|
|
*
|
|
* @param bookmarkID (optional)
|
|
* the ID of the bookmark for which this method is being called
|
|
*
|
|
* @returns an nsIMicrosummarySet of nsIMicrosummaries for the given page
|
|
*
|
|
*/
|
|
getMicrosummaries: function MSS_getMicrosummaries(pageURI, bookmarkID) {
|
|
var microsummaries = new MicrosummarySet();
|
|
|
|
// Get microsummaries defined by local generators.
|
|
for (var genURISpec in this._localGenerators) {
|
|
var generator = this._localGenerators[genURISpec];
|
|
|
|
if (generator.appliesToURI(pageURI)) {
|
|
var microsummary = new Microsummary(pageURI, generator.uri);
|
|
microsummary.generator = generator;
|
|
|
|
// If this is the current microsummary for this bookmark, load the content
|
|
// from the datastore so it shows up immediately in microsummary picking UI.
|
|
if (bookmarkID && this.isMicrosummary(bookmarkID, microsummary))
|
|
microsummary.content = this._getField(bookmarkID, FIELD_GENERATED_TITLE);
|
|
|
|
microsummaries.AppendElement(microsummary);
|
|
}
|
|
}
|
|
|
|
// Get microsummaries defined by the page. If we don't have the page,
|
|
// download it asynchronously, and then finish populating the set.
|
|
var pageContent = getLoadedPageContent(pageURI);
|
|
if (pageContent)
|
|
microsummaries.extractFromPage(pageURI, pageContent);
|
|
|
|
else {
|
|
// Load the page with a callback that will add the page's microsummaries
|
|
// to the set once the page has loaded.
|
|
var callback = {
|
|
_set: microsummaries,
|
|
handleEvent: function(event) {
|
|
event.target.removeEventListener("load", this, false);
|
|
|
|
var page = getPageFromEvent(event);
|
|
|
|
if (page.content)
|
|
this._set.extractFromPage(page.uri, page.content);
|
|
|
|
// Purge the hidden iframe we used to load the page, if any.
|
|
if (page.container && page.container.parentNode)
|
|
page.container.parentNode.removeChild(page.container);
|
|
}
|
|
};
|
|
|
|
downloadPage(pageURI, callback);
|
|
}
|
|
|
|
return microsummaries;
|
|
},
|
|
|
|
#ifdef MOZ_PLACES
|
|
_getField: function MSS__getField(bookmarkID, fieldName) {
|
|
var pageURI = bookmarkID.QueryInterface(Ci.nsIURI);
|
|
var fieldValue;
|
|
|
|
switch(fieldName) {
|
|
case FIELD_MICSUM_EXPIRATION:
|
|
fieldValue = this._ans.getAnnotationInt64(pageURI, fieldName);
|
|
break;
|
|
case FIELD_MICSUM_GEN_URI:
|
|
case FIELD_GENERATED_TITLE:
|
|
case FIELD_CONTENT_TYPE:
|
|
default:
|
|
fieldValue = this._ans.getAnnotationString(pageURI, fieldName);
|
|
break;
|
|
}
|
|
|
|
return fieldValue;
|
|
},
|
|
|
|
_setField: function MSS__setField(bookmarkID, fieldName, fieldValue) {
|
|
var pageURI = bookmarkID.QueryInterface(Ci.nsIURI);
|
|
|
|
switch(fieldName) {
|
|
case FIELD_MICSUM_EXPIRATION:
|
|
this._ans.setAnnotationInt64(pageURI,
|
|
fieldName,
|
|
fieldValue,
|
|
0,
|
|
this._ans.EXPIRE_NEVER);
|
|
break;
|
|
case FIELD_MICSUM_GEN_URI:
|
|
case FIELD_GENERATED_TITLE:
|
|
case FIELD_CONTENT_TYPE:
|
|
default:
|
|
this._ans.setAnnotationString(pageURI,
|
|
fieldName,
|
|
fieldValue,
|
|
0,
|
|
this._ans.EXPIRE_NEVER);
|
|
break;
|
|
}
|
|
},
|
|
|
|
_clearField: function MSS__clearField(bookmarkID, fieldName) {
|
|
var pageURI = bookmarkID.QueryInterface(Ci.nsIURI);
|
|
|
|
this._ans.removeAnnotation(pageURI, fieldName);
|
|
},
|
|
|
|
_hasField: function MSS__hasField(bookmarkID, fieldName) {
|
|
var pageURI = bookmarkID.QueryInterface(Ci.nsIURI);
|
|
return this._ans.hasAnnotation(pageURI, fieldName);
|
|
},
|
|
|
|
_getPageForBookmark: function MSS__getPageForBookmark(bookmarkID) {
|
|
var pageURI = bookmarkID.QueryInterface(Ci.nsIURI);
|
|
|
|
return pageURI;
|
|
},
|
|
|
|
#else
|
|
_getField: function MSS__getField(bookmarkID, fieldName) {
|
|
var bookmarkResource = bookmarkID.QueryInterface(Ci.nsIRDFResource);
|
|
var fieldValue;
|
|
|
|
var node = this._bmds.GetTarget(bookmarkResource,
|
|
this._resource(fieldName),
|
|
true);
|
|
if (node)
|
|
fieldValue = node.QueryInterface(Ci.nsIRDFLiteral).Value;
|
|
else
|
|
fieldValue = null;
|
|
|
|
return fieldValue;
|
|
},
|
|
|
|
_setField: function MSS__setField(bookmarkID, fieldName, fieldValue) {
|
|
var bookmarkResource = bookmarkID.QueryInterface(Ci.nsIRDFResource);
|
|
|
|
if (this._hasField(bookmarkID, fieldName)) {
|
|
var oldValue = this._getField(bookmarkID, fieldName);
|
|
this._bmds.Change(bookmarkResource,
|
|
this._resource(fieldName),
|
|
this._literal(oldValue),
|
|
this._literal(fieldValue));
|
|
}
|
|
else {
|
|
this._bmds.Assert(bookmarkResource,
|
|
this._resource(fieldName),
|
|
this._literal(fieldValue),
|
|
true);
|
|
}
|
|
|
|
this._forceToolbarRebuild();
|
|
},
|
|
|
|
_clearField: function MSS__clearField(bookmarkID, fieldName) {
|
|
var bookmarkResource = bookmarkID.QueryInterface(Ci.nsIRDFResource);
|
|
|
|
var node = this._bmds.GetTarget(bookmarkResource,
|
|
this._resource(fieldName),
|
|
true);
|
|
if (node) {
|
|
this._bmds.Unassert(bookmarkResource,
|
|
this._resource(fieldName),
|
|
node);
|
|
this._forceToolbarRebuild();
|
|
}
|
|
},
|
|
|
|
_hasField: function MSS__hasField(bookmarkID, fieldName) {
|
|
var bookmarkResource = bookmarkID.QueryInterface(Ci.nsIRDFResource);
|
|
|
|
var node = this._bmds.GetTarget(bookmarkResource,
|
|
this._resource(fieldName),
|
|
true);
|
|
return node ? true : false;
|
|
},
|
|
|
|
/**
|
|
* Oy vey, a hack! Force the bookmarks toolbars to rebuild, since they don't
|
|
* seem to be able to do it correctly on their own right after we twiddle
|
|
* something microsummaryish (but they rebuild fine otherwise, incorporating
|
|
* all the microsummary changes upon next full rebuild, f.e. if you open
|
|
* a new window or shut down and restart your browser).
|
|
*
|
|
*/
|
|
_forceToolbarRebuild: function MSS__forceToolbarRebuild() {
|
|
var mediator = Cc["@mozilla.org/appshell/window-mediator;1"].
|
|
getService(Ci.nsIWindowMediator);
|
|
var windows = mediator.getEnumerator("navigator:browser");
|
|
while (windows.hasMoreElements()) {
|
|
var win = windows.getNext();
|
|
var bookmarksToolbar = win.document.getElementById("bookmarks-ptf");
|
|
bookmarksToolbar.builder.rebuild();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get the URI of the page to which a given bookmark refers.
|
|
*
|
|
* @param bookmarkResource
|
|
* an nsIResource uniquely identifying the bookmark
|
|
*
|
|
* @returns an nsIURI object representing the bookmark's page,
|
|
* or null if the bookmark doesn't exist
|
|
*
|
|
*/
|
|
_getPageForBookmark: function MSS__getPageForBookmark(bookmarkID) {
|
|
var bookmarkResource = bookmarkID.QueryInterface(Ci.nsIRDFResource);
|
|
|
|
var node = this._bmds.GetTarget(bookmarkResource,
|
|
this._resource(NC_NS + "URL"),
|
|
true);
|
|
|
|
if (!node)
|
|
return null;
|
|
|
|
var pageSpec = node.QueryInterface(Ci.nsIRDFLiteral).Value;
|
|
var pageURI = this._uri(pageSpec);
|
|
return pageURI;
|
|
},
|
|
#endif
|
|
|
|
/**
|
|
* Get the current microsummary for the given bookmark.
|
|
*
|
|
* @param bookmarkID
|
|
* the bookmark for which to get the current microsummary
|
|
*
|
|
* @returns the current microsummary for the bookmark, or null
|
|
* if the bookmark does not have a current microsummary
|
|
*
|
|
*/
|
|
getMicrosummary: function MSS_getMicrosummary(bookmarkID) {
|
|
if (!this.hasMicrosummary(bookmarkID))
|
|
return null;
|
|
|
|
var pageURI = this._getPageForBookmark(bookmarkID);
|
|
var genURI = this._uri(this._getField(bookmarkID, FIELD_MICSUM_GEN_URI));
|
|
|
|
var microsummary = new Microsummary(pageURI, genURI);
|
|
if (this._localGenerators[generatorURI.spec])
|
|
microsummary.generator = this._localGenerators[generatorURI.spec];
|
|
|
|
return microsummary;
|
|
},
|
|
|
|
/**
|
|
* Set the current microsummary for the given bookmark.
|
|
*
|
|
* @param bookmarkID
|
|
* the bookmark for which to set the current microsummary
|
|
*
|
|
* @param microsummary
|
|
* the microsummary to set as the current one
|
|
*
|
|
*/
|
|
setMicrosummary: function MSS_setMicrosummary(bookmarkID, microsummary) {
|
|
this._setField(bookmarkID, FIELD_MICSUM_GEN_URI, microsummary.generatorURI.spec);
|
|
|
|
// If the microsummary content has already been generated,
|
|
// set the URI's generated title to the microsummary content
|
|
// and expire the microsummary after the normal interval.
|
|
if (microsummary.content) {
|
|
var now = new Date().getTime();
|
|
this._setField(bookmarkID, FIELD_GENERATED_TITLE, microsummary.content);
|
|
this._setField(bookmarkID, FIELD_MICSUM_EXPIRATION, now + UPDATE_INTERVAL);
|
|
}
|
|
|
|
// Otherwise, expire the microsummary and update it immediately.
|
|
else {
|
|
if (this._hasField(bookmarkID, FIELD_MICSUM_EXPIRATION))
|
|
this._clearField(bookmarkID, FIELD_MICSUM_EXPIRATION);
|
|
microsummary.addObserver(this);
|
|
microsummary.update();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Remove the current microsummary for the given bookmark.
|
|
*
|
|
* @param bookmarkID
|
|
* the bookmark for which to remove the current microsummary
|
|
*
|
|
*/
|
|
removeMicrosummary: function MSS_removeMicrosummary(bookmarkID) {
|
|
var fields = [FIELD_MICSUM_GEN_URI,
|
|
FIELD_MICSUM_EXPIRATION,
|
|
FIELD_GENERATED_TITLE,
|
|
FIELD_CONTENT_TYPE];
|
|
|
|
for ( var i = 0; i < fields.length; i++ ) {
|
|
var field = fields[i];
|
|
if (this._hasField(bookmarkID, field))
|
|
this._clearField(bookmarkID, field);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Whether or not the given bookmark has a current microsummary.
|
|
*
|
|
* @param bookmarkID
|
|
* the bookmark for which to set the current microsummary
|
|
*
|
|
* @returns a boolean representing whether or not the given bookmark
|
|
* has a current microsummary
|
|
*
|
|
*/
|
|
hasMicrosummary: function MSS_hasMicrosummary(bookmarkID) {
|
|
return this._hasField(bookmarkID, FIELD_MICSUM_GEN_URI);
|
|
},
|
|
|
|
/**
|
|
* Whether or not the given microsummary is the current microsummary
|
|
* for the given bookmark.
|
|
*
|
|
* @param bookmarkID
|
|
* the bookmark to check
|
|
*
|
|
* @param microsummary
|
|
* the microsummary to check
|
|
*
|
|
* @returns whether or not the microsummary is the current one
|
|
* for the bookmark
|
|
*
|
|
*/
|
|
isMicrosummary: function MSS_isMicrosummary(bookmarkID, microsummary) {
|
|
if (!this.hasMicrosummary(bookmarkID))
|
|
return false;
|
|
|
|
var currentGen = this._getField(bookmarkID, FIELD_MICSUM_GEN_URI);
|
|
|
|
if (microsummary.generatorURI.equals(this._uri(currentGen)))
|
|
return true;
|
|
|
|
return false
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function Microsummary(pageURI, generatorURI) {
|
|
this._observers = [];
|
|
this.pageURI = pageURI;
|
|
this.generatorURI = generatorURI;
|
|
}
|
|
|
|
Microsummary.prototype = {
|
|
interfaces: [Ci.nsIMicrosummary, Ci.nsISupports],
|
|
|
|
// nsISupports
|
|
|
|
QueryInterface: function (iid) {
|
|
//if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
|
|
if (!iid.equals(Ci.nsIMicrosummary) &&
|
|
!iid.equals(Ci.nsISupports))
|
|
throw Components.results.NS_ERROR_NO_INTERFACE;
|
|
return this;
|
|
},
|
|
|
|
// nsIMicrosummary
|
|
|
|
_content: null,
|
|
get content() {
|
|
// If we have everything we need to generate the content, generate it.
|
|
if (this._content == null &&
|
|
this.generator &&
|
|
(this.pageContent || !this.generator.needsPageContent))
|
|
this._content = this.generator.generateMicrosummary(this.pageContent);
|
|
|
|
// Note: we return "null" if the content wasn't already generated and we
|
|
// couldn't retrieve it from the generated title annotation or generate it
|
|
// ourselves. So callers shouldn't count on getting content; instead,
|
|
// they should call update if the return value of this getter is "null",
|
|
// setting an observer to tell them when content generation is done.
|
|
return this._content;
|
|
},
|
|
set content(newValue) { this._content = newValue },
|
|
|
|
_generatorURI: null,
|
|
get generatorURI() { return this._generatorURI },
|
|
set generatorURI(newValue) { this._generatorURI = newValue },
|
|
|
|
_generator: null,
|
|
get generator() { return this._generator },
|
|
set generator(newValue) { this._generator = newValue },
|
|
|
|
_pageURI: null,
|
|
get pageURI() { return this._pageURI },
|
|
set pageURI(newValue) { this._pageURI = newValue },
|
|
|
|
_pageContent: null,
|
|
get pageContent() {
|
|
if (!this._pageContent) {
|
|
// If the page is currently loaded into a browser window, use that.
|
|
var pageContent = getLoadedPageContent(this.pageURI);
|
|
if (pageContent)
|
|
this._pageContent = pageContent;
|
|
}
|
|
|
|
return this._pageContent;
|
|
},
|
|
set pageContent(newValue) { this._pageContent = newValue },
|
|
|
|
// nsIMicrosummary
|
|
|
|
_observers: null,
|
|
|
|
addObserver: function MS_addObserver(observer) {
|
|
// Register the observer, but only if it isn't already registered,
|
|
// so that we don't call the same observer twice for any given change.
|
|
if (this._observers.indexOf(observer) == -1)
|
|
this._observers.push(observer);
|
|
},
|
|
|
|
removeObserver: function MS_removeObserver(observer) {
|
|
//NS_ASSERT(this._observers.indexOf(observer) != -1,
|
|
// "can't remove microsummary observer " + observer + ": not registered");
|
|
|
|
//this._observers =
|
|
// this._observers.filter(function(i) { observer != i });
|
|
if (this._observers.indexOf(observer) != -1)
|
|
this._observers.splice(this._observers.indexOf(observer), 1);
|
|
},
|
|
|
|
/**
|
|
* Regenerates the microsummary, asynchronously downloading its generator
|
|
* and content as needed.
|
|
*
|
|
*/
|
|
update: function MS_update() {
|
|
LOG("microsummary.update called for page:\n " + this.pageURI.spec +
|
|
"\nwith generator:\n " + this.generatorURI.spec);
|
|
|
|
// If we don't have the generator, download it now. After it downloads,
|
|
// we'll re-call this method to continue updating the microsummary.
|
|
if (!this.generator) {
|
|
LOG("generator not yet loaded; downloading it");
|
|
|
|
var generatorCallback = {
|
|
_self: this,
|
|
handleEvent: function(event) {
|
|
this._self._generatorLoadHandler(event);
|
|
event.target.removeEventListener("load", this, false);
|
|
this._self = null;
|
|
}
|
|
};
|
|
|
|
downloadXMLPage(this.generatorURI, generatorCallback);
|
|
return;
|
|
}
|
|
|
|
// If we need the page content, and we don't have it, download it now.
|
|
// Afterwards we'll re-call this method to continue updating the microsummary.
|
|
if (this.generator.needsPageContent && !this.pageContent) {
|
|
LOG("page content not yet loaded; downloading it");
|
|
|
|
var pageCallback = {
|
|
_self: this,
|
|
handleEvent: function(event) {
|
|
this._self._pageLoadHandler(event);
|
|
event.target.removeEventListener("load", this, false);
|
|
this._self = null;
|
|
}
|
|
};
|
|
|
|
downloadPage(this.pageURI, pageCallback);
|
|
return;
|
|
}
|
|
|
|
LOG("generator (and page, if needed) both loaded; generating microsummary");
|
|
|
|
// Now that we have both the generator and (if needed) the page content,
|
|
// generate the microsummary, then let the observers know about it.
|
|
this.content = this.generator.generateMicrosummary(this.pageContent);
|
|
this.pageContent = null;
|
|
for ( var i = 0; i < this._observers.length; i++ )
|
|
this._observers[i].onContentLoaded(this);
|
|
|
|
LOG("generated microsummary: " + this.content);
|
|
},
|
|
|
|
_generatorLoadHandler: function MS__generatorLoadHandler(event) {
|
|
var request = event.target;
|
|
var uri = request.channel.originalURI;
|
|
|
|
var generator = new MicrosummaryGenerator();
|
|
generator.uri = uri;
|
|
LOG(uri.spec + " microsummary generator downloaded");
|
|
|
|
if (request.responseXML) {
|
|
if (request.responseXML.documentElement.nodeName == "parsererror") {
|
|
// XXX Figure out the parsererror format and log a specific error.
|
|
LOG(uri.spec + " microsummary generator downloaded, but invalid XML");
|
|
return;
|
|
}
|
|
|
|
generator.initFromXML(request.responseXML.documentElement);
|
|
}
|
|
else if (request.responseText) {
|
|
generator.initFromText(request.responseText);
|
|
}
|
|
|
|
this.generator = generator;
|
|
this.update();
|
|
},
|
|
|
|
_pageLoadHandler: function MS__pageLoadHandler(event) {
|
|
var page = getPageFromEvent(event);
|
|
this.pageContent = page.content;
|
|
this.update();
|
|
|
|
// Purge the hidden iframe we used to load the page, if any.
|
|
if (page.container && page.container.parentNode)
|
|
page.container.parentNode.removeChild(page.container);
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function MicrosummaryGenerator() {}
|
|
|
|
MicrosummaryGenerator.prototype = {
|
|
|
|
// IO Service
|
|
__ios: null,
|
|
get _ios() {
|
|
if (!this.__ios)
|
|
this.__ios = Cc["@mozilla.org/network/io-service;1"].
|
|
getService(Ci.nsIIOService);
|
|
return this.__ios;
|
|
},
|
|
|
|
interfaces: [Ci.nsIMicrosummaryGenerator, Ci.nsISupports],
|
|
|
|
// nsISupports
|
|
|
|
QueryInterface: function (iid) {
|
|
//if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
|
|
if (!iid.equals(Ci.nsIMicrosummaryGenerator) &&
|
|
!iid.equals(Ci.nsISupports))
|
|
throw Components.results.NS_ERROR_NO_INTERFACE;
|
|
return this;
|
|
},
|
|
|
|
// nsIMicrosummaryGenerator
|
|
|
|
// Normally this is just the URL from which we download the generator,
|
|
// but for generators stored in the app or profile generators directory
|
|
// it's the value of the generator tag's sourceURI attribute.
|
|
_uri: null,
|
|
get uri() { return this._uri },
|
|
set uri(newValue) { this._uri = newValue },
|
|
|
|
// For generators bundled with the browser or installed by the user,
|
|
// the local URI is the URI of the local file containing the generator XML.
|
|
_localURI: null,
|
|
get localURI() { return this._localURI },
|
|
set localURI(newValue) { this._localURI = newValue },
|
|
|
|
_name: null,
|
|
get name() { return this._name },
|
|
set name(newValue) { this._name = newValue },
|
|
|
|
_template: null,
|
|
get template() { return this._template },
|
|
set template(newValue) { this._template = newValue },
|
|
|
|
_content: null,
|
|
get content() { return this._content },
|
|
set content(newValue) { this._content = newValue },
|
|
|
|
_rules: null,
|
|
|
|
/**
|
|
* Determines whether or not the generator applies to a given URI.
|
|
* By default, the generator does not apply to any URI. In order for it
|
|
* to apply to a URI, the URI must match one or more of the generator's
|
|
* "include" rules and not match any of the generator's "exclude" rules.
|
|
*
|
|
* @param uri
|
|
* the URI to test to see if this generator applies to it
|
|
*
|
|
* @returns boolean
|
|
* whether or not the generator applies to the given URI
|
|
*
|
|
*/
|
|
appliesToURI: function(uri) {
|
|
var applies = false;
|
|
|
|
for ( var i = 0 ; i < this._rules.length ; i++ ) {
|
|
var rule = this._rules[i];
|
|
|
|
switch (rule.type) {
|
|
case "include":
|
|
if (rule.regexp.test(uri.spec))
|
|
applies = true;
|
|
break;
|
|
case "exclude":
|
|
if (rule.regexp.test(uri.spec))
|
|
return false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return applies;
|
|
},
|
|
|
|
get needsPageContent() {
|
|
if (this.template)
|
|
return true;
|
|
else if (this.content)
|
|
return false;
|
|
else
|
|
throw("needsPageContent called on uninitialized microsummary generator");
|
|
},
|
|
|
|
/**
|
|
* Initializes a generator from text content. Generators initialized
|
|
* from text content merely return that content when their generate() method
|
|
* gets called.
|
|
*
|
|
* @param text
|
|
* the text content
|
|
*
|
|
*/
|
|
initFromText: function(text) {
|
|
this.content = text;
|
|
},
|
|
|
|
/**
|
|
* Initializes a generator from an XML description of it.
|
|
*
|
|
* @param node
|
|
* The XML DOM node containing the description. This node should be
|
|
* a "generator" element in the microsummaries namespace.
|
|
*
|
|
*/
|
|
initFromXML: function(xml) {
|
|
// XXX Make sure the argument is a DOM node, that its name is "generator",
|
|
// and that it is in the microsummaries namespace.
|
|
|
|
// XXX I would have wanted to retrieve the info from the XML via E4X,
|
|
// but we'll need to pass the XSLT transform sheet to the XSLT processor,
|
|
// and the processor can't deal with an E4X-wrapped template node.
|
|
|
|
this.name = xml.getAttribute("name");
|
|
|
|
// Only set the source URI from the XML if we have a local URI, i.e.
|
|
// if this is a locally-installed generator, since for remote generators
|
|
// the source URI of the generator is the URI from which we downloaded it.
|
|
if (this.localURI) {
|
|
//NS_ASSERT(xml.hasAttribute("sourceURI"), "local generator has no source URI");
|
|
this.uri = this._ios.newURI(xml.getAttribute("sourceURI"), null, null);
|
|
}
|
|
|
|
// Slurp the include/exclude rules that determine the pages to which
|
|
// this generator applies. Order is important, so we add the rules
|
|
// in the order in which they appear in the XML.
|
|
this._rules = [];
|
|
var pages = xml.getElementsByTagNameNS(MICSUM_NS, "pages")[0];
|
|
if (pages) {
|
|
// XXX Make sure the pages tag exists.
|
|
for ( var i = 0; i < pages.childNodes.length ; i++ ) {
|
|
var node = pages.childNodes[i];
|
|
if (node.nodeType != node.ELEMENT_NODE ||
|
|
node.namespaceURI != MICSUM_NS ||
|
|
(node.nodeName != "include" && node.nodeName != "exclude"))
|
|
continue;
|
|
this._rules.push({ type: node.nodeName, regexp: new RegExp(node.textContent) });
|
|
}
|
|
}
|
|
|
|
var templateNode = xml.getElementsByTagNameNS(MICSUM_NS, "template")[0];
|
|
if (templateNode) {
|
|
this.template = templateNode.getElementsByTagNameNS(XSLT_NS, "transform")[0]
|
|
|| templateNode.getElementsByTagNameNS(XSLT_NS, "stylesheet")[0];
|
|
}
|
|
// XXX Make sure the template is a valid XSL transform sheet.
|
|
},
|
|
|
|
generateMicrosummary: function MSD_generateMicrosummary(pageContent) {
|
|
if (this.content)
|
|
return this.content.replace(/^\s+|\s+$/g, "");
|
|
else if (this.template)
|
|
return this._processTemplate(pageContent);
|
|
else
|
|
throw("generateMicrosummary called on uninitialized microsummary generator");
|
|
},
|
|
|
|
_processTemplate: function MSD__processTemplate(doc) {
|
|
LOG("processing template " + this.template + " against document " + doc);
|
|
|
|
// XXX Should we just have one global instance of the processor?
|
|
var processor = Cc["@mozilla.org/document-transformer;1?type=xslt"].
|
|
createInstance(Ci.nsIXSLTProcessor);
|
|
processor.importStylesheet(this.template);
|
|
var fragment = processor.transformToFragment(doc, doc);
|
|
|
|
LOG("template processing result: " + fragment.textContent);
|
|
|
|
// XXX When we support HTML microsummaries we'll need to do something
|
|
// more sophisticated than just returning the text content of the fragment.
|
|
return fragment.textContent;
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Microsummary sets are collections of microsummaries. They allow callers
|
|
// to register themselves as observers of the set, and when any microsummary
|
|
// in the set changes, the observers get notified. Thus a caller can observe
|
|
// the set instead of each individual microsummary.
|
|
|
|
function MicrosummarySet() {
|
|
this._observers = [];
|
|
this._elements = [];
|
|
}
|
|
|
|
MicrosummarySet.prototype = {
|
|
|
|
// IO Service
|
|
__ios: null,
|
|
get _ios() {
|
|
if (!this.__ios)
|
|
this.__ios = Cc["@mozilla.org/network/io-service;1"].
|
|
getService(Ci.nsIIOService);
|
|
return this.__ios;
|
|
},
|
|
|
|
interfaces: [Ci.nsIMicrosummarySet,
|
|
Ci.nsIMicrosummaryObserver,
|
|
Ci.nsISupports],
|
|
|
|
QueryInterface: function (iid) {
|
|
//if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
|
|
if (!iid.equals(Ci.nsIMicrosummarySet) &&
|
|
!iid.equals(Ci.nsIMicrosummaryObserver) &&
|
|
!iid.equals(Ci.nsISupports))
|
|
throw Components.results.NS_ERROR_NO_INTERFACE;
|
|
return this;
|
|
},
|
|
|
|
_observers: null,
|
|
_elements: null,
|
|
|
|
// nsIMicrosummaryObserver
|
|
|
|
onContentLoaded: function MSSet_onContentLoaded(microsummary) {
|
|
for ( var i = 0; i < this._observers.length; i++ )
|
|
this._observers[i].onContentLoaded(microsummary);
|
|
},
|
|
|
|
// nsIMicrosummarySet
|
|
|
|
addObserver: function MSSet_addObserver(observer) {
|
|
if (this._observers.length == 0) {
|
|
for ( var i = 0 ; i < this._elements.length ; i++ )
|
|
this._elements[i].addObserver(this);
|
|
}
|
|
|
|
// Register the observer, but only if it isn't already registered,
|
|
// so that we don't call the same observer twice for any given change.
|
|
if (this._observers.indexOf(observer) == -1)
|
|
this._observers.push(observer);
|
|
},
|
|
|
|
removeObserver: function MSSet_removeObserver(observer) {
|
|
//NS_ASSERT(this._observers.indexOf(observer) != -1,
|
|
// "can't remove microsummary observer " + observer + ": not registered");
|
|
|
|
//this._observers =
|
|
// this._observers.filter(function(i) { observer != i });
|
|
if (this._observers.indexOf(observer) != -1)
|
|
this._observers.splice(this._observers.indexOf(observer), 1);
|
|
|
|
if (this._observers.length == 0) {
|
|
for ( var i = 0 ; i < this._elements.length ; i++ )
|
|
this._elements[i].removeObserver(this);
|
|
}
|
|
},
|
|
|
|
extractFromPage: function MSSet_extractFromPage(pageURI, pageContent) {
|
|
var links = pageContent.getElementsByTagName("LINK");
|
|
for ( var i = 0; i < links.length; i++ ) {
|
|
var link = links[i];
|
|
|
|
if (link.getAttribute("rel") != "microsummary")
|
|
continue;
|
|
|
|
// Unlike the "href" attribute, the "href" property contains
|
|
// an absolute URI spec, so we use it here to create the URI.
|
|
var genURI = this._ios.newURI(link.href, null, null);
|
|
|
|
var microsummary = new Microsummary(pageURI, genURI);
|
|
this.AppendElement(microsummary);
|
|
}
|
|
},
|
|
|
|
// XXX Turn this into a complete implementation of nsICollection?
|
|
AppendElement: function MSSet_AppendElement(element) {
|
|
// Query the element to a microsummary.
|
|
// XXX Should we NS_ASSERT if this fails?
|
|
element = element.QueryInterface(Ci.nsIMicrosummary);
|
|
|
|
if (this._elements.indexOf(element) == -1) {
|
|
this._elements.push(element);
|
|
element.addObserver(this);
|
|
}
|
|
|
|
// Notify observers that an element has been appended.
|
|
for ( var i = 0; i < this._observers.length; i++ )
|
|
this._observers[i].onElementAppended(element);
|
|
},
|
|
|
|
Enumerate: function MSSet_Enumerate() {
|
|
return new ArrayEnumerator(this._elements);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* An enumeration of items in a JS array.
|
|
* @constructor
|
|
*/
|
|
function ArrayEnumerator(aItems) {
|
|
this._index = 0;
|
|
if (aItems) {
|
|
for (var i = 0; i < aItems.length; ++i) {
|
|
if (!aItems[i])
|
|
aItems.splice(i, 1);
|
|
}
|
|
}
|
|
this._contents = aItems;
|
|
}
|
|
|
|
ArrayEnumerator.prototype = {
|
|
interfaces: [Ci.nsISimpleEnumerator, Ci.nsISupports],
|
|
|
|
QueryInterface: function (iid) {
|
|
//if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
|
|
if (!iid.equals(Ci.nsISimpleEnumerator) &&
|
|
!iid.equals(Ci.nsISupports))
|
|
throw Components.results.NS_ERROR_NO_INTERFACE;
|
|
return this;
|
|
},
|
|
|
|
_index: 0,
|
|
_contents: [],
|
|
|
|
hasMoreElements: function() {
|
|
return this._index < this._contents.length;
|
|
},
|
|
|
|
getNext: function() {
|
|
return this._contents[this._index++];
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function LOG(str) {
|
|
dump(str + "\n");
|
|
//var css = Cc['@mozilla.org/consoleservice;1'].
|
|
// getService(Ci.nsIConsoleService);
|
|
//css.logStringMessage("MsS: " + str)
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Get the document object for a page currently loaded into a browser window.
|
|
*
|
|
* @param pageURI
|
|
* the URI of the page
|
|
*
|
|
* @returns the document object, if the page is currently loaded
|
|
* into a browser window; otherwise null
|
|
*
|
|
*/
|
|
function getLoadedPageContent(pageURI) {
|
|
// Check open browser windows for the document, starting with the frontmost
|
|
// window (a.k.a. the most recent one).
|
|
var mediator = Cc["@mozilla.org/appshell/window-mediator;1"].
|
|
getService(Ci.nsIWindowMediator);
|
|
// Apparently the Z order enumerator is broken on Linux.
|
|
//var windows = mediator.getZOrderDOMWindowEnumerator("navigator:browser", true);
|
|
var windows = mediator.getEnumerator("navigator:browser");
|
|
while (windows.hasMoreElements()) {
|
|
var win = windows.getNext();
|
|
var tabBrowser = win.document.getElementById("content");
|
|
for ( i = 0; i < tabBrowser.browsers.length; i++ ) {
|
|
var browser = tabBrowser.browsers[i];
|
|
if (pageURI.equals(browser.currentURI))
|
|
return browser.contentDocument;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Download a page. This function checks the content type annotation
|
|
* for a clue how to load the file. It calls downloadXMLPage() for XML pages
|
|
* and downloadHTMLPage() otherwise.
|
|
*
|
|
* @param pageURI
|
|
* the URI of the page to download
|
|
*
|
|
* @param loadHandler
|
|
* the function to call when the page finishes downloading
|
|
*
|
|
*/
|
|
function downloadPage(pageURI, loadHandler) {
|
|
LOG(pageURI.spec + " downloading");
|
|
|
|
var contentType;
|
|
|
|
#ifdef MOZ_PLACES
|
|
var ans = Cc["@mozilla.org/browser/annotation-service;1"].
|
|
getService(Ci.nsIAnnotationService);
|
|
if (ans.hasAnnotation(pageURI, FIELD_CONTENT_TYPE))
|
|
contentType = ans.getAnnotationString(pageURI, FIELD_CONTENT_TYPE);
|
|
#endif
|
|
|
|
if (contentType && contentType.search(/xml$/) != -1)
|
|
downloadXMLPage(pageURI, loadHandler);
|
|
else
|
|
downloadHTMLPage(pageURI, loadHandler);
|
|
}
|
|
|
|
/**
|
|
* Download an XML page.
|
|
*
|
|
* @param pageURI
|
|
* the URI of the page to download
|
|
*
|
|
* @param loadHandler
|
|
* the function to call when the page finishes downloading
|
|
*
|
|
*/
|
|
function downloadXMLPage(pageURI, loadHandler) {
|
|
LOG(pageURI.spec + " downloading as XML");
|
|
|
|
var request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
|
|
|
|
if (loadHandler) {
|
|
request = request.QueryInterface(Ci.nsIDOMEventTarget);
|
|
request.addEventListener("load", loadHandler, false);
|
|
}
|
|
|
|
request = request.QueryInterface(Ci.nsIXMLHttpRequest);
|
|
request.open("GET", pageURI.spec, true);
|
|
request.send(null);
|
|
}
|
|
|
|
/**
|
|
* Download an HTML page.
|
|
*
|
|
* @param pageURI
|
|
* the URI of the page to download
|
|
*
|
|
* @param loadHandler
|
|
* the function to call when the page finishes downloading
|
|
*
|
|
*/
|
|
function downloadHTMLPage(pageURI, loadHandler) {
|
|
LOG(pageURI.spec + " downloading as HTML");
|
|
|
|
// We download HTML pages via hidden iframes in browser windows.
|
|
// According to bz, this is the best way to do it, since bug 102699
|
|
// is hard to fix.
|
|
var windowMediator = Cc['@mozilla.org/appshell/window-mediator;1'].
|
|
getService(Ci.nsIWindowMediator);
|
|
var window = windowMediator.getMostRecentWindow("navigator:browser");
|
|
// XXX We can use other windows, too, so perhaps we should try
|
|
// to get some other window if there's no browser window open.
|
|
if (!window)
|
|
throw(pageURI.spec + " can't download as HTML; no browser window");
|
|
var document = window.document;
|
|
var mainWindow = document.getElementById('main-window');
|
|
|
|
var iframe = document.createElement('iframe');
|
|
iframe.setAttribute("collapsed", true);
|
|
iframe.addEventListener("load", loadHandler, true);
|
|
mainWindow.appendChild(iframe);
|
|
|
|
// Now that we've added the iframe to the window, we can access docShell
|
|
// to turn off JavaScript for security and turn off images to reduce load
|
|
// on the network.
|
|
iframe.docShell.allowJavascript = false;
|
|
iframe.docShell.allowImages = false;
|
|
|
|
// Attach the URI to the iframe so we can retrieve it later.
|
|
iframe.originalURI = pageURI;
|
|
|
|
// Kick off the download.
|
|
iframe.setAttribute('src', pageURI.spec);
|
|
}
|
|
|
|
/**
|
|
* Get the original URI and the document object from a page load event.
|
|
* Abstracts away the differences between the XMLHttpRequest and hidden iframe
|
|
* approaches to loading pages.
|
|
*
|
|
* @param event
|
|
* the page load event from which to get the URI and doc object
|
|
*
|
|
* @returns A page object containing the page URI, content, and container.
|
|
* The container is the hidden iframe the page was loaded in; we have
|
|
* to pass that back so the caller can delete it after processing
|
|
* the document, since the document would get screwed up if we deleted
|
|
* the iframe here.
|
|
*
|
|
*/
|
|
function getPageFromEvent(event) {
|
|
var page = {};
|
|
|
|
if (event.target.channel) {
|
|
// this was an XMLHttpRequest page load
|
|
|
|
var request = event.target;
|
|
page.uri = request.channel.originalURI;
|
|
|
|
if (!request.responseXML) {
|
|
throw(page.uri.spec + " page downloaded as XML, but not XML");
|
|
// XXX Update the page's content type annotation
|
|
// XXX Do something about pages with an XML content type
|
|
// but which aren't XML?
|
|
}
|
|
|
|
page.content = request.responseXML;
|
|
|
|
if (page.content.documentElement.nodeName == "parsererror") {
|
|
throw(page.uri.spec + " page downloaded, but invalid XML");
|
|
// XXX Figure out the parsererror format and log a specific error.
|
|
}
|
|
}
|
|
else if (event.currentTarget && event.currentTarget.nodeName &&
|
|
event.currentTarget.nodeName == "iframe") {
|
|
// this was a hidden iframe page load
|
|
|
|
// Make sure this event doesn't reach any other load handlers
|
|
// intended for visible document frames in the browser window.
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
var iframe = event.currentTarget;
|
|
|
|
page.uri = iframe.originalURI;
|
|
page.content = iframe.contentDocument;
|
|
page.container = iframe;
|
|
}
|
|
|
|
#ifdef MOZ_PLACES
|
|
// This is as good a time as any to update the page's content type
|
|
// in the annotations database. XXX But should we only do this for pages
|
|
// being summarized? If we do it here, we'll do it for generators too.
|
|
var ans = Cc["@mozilla.org/browser/annotation-service;1"].
|
|
getService(Ci.nsIAnnotationService);
|
|
ans.setAnnotationString(page.uri,
|
|
FIELD_CONTENT_TYPE,
|
|
page.content.contentType,
|
|
0,
|
|
ans.EXPIRE_NEVER);
|
|
#endif
|
|
|
|
return page;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// From http://lxr.mozilla.org/mozilla/source/browser/components/search/nsSearchService.js
|
|
|
|
/**
|
|
* Removes all characters not in the "chars" string from aName.
|
|
*
|
|
* @returns a sanitized name to be used as a filename, or a random name
|
|
* if a sanitized name cannot be obtained (if aName contains
|
|
* no valid characters).
|
|
*/
|
|
function sanitizeName(aName) {
|
|
const chars = "-abcdefghijklmnopqrstuvwxyz0123456789";
|
|
|
|
var name = aName.toLowerCase();
|
|
name = name.replace(/ /g, "-");
|
|
//name = name.split("").filter(function (el) {
|
|
// return chars.indexOf(el) != -1;
|
|
// }).join("");
|
|
var filteredName = "";
|
|
for ( var i = 0 ; i < name.length ; i++ )
|
|
if (chars.indexOf(name[i]) != -1)
|
|
filteredName += name[i];
|
|
name = filteredName;
|
|
|
|
if (!name) {
|
|
// Our input had no valid characters - use a random name
|
|
for (var i = 0; i < 8; ++i)
|
|
name += chars.charAt(Math.round(Math.random() * (chars.length - 1)));
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 Components.results.NS_ERROR_NOT_IMPLEMENTED;
|
|
|
|
for (var key in this._objects) {
|
|
if (cid.equals(this._objects[key].CID))
|
|
return this._objects[key].factory;
|
|
}
|
|
|
|
throw Components.results.NS_ERROR_NO_INTERFACE;
|
|
},
|
|
|
|
_objects: {
|
|
service: {
|
|
CID : Components.ID("{460a9792-b154-4f26-a922-0f653e2c8f91}"),
|
|
contractID : "@mozilla.org/microsummary/service;1",
|
|
className : "Microsummary Service",
|
|
factory : MicrosummaryServiceFactory = {
|
|
createInstance: function(aOuter, aIID) {
|
|
if (aOuter != null)
|
|
throw Components.results.NS_ERROR_NO_AGGREGATION;
|
|
var svc = new MicrosummaryService();
|
|
svc._init();
|
|
return svc.QueryInterface(aIID);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
canUnload: function(componentManager) {
|
|
return true;
|
|
}
|
|
};
|
|
|
|
function NSGetModule(compMgr, fileSpec) {
|
|
return gModule;
|
|
}
|