<commit_before>/**
 * @fileOverview Loader module for restartless addons
 * @author       YUKI "Piro" Hiroshi
 * @version      6
 *
 * @license
 *   The MIT License, Copyright (c) 2010-2012 YUKI "Piro" Hiroshi.
 *   https://github.com/piroor/restartless/blob/master/license.txt
 * @url http://github.com/piroor/restartless
 */

/** You can customize shared properties for loaded scripts. */
var Application = (function() {
	if ('@mozilla.org/fuel/application;1' in Components.classes)
		return Components.classes['@mozilla.org/fuel/application;1']
				.getService(Components.interfaces.fuelIApplication);
	if ('@mozilla.org/steel/application;1' in Components.classes)
		return Components.classes['@mozilla.org/steel/application;1']
				.getService(Components.interfaces.steelIApplication);
	return null;
})();
var _namespacePrototype = {
		Cc : Components.classes,
		Ci : Components.interfaces,
		Cu : Components.utils,
		Cr : Components.results,
		Application : Application
	};
var _namespaces;

/**
 * This functiom loads specified script into a unique namespace for the URL.
 * Namespaces for loaded scripts have a wrapped version of this function.
 * Both this and wrapped work like as Components.utils.import().
 * Due to the reserved symbol "import", we have to use another name "load"
 * instead it.
 *
 * @param {String} aScriptURL
 *   URL of a script. Wrapped version of load() can handle related path.

 *   Related path will be resolved based on the location of the caller script.
 * @param {Object=} aExportTargetForImport
 *   EXPORTED_SYMBOLS in the loaded script will be exported to the object.
 *   If no object is specified, symbols will be exported to the global object
 *   of the caller.
 * @param {Object=} aExportTargetForRequire
 *   Properties of "exports" in the loaded script will be exported to the object.
 *
 * @returns {Object}
 *   The global object for the loaded script.
 */
function load(aURISpec, aExportTargetForImport, aExportTargetForRequire, aRoot)
{
	if (!_namespaces)
		_namespaces = {};
	var ns;
	if (aURISpec in _namespaces) {
		ns = _namespaces[aURISpec];
		_exportForImport(ns, aExportTargetForImport);
		_exportForRequire(ns, aExportTargetForRequire);
		return ns;
	}
	ns = _createNamespace(aURISpec, aRoot || aURISpec);
	try {
		Components.classes['@mozilla.org/moz/jssubscript-loader;1']
			.getService(Components.interfaces.mozIJSSubScriptLoader)
			.loadSubScript(aURISpec, ns);
	}
	catch(e) {
		let message = 'Loader::load('+aURISpec+') failed!\n'+e+'\n';
		dump(message);
		Components.utils.reportError(message + e.stack);
		throw e;
	}
	_exportForImport(ns, aExportTargetForImport);
	_exportForRequire(ns, aExportTargetForRequire);
	return _namespaces[aURISpec] = ns;
}

// JavaScript code module style
function _exportForImport(aSource, aTarget)
{
	if (
		!aTarget ||
		!('EXPORTED_SYMBOLS' in aSource) ||
		!aSource.EXPORTED_SYMBOLS ||
		!aSource.EXPORTED_SYMBOLS.forEach
		)
		return;
	for each (var symbol in aSource.EXPORTED_SYMBOLS)
	{
		aTarget[symbol] = aSource[symbol];
	}
}

// CommonJS style
function _exportForRequire(aSource, aTarget)
{
	if (
		!aTarget ||
		!('exports' in aSource) ||
		!aSource.exports ||
		typeof aSource.exports != 'object'
		)
		return;
	for (var symbol in aSource.exports)
	{
		aTarget[symbol] = aSource.exports[symbol];
	}
}

var IOService = Components.classes['@mozilla.org/network/io-service;1']
					.getService(Components.interfaces.nsIIOService);
var FileHandler = IOService.getProtocolHandler('file')
					.QueryInterface(Components.interfaces.nsIFileProtocolHandler);

/**
 * Checks existence of the file specified by the given relative path and the base URI.
 *
 * @param {String} aPath
 *   A relative path to a file or directory, from the aBaseURI.
 * @param {String} aBaseURI
 *   An absolute URI (with scheme) for relative paths.
 *
 * @returns {String}
 *   If the file (or directory) exists, returns the absolute URI. Otherwise null.
 */
function exists(aPath, aBaseURI)
{
	if (/^\w+:/.test(aPath)) {
		let leafName = aPath.match(/([^\/]+)$/);
		leafName = leafName ? leafName[1] : '' ;
		aBaseURI = aPath.replace(/(?:[^\/]+)$/, '');
		aPath = leafName;
	}
	var baseURI = aBaseURI.indexOf('file:') == 0 ?
					IOService.newFileURI(FileHandler.getFileFromURLSpec(aBaseURI)) :
					IOService.newURI(aBaseURI, null, null);
	if (aBaseURI.indexOf('jar:') == 0) {
		baseURI = baseURI.QueryInterface(Components.interfaces.nsIJARURI);
		var reader = Components.classes['@mozilla.org/libjar/zip-reader;1']
						.createInstance(Components.interfaces.nsIZipReader);
		reader.open(baseURI.JARFile.QueryInterface(Components.interfaces.nsIFileURL).file);
	    try {
			let baseEntry = baseURI.JAREntry.replace(/[^\/]+$/, '');
			let entries = reader.findEntries(baseEntry + aPath + '$');
			let found = entries.hasMore();
			return found ? baseURI.resolve(aPath) : null ;
		}
		finally {
			reader.close();
		}
	}
	else {
		let resolved = baseURI.resolve(aPath);
		return FileHandler.getFileFromURLSpec(resolved).exists() ? resolved : null ;
	}
}

function doAndWait(aAsyncTask)
{
	const Cc = Components.classes;
	const Ci = Components.interfaces;

	var done = false;
	var returnedValue = void(0);
	var continuation = function(aReturnedValue) {
			done = true;
			returnedValue = aReturnedValue;
		};

	var timer = Cc['@mozilla.org/timer;1']
					.createInstance(Ci.nsITimer);
	timer.init(function() {
		aAsyncTask(continuation);
	}, 0, Ci.nsITimer.TYPE_ONE_SHOT);

	var thread = Cc['@mozilla.org/thread-manager;1']
					.getService(Ci.nsIThreadManager)
					.currentThread;
	while (!done) {
		thread.processNextEvent(true);
	}
	return returnedValue;
}

function _readFrom(aURISpec, aEncoding)
{
	const Cc = Components.classes;
	const Ci = Components.interfaces;

	var uri = aURISpec.indexOf('file:') == 0 ?
			IOService.newFileURI(FileHandler.getFileFromURLSpec(aURISpec)) :
			IOService.newURI(aURISpec, null, null) ;
	var channel = IOService.newChannelFromURI(uri.QueryInterface(Ci.nsIURI));
	var stream = channel.open();

	var fileContents = null;
	try {
		if (aEncoding) {
			var converterStream = Cc['@mozilla.org/intl/converter-input-stream;1']
					.createInstance(Ci.nsIConverterInputStream);
			var buffer = stream.available();
			converterStream.init(stream, aEncoding, buffer,
				converterStream.DEFAULT_REPLACEMENT_CHARACTER);
			var out = { value : null };
			converterStream.readString(stream.available(), out);
			converterStream.close();
			fileContents = out.value;
		}
		else {
			var scriptableStream = Cc['@mozilla.org/scriptableinputstream;1']
					.createInstance(Ci.nsIScriptableInputStream);
			scriptableStream.init(stream);
			fileContents = scriptableStream.read(scriptableStream.available());
			scriptableStream.close();
		}
	}
	finally {
		stream.close();
	}
	return fileContents;
}

function _createNamespace(aURISpec, aRoot)
{
	var baseURI = aURISpec.indexOf('file:') == 0 ?
					IOService.newFileURI(FileHandler.getFileFromURLSpec(aURISpec)) :
					IOService.newURI(aURISpec, null, null);
	var rootURI = typeof aRoot == 'string' ?
					(aRoot.indexOf('file:') == 0 ?
						IOService.newFileURI(FileHandler.getFileFromURLSpec(aRoot)) :
						IOService.newURI(aRoot, null, null)
					) :
					aRoot ;
	var ns = {
			__proto__ : _namespacePrototype,
			location : _createFakeLocation(baseURI),
			exists : function(aPath, aBase) {
				return exists(aPath, aBase || baseURI.spec);
			},
			exist : function(aPath, aBase) { // alias
				return exists(aPath, aBase || baseURI.spec);
			},
			/** JavaScript code module style */
			load : function(aURISpec, aExportTarget) {
				if (!/\.jsm?$/.test(aURISpec)) {
					if (exists(aURISpec+'.js', baseURI.spec))
						aURISpec += '.js'
					else if (exists(aURISpec+'.jsm', baseURI.spec))
						aURISpec += '.jsm'
				}
				var resolved = baseURI.resolve(aURISpec);
				if (resolved == aURISpec)
					throw new Error('Recursive load!');
				return load(resolved, aExportTarget || ns, aExportTarget, rootURI);
			},
			'import' : function() { // alias
				return this.load.apply(this, arguments);
			},
			/**
			 * CommonJS style
			 * @url http://www.commonjs.org/specs/
			 */
			require : function(aURISpec) {
				if (!/\.jsm?$/.test(aURISpec)) {
					if (exists(aURISpec+'.js', baseURI.spec))
						aURISpec += '.js'
					else if (exists(aURISpec+'.jsm', baseURI.spec))
						aURISpec += '.jsm'
				}
				var resolved = (aURISpec.charAt(0) == '.' ? rootURI : baseURI ).resolve(aURISpec);
				if (resolved == aURISpec)
					throw new Error('Recursive load!');
				var exported = {};
				load(resolved, exported, exported, rootURI);
				return exported;
			},
			/* utility to resolve relative path from the file */
			resolve : function(aURISpec, aBaseURI) {
				var base = !aBaseURI ?
								baseURI :
							aBaseURI.indexOf('file:') == 0 ?
								IOService.newFileURI(FileHandler.getFileFromURLSpec(aURISpec)) :
								IOService.newURI(aURISpec, null, null) ;
				return base.resolve(aURISpec);
			},
			/* utility to read contents of a text file */
			read : function(aURISpec, aEncoding, aBaseURI) {
				return _readFrom(this.resolve(aURISpec, aBaseURI), aEncoding);
			},
			doAndWait : function(aAsyncTask) {
				return doAndWait(aAsyncTask);
			},
			exports : {}
		};
	return ns;
}

function _createFakeLocation(aURI)
{
	aURI = aURI.QueryInterface(Components.interfaces.nsIURL)
					.QueryInterface(Components.interfaces.nsIURI);
	return {
		href     : aURI.spec,
		search   : aURI.query ? '?'+aURI.query : '' ,
		hash     : aURI.ref ? '#'+aURI.ref : '' ,
		host     : aURI.scheme == 'jar' ? '' : aURI.hostPort,
		hostname : aURI.scheme == 'jar' ? '' : aURI.host,
		port     : aURI.scheme == 'jar' ? -1 : aURI.port,
		pathname : aURI.path,
		protocol : aURI.scheme+':',
		reload   : function() {},
		replace  : function() {},
		toString : function() {
			return this.href;
		}
	};
}

function _callHandler(aHandler, aReason)
{
	for (var i in _namespaces)
	{
		try {
			if (_namespaces[i][aHandler] &&
				typeof _namespaces[i][aHandler] == 'function')
				_namespaces[i][aHandler](aReason);
		}
		catch(e) {
			let message = i+'('+aHandler+', '+aReason+')\n'+e+'\n';
			dump(message);
			Components.utils.reportError(message + e.stack);
		}
	}
}

function registerResource(aName, aRoot)
{
	IOService.getProtocolHandler('resource')
		.QueryInterface(Components.interfaces.nsIResProtocolHandler)
		.setSubstitution(aName, aRoot);
}

function unregisterResource(aName)
{
	IOService.getProtocolHandler('resource')
		.QueryInterface(Components.interfaces.nsIResProtocolHandler)
		.setSubstitution(aName, null);
}

/** Handler for "install" of the bootstrap.js */
function install(aReason)
{
	_callHandler('install', aReason);
}

/** Handler for "uninstall" of the bootstrap.js */
function uninstall(aReason)
{
	_callHandler('uninstall', aReason);
}

/** Handler for "shutdown" of the bootstrap.js */
function shutdown(aReason)
{
	_callHandler('shutdown', aReason);

	for each (let ns in _namespaces)
	{
		for (let i in ns.exports)
		{
			if (ns.exports.hasOwnProperty(i))
				delete ns.exports[i];
		}
	}
	_namespaces = void(0);
	_namespacePrototype = void(0);
	Application = void(0);

	IOService = void(0);
	FileHandler = void(0);

	load = void(0);
	_exportSymbols = void(0);
	exists = void(0);
	_createNamespace = void(0);
	_callHandler = void(0);
	registerResource = void(0);
	unregisterResource = void(0);
	install = void(0);
	uninstall = void(0);
	shutdown = void(0);
}
<commit_msg>Export btoa and atob<commit_after>/**
 * @fileOverview Loader module for restartless addons
 * @author       YUKI "Piro" Hiroshi
 * @version      7
 *
 * @license
 *   The MIT License, Copyright (c) 2010-2014 YUKI "Piro" Hiroshi.
 *   https://github.com/piroor/restartless/blob/master/license.txt
 * @url http://github.com/piroor/restartless
 */

/** You can customize shared properties for loaded scripts. */
var Application = (function() {
	if ('@mozilla.org/fuel/application;1' in Components.classes)
		return Components.classes['@mozilla.org/fuel/application;1']
				.getService(Components.interfaces.fuelIApplication);
	if ('@mozilla.org/steel/application;1' in Components.classes)
		return Components.classes['@mozilla.org/steel/application;1']
				.getService(Components.interfaces.steelIApplication);
	return null;
})();
var _namespacePrototype = {
		Cc : Components.classes,
		Ci : Components.interfaces,
		Cu : Components.utils,
		Cr : Components.results,
		Application : Application,
		btoa: function(aInput) {
		  return btoa(aInput);
		},
		atob: function(aInput) {
		  return atob(aInput);
		}
	};
var _namespaces;

/**
 * This functiom loads specified script into a unique namespace for the URL.
 * Namespaces for loaded scripts have a wrapped version of this function.
 * Both this and wrapped work like as Components.utils.import().
 * Due to the reserved symbol "import", we have to use another name "load"
 * instead it.
 *
 * @param {String} aScriptURL
 *   URL of a script. Wrapped version of load() can handle related path.

 *   Related path will be resolved based on the location of the caller script.
 * @param {Object=} aExportTargetForImport
 *   EXPORTED_SYMBOLS in the loaded script will be exported to the object.
 *   If no object is specified, symbols will be exported to the global object
 *   of the caller.
 * @param {Object=} aExportTargetForRequire
 *   Properties of "exports" in the loaded script will be exported to the object.
 *
 * @returns {Object}
 *   The global object for the loaded script.
 */
function load(aURISpec, aExportTargetForImport, aExportTargetForRequire, aRoot)
{
	if (!_namespaces)
		_namespaces = {};
	var ns;
	if (aURISpec in _namespaces) {
		ns = _namespaces[aURISpec];
		_exportForImport(ns, aExportTargetForImport);
		_exportForRequire(ns, aExportTargetForRequire);
		return ns;
	}
	ns = _createNamespace(aURISpec, aRoot || aURISpec);
	try {
		Components.classes['@mozilla.org/moz/jssubscript-loader;1']
			.getService(Components.interfaces.mozIJSSubScriptLoader)
			.loadSubScript(aURISpec, ns);
	}
	catch(e) {
		let message = 'Loader::load('+aURISpec+') failed!\n'+e+'\n';
		dump(message);
		Components.utils.reportError(message + e.stack);
		throw e;
	}
	_exportForImport(ns, aExportTargetForImport);
	_exportForRequire(ns, aExportTargetForRequire);
	return _namespaces[aURISpec] = ns;
}

// JavaScript code module style
function _exportForImport(aSource, aTarget)
{
	if (
		!aTarget ||
		!('EXPORTED_SYMBOLS' in aSource) ||
		!aSource.EXPORTED_SYMBOLS ||
		!aSource.EXPORTED_SYMBOLS.forEach
		)
		return;
	for each (var symbol in aSource.EXPORTED_SYMBOLS)
	{
		aTarget[symbol] = aSource[symbol];
	}
}

// CommonJS style
function _exportForRequire(aSource, aTarget)
{
	if (
		!aTarget ||
		!('exports' in aSource) ||
		!aSource.exports ||
		typeof aSource.exports != 'object'
		)
		return;
	for (var symbol in aSource.exports)
	{
		aTarget[symbol] = aSource.exports[symbol];
	}
}

var IOService = Components.classes['@mozilla.org/network/io-service;1']
					.getService(Components.interfaces.nsIIOService);
var FileHandler = IOService.getProtocolHandler('file')
					.QueryInterface(Components.interfaces.nsIFileProtocolHandler);

/**
 * Checks existence of the file specified by the given relative path and the base URI.
 *
 * @param {String} aPath
 *   A relative path to a file or directory, from the aBaseURI.
 * @param {String} aBaseURI
 *   An absolute URI (with scheme) for relative paths.
 *
 * @returns {String}
 *   If the file (or directory) exists, returns the absolute URI. Otherwise null.
 */
function exists(aPath, aBaseURI)
{
	if (/^\w+:/.test(aPath)) {
		let leafName = aPath.match(/([^\/]+)$/);
		leafName = leafName ? leafName[1] : '' ;
		aBaseURI = aPath.replace(/(?:[^\/]+)$/, '');
		aPath = leafName;
	}
	var baseURI = aBaseURI.indexOf('file:') == 0 ?
					IOService.newFileURI(FileHandler.getFileFromURLSpec(aBaseURI)) :
					IOService.newURI(aBaseURI, null, null);
	if (aBaseURI.indexOf('jar:') == 0) {
		baseURI = baseURI.QueryInterface(Components.interfaces.nsIJARURI);
		var reader = Components.classes['@mozilla.org/libjar/zip-reader;1']
						.createInstance(Components.interfaces.nsIZipReader);
		reader.open(baseURI.JARFile.QueryInterface(Components.interfaces.nsIFileURL).file);
	    try {
			let baseEntry = baseURI.JAREntry.replace(/[^\/]+$/, '');
			let entries = reader.findEntries(baseEntry + aPath + '$');
			let found = entries.hasMore();
			return found ? baseURI.resolve(aPath) : null ;
		}
		finally {
			reader.close();
		}
	}
	else {
		let resolved = baseURI.resolve(aPath);
		return FileHandler.getFileFromURLSpec(resolved).exists() ? resolved : null ;
	}
}

function doAndWait(aAsyncTask)
{
	const Cc = Components.classes;
	const Ci = Components.interfaces;

	var done = false;
	var returnedValue = void(0);
	var continuation = function(aReturnedValue) {
			done = true;
			returnedValue = aReturnedValue;
		};

	var timer = Cc['@mozilla.org/timer;1']
					.createInstance(Ci.nsITimer);
	timer.init(function() {
		aAsyncTask(continuation);
	}, 0, Ci.nsITimer.TYPE_ONE_SHOT);

	var thread = Cc['@mozilla.org/thread-manager;1']
					.getService(Ci.nsIThreadManager)
					.currentThread;
	while (!done) {
		thread.processNextEvent(true);
	}
	return returnedValue;
}

function _readFrom(aURISpec, aEncoding)
{
	const Cc = Components.classes;
	const Ci = Components.interfaces;

	var uri = aURISpec.indexOf('file:') == 0 ?
			IOService.newFileURI(FileHandler.getFileFromURLSpec(aURISpec)) :
			IOService.newURI(aURISpec, null, null) ;
	var channel = IOService.newChannelFromURI(uri.QueryInterface(Ci.nsIURI));
	var stream = channel.open();

	var fileContents = null;
	try {
		if (aEncoding) {
			var converterStream = Cc['@mozilla.org/intl/converter-input-stream;1']
					.createInstance(Ci.nsIConverterInputStream);
			var buffer = stream.available();
			converterStream.init(stream, aEncoding, buffer,
				converterStream.DEFAULT_REPLACEMENT_CHARACTER);
			var out = { value : null };
			converterStream.readString(stream.available(), out);
			converterStream.close();
			fileContents = out.value;
		}
		else {
			var scriptableStream = Cc['@mozilla.org/scriptableinputstream;1']
					.createInstance(Ci.nsIScriptableInputStream);
			scriptableStream.init(stream);
			fileContents = scriptableStream.read(scriptableStream.available());
			scriptableStream.close();
		}
	}
	finally {
		stream.close();
	}
	return fileContents;
}

function _createNamespace(aURISpec, aRoot)
{
	var baseURI = aURISpec.indexOf('file:') == 0 ?
					IOService.newFileURI(FileHandler.getFileFromURLSpec(aURISpec)) :
					IOService.newURI(aURISpec, null, null);
	var rootURI = typeof aRoot == 'string' ?
					(aRoot.indexOf('file:') == 0 ?
						IOService.newFileURI(FileHandler.getFileFromURLSpec(aRoot)) :
						IOService.newURI(aRoot, null, null)
					) :
					aRoot ;
	var ns = {
			__proto__ : _namespacePrototype,
			location : _createFakeLocation(baseURI),
			exists : function(aPath, aBase) {
				return exists(aPath, aBase || baseURI.spec);
			},
			exist : function(aPath, aBase) { // alias
				return exists(aPath, aBase || baseURI.spec);
			},
			/** JavaScript code module style */
			load : function(aURISpec, aExportTarget) {
				if (!/\.jsm?$/.test(aURISpec)) {
					if (exists(aURISpec+'.js', baseURI.spec))
						aURISpec += '.js'
					else if (exists(aURISpec+'.jsm', baseURI.spec))
						aURISpec += '.jsm'
				}
				var resolved = baseURI.resolve(aURISpec);
				if (resolved == aURISpec)
					throw new Error('Recursive load!');
				return load(resolved, aExportTarget || ns, aExportTarget, rootURI);
			},
			'import' : function() { // alias
				return this.load.apply(this, arguments);
			},
			/**
			 * CommonJS style
			 * @url http://www.commonjs.org/specs/
			 */
			require : function(aURISpec) {
				if (!/\.jsm?$/.test(aURISpec)) {
					if (exists(aURISpec+'.js', baseURI.spec))
						aURISpec += '.js'
					else if (exists(aURISpec+'.jsm', baseURI.spec))
						aURISpec += '.jsm'
				}
				var resolved = (aURISpec.charAt(0) == '.' ? rootURI : baseURI ).resolve(aURISpec);
				if (resolved == aURISpec)
					throw new Error('Recursive load!');
				var exported = {};
				load(resolved, exported, exported, rootURI);
				return exported;
			},
			/* utility to resolve relative path from the file */
			resolve : function(aURISpec, aBaseURI) {
				var base = !aBaseURI ?
								baseURI :
							aBaseURI.indexOf('file:') == 0 ?
								IOService.newFileURI(FileHandler.getFileFromURLSpec(aURISpec)) :
								IOService.newURI(aURISpec, null, null) ;
				return base.resolve(aURISpec);
			},
			/* utility to read contents of a text file */
			read : function(aURISpec, aEncoding, aBaseURI) {
				return _readFrom(this.resolve(aURISpec, aBaseURI), aEncoding);
			},
			doAndWait : function(aAsyncTask) {
				return doAndWait(aAsyncTask);
			},
			exports : {}
		};
	return ns;
}

function _createFakeLocation(aURI)
{
	aURI = aURI.QueryInterface(Components.interfaces.nsIURL)
					.QueryInterface(Components.interfaces.nsIURI);
	return {
		href     : aURI.spec,
		search   : aURI.query ? '?'+aURI.query : '' ,
		hash     : aURI.ref ? '#'+aURI.ref : '' ,
		host     : aURI.scheme == 'jar' ? '' : aURI.hostPort,
		hostname : aURI.scheme == 'jar' ? '' : aURI.host,
		port     : aURI.scheme == 'jar' ? -1 : aURI.port,
		pathname : aURI.path,
		protocol : aURI.scheme+':',
		reload   : function() {},
		replace  : function() {},
		toString : function() {
			return this.href;
		}
	};
}

function _callHandler(aHandler, aReason)
{
	for (var i in _namespaces)
	{
		try {
			if (_namespaces[i][aHandler] &&
				typeof _namespaces[i][aHandler] == 'function')
				_namespaces[i][aHandler](aReason);
		}
		catch(e) {
			let message = i+'('+aHandler+', '+aReason+')\n'+e+'\n';
			dump(message);
			Components.utils.reportError(message + e.stack);
		}
	}
}

function registerResource(aName, aRoot)
{
	IOService.getProtocolHandler('resource')
		.QueryInterface(Components.interfaces.nsIResProtocolHandler)
		.setSubstitution(aName, aRoot);
}

function unregisterResource(aName)
{
	IOService.getProtocolHandler('resource')
		.QueryInterface(Components.interfaces.nsIResProtocolHandler)
		.setSubstitution(aName, null);
}

/** Handler for "install" of the bootstrap.js */
function install(aReason)
{
	_callHandler('install', aReason);
}

/** Handler for "uninstall" of the bootstrap.js */
function uninstall(aReason)
{
	_callHandler('uninstall', aReason);
}

/** Handler for "shutdown" of the bootstrap.js */
function shutdown(aReason)
{
	_callHandler('shutdown', aReason);

	for each (let ns in _namespaces)
	{
		for (let i in ns.exports)
		{
			if (ns.exports.hasOwnProperty(i))
				delete ns.exports[i];
		}
	}
	_namespaces = void(0);
	_namespacePrototype = void(0);
	Application = void(0);

	IOService = void(0);
	FileHandler = void(0);

	load = void(0);
	_exportSymbols = void(0);
	exists = void(0);
	_createNamespace = void(0);
	_callHandler = void(0);
	registerResource = void(0);
	unregisterResource = void(0);
	install = void(0);
	uninstall = void(0);
	shutdown = void(0);
}
<|endoftext|>


<commit_before>// Any copyright is dedicated to the Public Domain.
// http://creativecommons.org/publicdomain/zero/1.0/

/* eslint no-console: 0 */

import expect from 'expect';

import fs from 'fs';
import microtime from 'microtime-fast';
import path from 'path';
import tmp from 'tmp';

import { DB } from '../../app/services/sqlite';
import { ProfileStorage } from '../../app/services/storage';

describe('open', () => {
  it('Should create the DB file.', (done) => {
    (async function() {
      const tempPath = tmp.tmpNameSync();
      console.log(`Temporary DB is ${tempPath}.`);

      const db = await DB.open(tempPath);

      expect(db instanceof DB);
      fs.accessSync(tempPath);   // Throws on failure.

      const row = await db.get('PRAGMA user_version');
      expect(row.user_version === 0);
      await db.close();
      console.log('Cleaning up.');
      fs.unlinkSync(tempPath);   // Clean up.

      done();
    }());
  });

  it('Should be accessible through the directory only, including creating dirs.', (done) => {
    (async function() {
      const parent = tmp.dirSync({}).name;
      console.log(`Temp root: ${parent}`);

      // Ensure that subdirectories will be created if necessary.
      const tempDir = path.join(parent, '/foo/bar/');
      const storage = await ProfileStorage.open(tempDir);

      expect(storage instanceof ProfileStorage);
      const v = await storage.userVersion();
      console.log(`Storage version: ${v}.`);
      await storage.close();

      console.log('Cleaning up.');

      // This should be the only file after we close.
      fs.unlinkSync(path.join(tempDir, 'browser.db'));
      fs.rmdirSync(tempDir);
      done();
    }());
  });

  it('Is created with the current version.', (done) => {
    (async function() {
      const tempDir = tmp.dirSync({}).name;
      const storage = await ProfileStorage.open(tempDir);

      // You'll need to bump this every time the current version changes.
      const v = await storage.userVersion();
      expect(v === 1);

      // Make sure the tables exist.

      const now = microtime.now();
      await storage.db.run(`INSERT INTO placeEvents (id, url, ts) VALUES (1, \'http://example.com/\', ${now})`);
      await storage.db.run(`INSERT INTO visitEvents (place, ts) VALUES (1, ${now})`);
      const pE = await storage.db.get('SELECT ts FROM placeEvents');
      const vE = await storage.db.get('SELECT place FROM visitEvents');
      expect(pE.foo === now);
      expect(vE.foo === 1);

      await storage.close();

      console.log('Cleaning up.');

      // This should be the only file after we close.
      fs.unlinkSync(path.join(tempDir, 'browser.db'));
      fs.rmdirSync(tempDir);

      done();
    }());
  });

  it('Can save places across opens.', (done) => {
    (async function () {
      const tempDir = tmp.dirSync({}).name;
      const storageA = await ProfileStorage.open(tempDir);

      const idFoo = await storageA.savePlace('http://example.com/foo');
      const idBar = await storageA.savePlace('http://example.com/bar');
      const again = await storageA.savePlace('http://example.com/foo');

      expect(idFoo === again);
      expect((idBar - idFoo) === 1);

      const fetched = await storageA.db.get('SELECT id FROM placeEvents WHERE url = ?', ['http://example.com/foo']);
      expect(fetched.id === idFoo);

      await storageA.close();

      // Reopen a new instance.
      const storageB = await ProfileStorage.open(tempDir);
      const idBarB = await storageB.savePlace('http://example.com/bar');

      // The ID is the same.
      expect(idBarB === idBar);

      const urlsA = await storageB.visited(0, 1);
      expect(urlsA.length === 1);
      const urlsB = await storageB.visited(0, 3);
      expect(urlsB.length === 2);

      await storageB.close();

      console.log('Cleaning up.');

      // This should be the only file after we close.
      fs.unlinkSync(path.join(tempDir, 'browser.db'));
      fs.rmdirSync(tempDir);

      done();
    }());
  });

  it('Saves visits.', (done) => {
    (async function () {
      const tempDir = tmp.dirSync({}).name;
      const storage = await ProfileStorage.open(tempDir);

      const idFoo = await storage.visit('http://example.com/foo');
      const idBar = await storage.visit('http://example.com/bar');
      const again = await storage.visit('http://example.com/foo');

      expect(idFoo === again);
      expect((idBar - idFoo) === 1);

      await storage.close();

      console.log('Cleaning up.');

      // This should be the only file after we close.
      fs.unlinkSync(path.join(tempDir, 'browser.db'));
      fs.rmdirSync(tempDir);

      done();
    }());
  });

  it('Can query by URL match.', (done) => {
    (async function () {
      const tempDir = tmp.dirSync({}).name;
      const storage = await ProfileStorage.open(tempDir);

      await storage.visit('http://example.com/foo/noo');
      await storage.visit('http://example.com/barbaz/noo');
      await storage.visit('http://example.com/barbar');

      const results = await storage.visitedMatches('bar');
      expect(results.length === 2);
      expect(results[0] === 'http://example.com/barbar');
      expect(results[1] === 'http://example.com/barbaz/noo');

      await storage.close();

      console.log('Cleaning up.');

      // This should be the only file after we close.
      fs.unlinkSync(path.join(tempDir, 'browser.db'));
      fs.rmdirSync(tempDir);

      done();
    }());
  });
});
<commit_msg>Use beforeEach/afterEach in storage tests. r=nalexander<commit_after>// Any copyright is dedicated to the Public Domain.
// http://creativecommons.org/publicdomain/zero/1.0/

/* eslint no-console: 0 */

import expect from 'expect';

import fs from 'fs';
import microtime from 'microtime-fast';
import path from 'path';
import tmp from 'tmp';

import { DB } from '../../app/services/sqlite';
import { ProfileStorage } from '../../app/services/storage';

describe('DB.open', () => {
  it('Should create the DB file.', (done) => {
    (async function () {
      const tempPath = tmp.tmpNameSync();
      console.log(`Temporary DB is ${tempPath}.`);

      const db = await DB.open(tempPath);

      expect(db instanceof DB);
      fs.accessSync(tempPath);   // Throws on failure.

      const row = await db.get('PRAGMA user_version');
      expect(row.user_version === 0);
      await db.close();
      console.log('Cleaning up.');
      fs.unlinkSync(tempPath);   // Clean up.

      done();
    }());
  });
});

describe('ProfileStorage.open', () => {
  it('Should be accessible through the directory only, including creating dirs.', (done) => {
    (async function () {
      const parent = tmp.dirSync({}).name;
      console.log(`Temp root: ${parent}`);

      // Ensure that subdirectories will be created if necessary.
      const tempDir = path.join(parent, '/foo/bar/');
      const storage = await ProfileStorage.open(tempDir);

      expect(storage instanceof ProfileStorage);
      const v = await storage.userVersion();
      console.log(`Storage version: ${v}.`);
      await storage.close();

      console.log('Cleaning up.');

      // This should be the only file after we close.
      fs.unlinkSync(path.join(tempDir, 'browser.db'));
      fs.rmdirSync(tempDir);
      done();
    }());
  });
});

describe('ProfileStorage data access', () => {
  let tempDir = null;

  beforeEach(() => {
    tempDir = tmp.dirSync({}).name;
  });

  afterEach(() => {
    console.log('Cleaning up.');

    // This should be the only file after we close.
    fs.unlinkSync(path.join(tempDir, 'browser.db'));
    fs.rmdirSync(tempDir);
  });

  it('Is created with the current version.', (done) => {
    (async function() {
      const storage = await ProfileStorage.open(tempDir);

      // You'll need to bump this every time the current version changes.
      const v = await storage.userVersion();
      expect(v === 1);

      // Make sure the tables exist.

      const now = microtime.now();
      await storage.db.run(`INSERT INTO placeEvents (id, url, ts) VALUES (1, \'http://example.com/\', ${now})`);
      await storage.db.run(`INSERT INTO visitEvents (place, ts) VALUES (1, ${now})`);
      const pE = await storage.db.get('SELECT ts FROM placeEvents');
      const vE = await storage.db.get('SELECT place FROM visitEvents');
      expect(pE.foo === now);
      expect(vE.foo === 1);

      await storage.close();

      done();
    }());
  });

  it('Can save places across opens.', (done) => {
    (async function () {
      const storageA = await ProfileStorage.open(tempDir);

      const idFoo = await storageA.savePlace('http://example.com/foo');
      const idBar = await storageA.savePlace('http://example.com/bar');
      const again = await storageA.savePlace('http://example.com/foo');

      expect(idFoo === again);
      expect((idBar - idFoo) === 1);

      const fetched = await storageA.db.get('SELECT id FROM placeEvents WHERE url = ?', ['http://example.com/foo']);
      expect(fetched.id === idFoo);

      await storageA.close();

      // Reopen a new instance.
      const storageB = await ProfileStorage.open(tempDir);
      const idBarB = await storageB.savePlace('http://example.com/bar');

      // The ID is the same.
      expect(idBarB === idBar);

      const urlsA = await storageB.visited(0, 1);
      expect(urlsA.length === 1);
      const urlsB = await storageB.visited(0, 3);
      expect(urlsB.length === 2);

      await storageB.close();

      done();
    }());
  });

  it('Saves visits.', (done) => {
    (async function () {
      const storage = await ProfileStorage.open(tempDir);

      const idFoo = await storage.visit('http://example.com/foo');
      const idBar = await storage.visit('http://example.com/bar');
      const again = await storage.visit('http://example.com/foo');

      expect(idFoo === again);
      expect((idBar - idFoo) === 1);

      await storage.close();

      done();
    }());
  });

  it('Can query by URL match.', (done) => {
    (async function () {
      const storage = await ProfileStorage.open(tempDir);

      await storage.visit('http://example.com/foo/noo');
      await storage.visit('http://example.com/barbaz/noo');
      await storage.visit('http://example.com/barbar');

      const results = await storage.visitedMatches('bar');
      expect(results.length === 2);
      expect(results[0] === 'http://example.com/barbar');
      expect(results[1] === 'http://example.com/barbaz/noo');

      await storage.close();

      done();
    }());
  });
});
<|endoftext|>


<commit_before>/**
 * Author: Carl Glaysher, Schahriar SaffarShargh
 * Date Created: 17/03/2012
 * Description: Module to emulate SPAMC client in a node way
 * Protocol Specs: https://svn.apache.org/repos/asf/spamassassin/trunk/spamd/PROTOCOL
 * Available Commands:
 *
 *  ping - returns boolean
 *  check - returns object
 *  symbols - returns object with matches
 *  report  - returns objects with matches and descriptions
 *  reportIfSpam - returns object with matches and descriptions
 *  process - returns object with modified message
 *  headers - returns object with modified headers only
 *  learn - TELL spamassassin message is Spam or Ham
 *  tell - TELL spamassassin message is Spam
 *  revoke - remove Spammed Message as being spam from spamassassin
 *
 */
var net = require('net');
var stream = require('stream');

var patterns = {
    processAll: /(\s|-)([0-9\.]+)\s([A-Z0-9\_]+)\s([^:]+)\:\s([^\n]+)/g,
    process: /(\s|-)([0-9\.]+)\s([A-Z0-9\_]+)\s([^:]+)\:\s([^\s]+)/,
    // A fix proposed by hassansin @ https://github.com/Flolagale/spamc/commit/cf719a3436e57ff4d799eac1e58b06ab2260fbb1
    responseHead: /SPAMD\/([0-9\.\-]+)\s([0-9]+)\s([0-9A-Z_]+)/,
    response: /Spam:\s(True|False|Yes|No)\s;\s([0-9\.]+)\s\/\s([0-9\.]+)/
}

var spamc = function (host, port, timeout) {
    var self = this;
    var protocolVersion = 1.5;
    host = (host == undefined) ? '127.0.0.1' : host;
    port = (port == undefined) ? 783 : port;
    var connTimoutSecs = (timeout == undefined) ? 10 : timeout;
    var isStream = function CHECK_IF_IS_STREAM(object) {
        return (object instanceof stream.Stream);
    }
    var MessageFactory = function MESSAGE_FACTORY(command, master_headers, processAs) {
        /*
            Handles Common Command Cases
            where theres is no special processing
            involved.
        */
        return function MESSAGE_FACTORY_PRODUCT(headers, message, callback) {
            var PassThroughStream = new stream.PassThrough;

            // If Streamable api is used emit
            if(!callback) {
                callback = function(error, results) {
                    if(error) {
                        PassThroughStream.emit('error', error);
                    }else{
                        PassThroughStream.emit('report', results)
                    }
                }
            }
            // Merge Master Headers to Headers (IF ANY)
            if(master_headers) {
                if((!!headers) || (typeof(headers) === 'object')) {
                    var key;
                    for (key in master_headers){
                        headers[key] = master_headers[key];
                    }
                }else{
                    // If headers are not set, use master headers
                    headers = master_headers;
                }
            }
            // Execute Command & Return Stream
            exec.apply(PassThroughStream, [command, message, headers, function (error, data) {
                if(error) return callback(error);
                var response = processResponse(processAs || command, data);
                // Return if Tell error occurred
                if ((response) && (response[1]) && (response[1].responseCode == 69)) {
                    if(callback) callback(new Error('TELL commands are not enabled, set the --allow-tell switch.'));
                    return;
                }
                // Callback after parsing response into an argument array
                if (callback) callback.apply(this, response);
            }]);

            return PassThroughStream;
        }
    }
    /*
     * Description: Sends a Ping to spamd and returns Pong on response
     * Param: callback {function}
     */
    this.ping = function (callback) {
        exec('PING', null, null, function (error, data) {
            /* Check Response has the word PONG */
            callback(error, (data)?(data[0].indexOf('PONG') > 0):false);
        });
    };
    /*
     * Description: returns spam score
     * Param: message {string}
     * Param: callback {function}
     * Returns: PassThrough
     */
    this.check = MessageFactory('CHECK');
    /*
     * Description: Returns Spam Score and Matches
     * Param: message {string}
     * Param: callback {function}
     * Returns: PassThrough
     */
    this.symbols = MessageFactory('SYMBOLS');
    /*
     * Description: Returns an object report
     * Param: message {string}
     * Param: callback {function}
     * Returns: PassThrough
     */
    this.report = MessageFactory('REPORT');
    /*
     * Description: Returns Object Report if is spam
     * Param: message {string}
     * Param: callback {function}
     * Returns: PassThrough
     */
    this.reportIfSpam = MessageFactory('REPORT_IFSPAM');
    /*
     * Description: Returns back a report for the message + the message
     * Param: message {string}
     * Param: callback {function}
     * Returns: PassThrough
     */
    this.process = MessageFactory('PROCESS');
    /*
     * Description: Returns headers for the message
     * Param: message {string}
     * Param: callback {function}
     * Returns: PassThrough
     */
    this.headers = MessageFactory('HEADERS');

    /*
     * Description: Tell spamd to learn message is spam/ham or forget
     * Param: message {string}
     * Param: callback {function}
     * Returns: PassThrough
     */
    this.spam = MessageFactory('TELL', {
        'Message-class':    'spam',
        'Set':              'local'
    }, 'HEADERS');
    this.ham = MessageFactory('TELL', {
        'Message-class':    'ham',
        'Set':              'local'
    }, 'HEADERS');
    this.forget = MessageFactory('TELL', {
        'Remove':   'local'
    }, 'HEADERS');
    /*
     * Description: tell spamd message is not spam
     * Param: message {string}
     * Param: callback {function}
     * Returns: PassThrough
     */
    this.revoke = MessageFactory('TELL', {
        'Message-class':    'ham',
        'Set':              'local,remote'
    }, 'HEADERS');

    /*
     * Description: Tell spamd message is spam
     * Param: message {string}
     * Param: callback {function}
     * Returns: PassThrough
     */
    this.tell = MessageFactory('TELL', {
        'Message-class':    'spam',
        'Set':              'local,remote'
    }, 'HEADERS');
    /*
     * Description: Sends a command to spamd
     * Param: cmd {string}
     * Param: message {string}
     * Param: onData {function(data)}
     */
    var exec = function (cmd, message, extraHeaders, callback) {
        var _this = this;
        var responseData = [];
        var stream = net.createConnection(port, host);
        stream.setTimeout(connTimoutSecs * 1000, function () {
            if(callback) callback(new Error('Connection to spamd Timed Out'));
        });
        stream.on('connect', function () {
            /* Create Command to Send to spamd */
            cmd = cmd + " SPAMC/" + protocolVersion + "\r\n";
            /* Process Extra Headers if Any */
            if(extraHeaders) {
                var key;
                for(key in extraHeaders) {
                    cmd = cmd + key + ": " + extraHeaders[key] + "\r\n";
                }
            }
            if(isStream(_this)) {
                if((!extraHeaders) || (!extraHeaders['Content-length'])) {
                    console.warn("A Content-length may be required in the headers object to process streams. You can pass it through the third argument of every command like so { 'Content-length': length }");
                }
                stream.write(cmd + "\r\n");
                _this.setEncoding('utf8');
                _this.pipe(stream);
            }else if ((Buffer.isBuffer(message)) || (typeof (message) === 'string')) {
                message = message.toString('utf8') + '\r\n';
                cmd = cmd + "Content-length: " + (Buffer.byteLength(message)) + "\r\n";
                cmd = cmd + "\r\n" + message;
                stream.write(cmd + "\r\n");
            }else {
                stream.write(cmd + "\r\n");
            }
        });
        stream.on('error', function (data) {
            if(callback) callback(Error('spamd returned a error: ' + data.toString()));
        });
        stream.on('data', function (data) {
            var data = data.toString();
            /* Remove Last new Line and Return and Split Lines into Array */
            data = data.split("\r\n");
            for (var i = 0; i < data.length; i++) {
                if (data[i].length > 0) {
                    responseData[responseData.length] = data[i];
                }
            }
        });
        stream.on('close', function () {
            if(callback) callback(null, responseData);
        })
    };
    /*
     * Description: Processes Response from spamd and put into a formatted object
     * Param: cmd {string}
     * Param: lines {array[string]}
     * Return: [{Error}, {Object}]
     */
    var processResponse = function (cmd, lines) {
        var returnObj = {};
        if(!lines[0]) return ["Could not match response", null];
        var result = lines[0].match(patterns.responseHead);
        if (result == null) {
            return [new Error('spamd unreconized response:' + lines[0])]
        }
        returnObj.responseCode = parseInt(result[2]);
        returnObj.responseMessage = result[3];
        if (cmd == 'TELL') {
            returnObj.didSet = false;
            returnObj.didRemove = false;
        }
        for (var i = 0; i < lines.length; i++) {
            result = lines[i].match(patterns.response);
            if (result != null) {
                returnObj.isSpam = false;
                if (result[1] == 'True' || result[1] == 'Yes') {
                    returnObj.isSpam = true;
                }
                returnObj.spamScore = parseFloat(result[2]);
                returnObj.baseSpamScore = parseFloat(result[3]);
            }
            if (result == null) {
                result = lines[i].match(/([A-Z0-9\_]+)\,/g);
                if (result != null) {
                    returnObj.matches = [];
                    for (var ii = 0; ii < result.length; ii++) {
                        returnObj.matches[ii] = result[ii].substring(0, result[ii].length - 1);
                    }
                }
            }
            if (result == null && cmd != 'PROCESS') {
                result = lines[i].match(patterns.processAll);
                if (result != null) {
                    returnObj.report = [];
                    for (var ii = 0; ii < result.length; ii++) {
                        /* Remove New Line if Found */
                        result[ii] = result[ii].replace(/\n([\s]*)/, ' ');
                        /* Match Sections */
                        var matches = result[ii].match(patterns.process);
                        returnObj.report[returnObj.report.length] = {
                            score: matches[2],
                            name: matches[3],
                            description: matches[4].replace(/^\s*([\S\s]*)\b\s*$/, '$1'),
                            type: matches[5]
                        };
                    }
                }

            }
            if (lines[i].indexOf('DidSet:') >= 0) {
                returnObj.didSet = true;
            }
            if (lines[i].indexOf('DidRemove:') >= 0) {
                returnObj.didRemove = true;
            }
        }
        if (cmd == 'PROCESS') {
            returnObj.message = '';
            for (var i = 3; i < lines.length; i++) {
                returnObj.message = returnObj.message + lines[i] + "\r\n";
            }
        }
        if (cmd == 'HEADERS') {
            returnObj.headers = [];
            for (var i = 3; i < lines.length; i++) {
                if (lines[i].indexOf('\t') < 0) {
                    returnObj.headers[returnObj.headers.length] = lines[i];
                } else {
                    returnObj.headers[returnObj.headers.length - 1] = returnObj.headers[returnObj.headers.length - 1] + lines[i];
                }
            }
        }

        return [null, returnObj];
    };
};

module.exports = spamc;
<commit_msg>[breaking] API is now only streamable<commit_after>/**
 * Author: Carl Glaysher, Schahriar SaffarShargh
 * Date Created: 17/03/2012
 * Description: Module to emulate SPAMC client in a node way
 * Protocol Specs: https://svn.apache.org/repos/asf/spamassassin/trunk/spamd/PROTOCOL
 * Available Commands:
 *
 *  ping - returns boolean
 *  check - returns object
 *  symbols - returns object with matches
 *  report  - returns objects with matches and descriptions
 *  reportIfSpam - returns object with matches and descriptions
 *  process - returns object with modified message
 *  headers - returns object with modified headers only
 *  learn - TELL spamassassin message is Spam or Ham
 *  tell - TELL spamassassin message is Spam
 *  revoke - remove Spammed Message as being spam from spamassassin
 *
 */
var net = require('net');
var stream = require('stream');

var patterns = {
    processAll: /(\s|-)([0-9\.]+)\s([A-Z0-9\_]+)\s([^:]+)\:\s([^\n]+)/g,
    process: /(\s|-)([0-9\.]+)\s([A-Z0-9\_]+)\s([^:]+)\:\s([^\s]+)/,
    // A fix proposed by hassansin @ https://github.com/Flolagale/spamc/commit/cf719a3436e57ff4d799eac1e58b06ab2260fbb1
    responseHead: /SPAMD\/([0-9\.\-]+)\s([0-9]+)\s([0-9A-Z_]+)/,
    response: /Spam:\s(True|False|Yes|No)\s;\s([0-9\.]+)\s\/\s([0-9\.]+)/
}

var spamc = function (host, port, timeout) {
    var self = this;
    var protocolVersion = 1.5;
    host = (host == undefined) ? '127.0.0.1' : host;
    port = (port == undefined) ? 783 : port;
    var connTimoutSecs = (timeout == undefined) ? 10 : timeout;
    var isStream = function CHECK_IF_IS_STREAM(object) {
        return (object instanceof stream.Stream);
    }
    var MessageFactory = function MESSAGE_FACTORY(command, master_headers, processAs) {
        /*
            Handles Common Command Cases
            where theres is no special processing
            involved.
        */
        return function MESSAGE_FACTORY_PRODUCT(headers) {
            var PassThroughStream = new stream.PassThrough;

            // Callback For Results
            var callback = function(error, results) {
                if(error) {
                    PassThroughStream.emit('error', error);
                }else{
                    PassThroughStream.emit('report', results)
                }
            }
            // Merge Master Headers to Headers (IF ANY)
            if(master_headers) {
                if((!!headers) || (typeof(headers) === 'object')) {
                    var key;
                    for (key in master_headers){
                        headers[key] = master_headers[key];
                    }
                }else{
                    // If headers are not set, use master headers
                    headers = master_headers;
                }
            }
            // Execute Command & Return Stream
            exec.apply(PassThroughStream, [command, headers, function (error, data) {
                if(error) return callback(error);
                var response = processResponse(processAs || command, data);
                // Return if Tell error occurred
                if ((response) && (response[1]) && (response[1].responseCode == 69)) {
                    if(callback) callback(new Error('TELL commands are not enabled, set the --allow-tell switch.'));
                    return;
                }
                // Callback after parsing response into an argument array
                if (callback) callback.apply(this, response);
            }]);

            return PassThroughStream;
        }
    }
    /*
     * Description: Sends a Ping to spamd and returns Pong on response
     * Param: callback {function}
     */
    this.ping = function (callback) {
        exec('PING', null, function (error, data) {
            /* Check Response has the word PONG */
            callback(error, (data)?(data[0].indexOf('PONG') > 0):false);
        });
    };
    /*
     * Description: returns spam score
     * Param: message {string}
     * Param: callback {function}
     * Returns: PassThrough
     */
    this.check = MessageFactory('CHECK');
    /*
     * Description: Returns Spam Score and Matches
     * Param: message {string}
     * Param: callback {function}
     * Returns: PassThrough
     */
    this.symbols = MessageFactory('SYMBOLS');
    /*
     * Description: Returns an object report
     * Param: message {string}
     * Param: callback {function}
     * Returns: PassThrough
     */
    this.report = MessageFactory('REPORT');
    /*
     * Description: Returns Object Report if is spam
     * Param: message {string}
     * Param: callback {function}
     * Returns: PassThrough
     */
    this.reportIfSpam = MessageFactory('REPORT_IFSPAM');
    /*
     * Description: Returns back a report for the message + the message
     * Param: message {string}
     * Param: callback {function}
     * Returns: PassThrough
     */
    this.process = MessageFactory('PROCESS');
    /*
     * Description: Returns headers for the message
     * Param: message {string}
     * Param: callback {function}
     * Returns: PassThrough
     */
    this.headers = MessageFactory('HEADERS');

    /*
     * Description: Tell spamd to learn message is spam/ham or forget
     * Param: message {string}
     * Param: callback {function}
     * Returns: PassThrough
     */
    this.spam = MessageFactory('TELL', {
        'Message-class':    'spam',
        'Set':              'local'
    }, 'HEADERS');
    this.ham = MessageFactory('TELL', {
        'Message-class':    'ham',
        'Set':              'local'
    }, 'HEADERS');
    this.forget = MessageFactory('TELL', {
        'Remove':   'local'
    }, 'HEADERS');
    /*
     * Description: tell spamd message is not spam
     * Param: message {string}
     * Param: callback {function}
     * Returns: PassThrough
     */
    this.revoke = MessageFactory('TELL', {
        'Message-class':    'ham',
        'Set':              'local,remote'
    }, 'HEADERS');

    /*
     * Description: Tell spamd message is spam
     * Param: message {string}
     * Param: callback {function}
     * Returns: PassThrough
     */
    this.tell = MessageFactory('TELL', {
        'Message-class':    'spam',
        'Set':              'local,remote'
    }, 'HEADERS');
    /*
     * Description: Sends a command to spamd
     * Param: cmd {string}
     * Param: message {string}
     * Param: onData {function(data)}
     */
    var exec = function (cmd, extraHeaders, callback) {
        var _this = this;
        var responseData = [];
        var stream = net.createConnection(port, host);
        stream.setTimeout(connTimoutSecs * 1000, function () {
            if(callback) callback(new Error('Connection to spamd Timed Out'));
        });
        stream.on('connect', function () {
            /* Create Command to Send to spamd */
            cmd = cmd + " SPAMC/" + protocolVersion + "\r\n";
            /* Process Extra Headers if Any */
            if(extraHeaders) {
                var key;
                for(key in extraHeaders) {
                    cmd = cmd + key + ": " + extraHeaders[key] + "\r\n";
                }
            }
            if(isStream(_this)) {
                if((!extraHeaders) || (!extraHeaders['Content-length'])) {
                    console.warn("A Content-length may be required in the headers object to process streams. You can pass it through the third argument of every command like so { 'Content-length': length }");
                }
                stream.write(cmd + "\r\n");
                _this.setEncoding('utf8');
                _this.pipe(stream);
            }else {
                stream.write(cmd + "\r\n");
            }
        });
        stream.on('error', function (data) {
            if(callback) callback(Error('spamd returned a error: ' + data.toString()));
        });
        stream.on('data', function (data) {
            var data = data.toString();
            /* Remove Last new Line and Return and Split Lines into Array */
            data = data.split("\r\n");
            for (var i = 0; i < data.length; i++) {
                if (data[i].length > 0) {
                    responseData[responseData.length] = data[i];
                }
            }
        });
        stream.on('close', function () {
            if(callback) callback(null, responseData);
        })
    };
    /*
     * Description: Processes Response from spamd and put into a formatted object
     * Param: cmd {string}
     * Param: lines {array[string]}
     * Return: [{Error}, {Object}]
     */
    var processResponse = function (cmd, lines) {
        var returnObj = {};
        if(!lines[0]) return ["Could not match response", null];
        var result = lines[0].match(patterns.responseHead);
        if (result == null) {
            return [new Error('spamd unreconized response:' + lines[0])]
        }
        returnObj.responseCode = parseInt(result[2]);
        returnObj.responseMessage = result[3];
        if (cmd == 'TELL') {
            returnObj.didSet = false;
            returnObj.didRemove = false;
        }
        for (var i = 0; i < lines.length; i++) {
            result = lines[i].match(patterns.response);
            if (result != null) {
                returnObj.isSpam = false;
                if (result[1] == 'True' || result[1] == 'Yes') {
                    returnObj.isSpam = true;
                }
                returnObj.spamScore = parseFloat(result[2]);
                returnObj.baseSpamScore = parseFloat(result[3]);
            }
            if (result == null) {
                result = lines[i].match(/([A-Z0-9\_]+)\,/g);
                if (result != null) {
                    returnObj.matches = [];
                    for (var ii = 0; ii < result.length; ii++) {
                        returnObj.matches[ii] = result[ii].substring(0, result[ii].length - 1);
                    }
                }
            }
            if (result == null && cmd != 'PROCESS') {
                result = lines[i].match(patterns.processAll);
                if (result != null) {
                    returnObj.report = [];
                    for (var ii = 0; ii < result.length; ii++) {
                        /* Remove New Line if Found */
                        result[ii] = result[ii].replace(/\n([\s]*)/, ' ');
                        /* Match Sections */
                        var matches = result[ii].match(patterns.process);
                        returnObj.report[returnObj.report.length] = {
                            score: matches[2],
                            name: matches[3],
                            description: matches[4].replace(/^\s*([\S\s]*)\b\s*$/, '$1'),
                            type: matches[5]
                        };
                    }
                }

            }
            if (lines[i].indexOf('DidSet:') >= 0) {
                returnObj.didSet = true;
            }
            if (lines[i].indexOf('DidRemove:') >= 0) {
                returnObj.didRemove = true;
            }
        }
        if (cmd == 'PROCESS') {
            returnObj.message = '';
            for (var i = 3; i < lines.length; i++) {
                returnObj.message = returnObj.message + lines[i] + "\r\n";
            }
        }
        if (cmd == 'HEADERS') {
            returnObj.headers = [];
            for (var i = 3; i < lines.length; i++) {
                if (lines[i].indexOf('\t') < 0) {
                    returnObj.headers[returnObj.headers.length] = lines[i];
                } else {
                    returnObj.headers[returnObj.headers.length - 1] = returnObj.headers[returnObj.headers.length - 1] + lines[i];
                }
            }
        }

        return [null, returnObj];
    };
};

module.exports = spamc;
<|endoftext|>


<commit_before>import React from "react";
import { expect } from "chai";
import { PropTypes } from "../../src/react";
import reactSchema from "react-schema";

describe("React PropTypes", function() {
  it("has a validate method", () => {
    expect(PropTypes.validate).to.be.an.instanceof(Function);
  });

  it("exposes react prop-types", () => {
    Object.keys(React.PropTypes).forEach((key) => {
      expect(PropTypes[key]).to.be.an.instanceof(Function);
    });
  });
});
<commit_msg>Fix failing test.<commit_after>import React from "react";
import { expect } from "chai";
import { PropTypes } from "../../src/react";
import reactSchema from "react-schema";

describe("React PropTypes", function() {
  it("has a validate method", () => {
    expect(reactSchema.validate).to.be.an.instanceof(Function);
  });

  it("exposes react prop-types", () => {
    Object.keys(React.PropTypes).forEach((key) => {
      expect(PropTypes[key]).to.be.an.instanceof(Function);
    });
  });
});
<|endoftext|>


<commit_before>/* global Expo */
import { arr, string } from "lively.lang";
import { pt, rect, Color, Rectangle } from "lively.graphics";
import { Label, Icon, morph, Morph, ShadowObject } from "lively.morphic";
import { connect, signal } from "lively.bindings";
import {StyleSheet} from '../style-rules.js';
import {HorizontalLayout} from '../layout.js';

export default class Window extends Morph {

  static get styleSheet() {
    let windowButtonSize = pt(13, 13);
    return new StyleSheet('System Window Style',{
      ".Window .buttonGroup": {
        draggable: false,
        fill: Color.transparent,
        position: pt(0, 0),
        layout: new HorizontalLayout({autoResize: true, spacing: 6})
      },
      ".Window.inactive": {
        fill: Color.gray.lighter().lighter(),
        borderRadius: 7,
        borderColor: Color.gray,
        borderWidth: 1,
        dropShadow: {
          rotation: 90,
          distance: 8,
          blur: 10,
          color: Color.gray.withA(0.5)
        }
      },
      ".Window.inactive .windowButton": {
        borderColor: Color.gray.darker(),
        fill: Color.gray,
        borderRadius: 14,
        extent: windowButtonSize
      },
      ".Window .defaultLabelStyle": {
        fill: Color.transparent,
        opacity: 0.5,
        fontSize: 11,
        position: pt(2,1)
      },
      ".Window.active .closeButton": {
        borderWidth: 1,
        borderRadius: 14,
        extent: windowButtonSize,
        borderColor: Color.rgb(223, 75, 75),
        fill: Color.rgb(255, 96, 82)
      },
      ".Window.active .minimizeButton": {
        borderWidth: 1,
        borderRadius: 14,
        extent: windowButtonSize,
        borderColor: Color.rgb(224, 177, 77),
        fill: Color.rgb(255, 190, 6)
      },
      ".Window.active .maximizeButton": {
        borderWidth: 1,
        borderRadius: 14,
        extent: windowButtonSize,
        borderColor: Color.rgb(91,181,91),
        fill: Color.green
      },
      ".Window .windowTitleLabel": {
        fill: Color.transparent,
        fontColor: Color.darkGray
      },
      ".Window.active": {
        fill: Color.lightGray,
        borderRadius: 7,
        borderColor: Color.gray,
        borderWidth: 1,
        dropShadow: {
          rotation: 90,
          distance: 8,
          blur: 35,
          color: Color.black.withA(0.3),
          spread: 5
        }
      }
    });
  }

  static get properties() {
    return {
      controls: {
        after: ["submorphs"],
        initialize() {
          this.controls = this.getControls();
          if (this.resizable) this.controls.push(this.resizer());
          this.submorphs = [...this.submorphs, ...this.controls];
        }
      },
      styleClasses: {defaultValue: ["active"]},
      clipMode: {defaultValue: "hidden"},
      resizable: {defaultValue: true},

      title: {
        after: ["controls"],
        derived: true,
        get() {
          return this.titleLabel().textString;
        },
        set(title) {
          let textAndAttrs = typeof title === "string" ? [title, {}] : title, maxLength = 100, length = 0, truncated = [];
          for (var i = 0; i < textAndAttrs.length; i = i + 2) {
            let string = textAndAttrs[i], attr = textAndAttrs[i + 1];
            string = string.replace(/\n/g, "");
            var delta = string.length + length - maxLength;
            if (delta > 0) string = string.slice(0, -delta);
            truncated.push(string, attr || {});
            if (length >= maxLength) break;
          }
          this.titleLabel().value = truncated;
          this.relayoutWindowControls();
        }
      },

      targetMorph: {
        after: ["controls"],
        derived: true,
        get() {
          return arr.withoutAll(this.submorphs, this.controls)[0];
        },
        set(morph) {
          let ctrls = this.controls;
          arr.withoutAll(this.submorphs, ctrls).forEach(ea => ea.remove());
          if (morph) this.addMorph(morph, ctrls[0]);
          this.whenRendered().then(() => this.relayoutWindowControls());
        }
      },

      minimizedBounds: {serialize: false},
      nonMinizedBounds: {},
      nonMaximizedBounds: {},
      minimized: {},
      maximized: {}
    };
  }

  constructor(props = {}) {
    super(props);
    this.relayoutWindowControls();
    connect(this, "extent", this, "relayoutWindowControls");
    connect(this.titleLabel(), "extent", this, "relayoutWindowControls");
    connect(this.getSubmorphNamed("window menu button"), "onMouseDown", this, "openWindowMenu");
  }

  async openWindowMenu() {
    let menuItems = [
      [
        "Change Window Title",
        async () => {
          let newTitle = await $world.prompt("Enter New Name", {input: this.title});
          if (newTitle) this.title = newTitle;
        }
      ],
      {isDivider: true},
      ...(await this.targetMorph.menuItems())
    ];
    this.targetMorph.world().openMenu(menuItems);
  }

  get isWindow() {
    return true;
  }

  targetMorphBounds() {
    return new Rectangle(0, 25, this.width, this.height - 25);
  }

  relayoutWindowControls() {
    var innerB = this.innerBounds(),
        title = this.titleLabel(),
        labelBounds = innerB.withHeight(25),
        windowMenuButton = this.getSubmorphNamed('window menu button'),
        lastButtonOrWrapper = this.getSubmorphNamed("button wrapper") || arr.last(this.buttons()),
        buttonOffset = lastButtonOrWrapper.bounds().right() + 3,
        minLabelBounds = labelBounds.withLeftCenter(pt(buttonOffset, labelBounds.height / 2));

    // resizer
    this.resizer().bottomRight = innerB.bottomRight();

    // targetMorph
    if (!this.minimized && this.targetMorph && this.targetMorph.isLayoutable) this.targetMorph.setBounds(this.targetMorphBounds());

    // title
    title.textBounds().width < labelBounds.width - 2 * buttonOffset
      ? (title.center = labelBounds.center())
      : (title.leftCenter = minLabelBounds.leftCenter());

    windowMenuButton.leftCenter = title.rightCenter;
  }

  ensureNotOverTheTop() {
    let world = this.world();
    if (!world) return;
    let bounds = this.globalBounds();
    if (bounds.top() < world.innerBounds().top())
      this.moveBy(pt(0, world.innerBounds().top() - bounds.top()))
  }

  getControls() {
    return [
      morph({
        name: "button wrapper",
        styleClasses: ["buttonGroup"],
        submorphs: this.buttons()
      }),
      this.titleLabel(),
      Icon.makeLabel("list", {
        styleClasses: ["windowTitleLabel"],
        fontSize: 15,
        nativeCursor: 'pointer',
        padding: rect(5,0,0,0),
        name: "window menu button"
      })
    ];
  }

  buttons() {
    let closeButton =
      this.getSubmorphNamed("close") ||
      morph({
        name: "close",
        styleClasses: ['windowButton', "closeButton"],
        tooltip: "close window",
        submorphs: [
          Label.icon("times", {
            styleClasses: ["defaultLabelStyle"],
            visible: false
          })
        ]
      });
    connect(closeButton, "onMouseDown", this, "close");
    connect(closeButton, "onHoverIn", closeButton.submorphs[0], "visible", {
      converter: () => true
    });
    connect(closeButton, "onHoverOut", closeButton.submorphs[0], "visible", {
      converter: () => false
    });

    let minimizeButton =
      this.getSubmorphNamed("minimize") ||
      morph({
        name: "minimize",
        styleClasses: ['windowButton', "minimizeButton"],
        tooltip: "collapse window",
        submorphs: [
          Label.icon("minus", {
            styleClasses: ["defaultLabelStyle"],
            visible: false
          })
        ]
      });
    connect(minimizeButton, "onMouseDown", this, "toggleMinimize");
    connect(minimizeButton, "onHoverIn", minimizeButton.submorphs[0], "visible", {
      converter: () => true
    });
    connect(minimizeButton, "onHoverOut", minimizeButton.submorphs[0], "visible", {
      converter: () => false
    });

    if (this.resizable) {
      var maximizeButton =
        this.getSubmorphNamed("maximize") ||
        morph({
          name: "maximize",
          styleClasses: ['windowButton', "maximizeButton"],
          tooltip: "maximize window",
          submorphs: [
            Label.icon("plus", {
              styleClasses: ["defaultLabelStyle"],
              visible: false
            })
          ]
        });
      connect(maximizeButton, "onMouseDown", this, "toggleMaximize");
      connect(maximizeButton, "onHoverIn", maximizeButton.submorphs[0], "visible", {
        converter: () => true
      });
      connect(maximizeButton, "onHoverOut", maximizeButton.submorphs[0], "visible", {
        converter: () => false
      });
    }

    return arr.compact([closeButton, minimizeButton, maximizeButton]);
  }

  titleLabel() {
    return (
      this.getSubmorphNamed("titleLabel") ||
      this.addMorph({
        padding: Rectangle.inset(0, 2, 0, 0),
        styleClasses: ["windowTitleLabel"],
        type: "label",
        name: "titleLabel",
        reactsToPointer: false,
        value: ""
      })
    );
  }

  resizer() {
    var win = this, resizer = this.getSubmorphNamed("resizer");
    if (resizer) return resizer;
    resizer = morph({
      name: "resizer",
      nativeCursor: "nwse-resize",
      extent: pt(20, 20),
      fill: Color.transparent,
      bottomRight: this.extent
    });
    connect(resizer, "onDrag", win, "resizeBy", {converter: evt => evt.state.dragDelta});
    return resizer;
  }

  toggleMinimize() {
    let {nonMinizedBounds, minimized, width} = this,
        bounds = this.bounds(),
        duration = 200,
        collapseButton = this.getSubmorphNamed("minimize"),
        easing = Expo.easeOut;

    if (minimized) {
      this.minimized = false;
      this.minimizedBounds = bounds;
      this.animate({bounds: nonMinizedBounds || bounds, duration, easing});
      collapseButton.tooltip = "collapse window";
    } else {
      this.minimized = true;
      this.nonMinizedBounds = bounds;
      var minimizedBounds = this.minimizedBounds || bounds.withExtent(pt(width, 25)),
          labelBounds = this.titleLabel().textBounds(),
          buttonOffset = this.get('button wrapper').bounds().right() + 3;
      if (labelBounds.width + 2 * buttonOffset < minimizedBounds.width)
        minimizedBounds = minimizedBounds.withWidth(labelBounds.width + buttonOffset + 3);
      this.minimizedBounds = minimizedBounds;
      collapseButton.tooltip = "uncollapse window";
      this.animate({bounds: minimizedBounds, duration, easing});
    }
    this.resizer().visible = !this.minimized;
  }

  toggleMaximize() {
    var easing = Expo.easeOut, duration = 200;
    if (this.maximized) {
      this.animate({bounds: this.nonMaximizedBounds, duration, easing});
      this.resizer().bottomRight = this.extent;
      this.maximized = false;
    } else {
      this.nonMaximizedBounds = this.bounds();
      this.animate({bounds: this.world().visibleBounds().insetBy(5), duration, easing});
      this.resizer().visible = true;
      this.maximized = true;
      this.minimized = false;
    }
  }

  close() {
    var world = this.world();
    this.deactivate();
    this.remove();

    var next = world.activeWindow() || arr.last(world.getWindows());
    next && next.activate();

    signal(this, "windowClosed", this);
    if (this.targetMorph && typeof this.targetMorph.onWindowClose === "function")
      this.targetMorph.onWindowClose();
  }

  onMouseDown(evt) {
    this.activate(evt);
  }

  onDrag(evt) {
    super.onDrag(evt);
    this.ensureNotOverTheTop();
  }

  focus() {
    var w = this.world(), t = this.targetMorph;
    if (!w || !t) return;
    if (w.focusedMorph && (w.focusedMorph === t || t.isAncestorOf(w.focusedMorph))) return;
    t.focus();
  }

  isActive() {
    var w = this.world();
    if (!w) return false;
    if (this.titleLabel().fontWeight != "bold") return false;
    return arr.last(w.getWindows()) === this;
  }

  activate(evt) {
    if (this.isActive()) {
      this.focus(evt);
      return this;
    }

    this.styleClasses = ['active'];

    if (!this.world()) this.openInWorldNearHand();
    else this.bringToFront();
    let w = this.world() || this.env.world;

    arr.without(w.getWindows(), this).forEach(ea => ea.deactivate());
    this.titleLabel().fontWeight = "bold";
    this.focus(evt);

    signal(this, "windowActivated", this);
    setTimeout(() => this.relayoutWindowControls(), 0);

    return this;
  }

  deactivate() {
    if (this.styleClasses.includes('inactive')) return;
    this.styleClasses = ["inactive"];
    this.titleLabel().fontWeight = "normal";
    this.relayoutWindowControls();
  }

  get keybindings() {
    return super.keybindings.concat([
      {
        keys: {
          mac: "Meta-Shift-L R E N",
          win: "Ctrl-Shift-L R E N",
        },
        command: "[window] change title"
      }
    ]);
  }

  get commands() {
    return super.commands.concat([
      {
        name: "[window] change title",
        exec: async (win, args = {}) =>  {
          let title = args.title ||
            (await win.world().prompt("Enter new title", {
                input: win.title,
                historyId: "lively.morphic-window-title-hist"
              }));
          if (title) win.title = title;
          return true;
        }
      }
    ])
  }
}
<commit_msg>windows: change styling<commit_after>/* global Expo */
import { arr, string } from "lively.lang";
import { pt, LinearGradient, rect, Color, Rectangle } from "lively.graphics";
import { Label, Icon, morph, Morph, ShadowObject } from "lively.morphic";
import { connect, signal } from "lively.bindings";
import {StyleSheet} from '../style-rules.js';
import {HorizontalLayout} from '../layout.js';

export default class Window extends Morph {

  static get styleSheet() {
    let windowButtonSize = pt(13, 13);
    return new StyleSheet('System Window Style',{
      ".Window .buttonGroup": {
        draggable: false,
        fill: Color.transparent,
        position: pt(0, 0),
        layout: new HorizontalLayout({autoResize: true, spacing: 6})
      },
      ".Window.inactive .windowButton .Label": {
        fontColor: Color.gray.darker(),
      },
      ".Window .defaultLabelStyle": {
        fontSize: 14,
        position: pt(2,1)
      },
      ".Window .buttonGroup .windowButton": {
        borderRadius: 14,
        extent: windowButtonSize,
        fill: Color.transparent,
        nativeCursor: 'pointer',
        extent: pt(15,13)
      },
      ".Window .windowButton .Label": {
        nativeCursor: 'pointer'
      },
      ".Window .closeButton .Label.highlight": {
        fontColor: Color.rgb(223, 75, 75),
      },
      ".Window.active .closeButton .Label.default": {
        fontColor: Color.rgb(255, 96, 82)
      },
      ".Window .minimizeButton .Label.highlight": {
        fontColor: Color.rgb(224, 177, 77),
      },
        ".Window.active .minimizeButton .Label.default": {
           fontColor: Color.rgb(255, 190, 6)
        },
      ".Window .maximizeButton .Label.highlight": {
        fontColor: Color.rgb(40,116,166),
      },
      ".Window.active .maximizeButton .Label.default": {
        fontColor: Color.rgb(52,152,219),
      },
      ".Window .windowTitleLabel": {
        fill: Color.transparent,
        fontColor: Color.darkGray
      },
      ".Window": {
        borderRadius: 7,
        borderColor: Color.gray,
        borderWidth: 1
      },
      ".Window.inactive": {
        fill: LinearGradient.create({0: Color.rgbHex('f8f9f9'), .2: Color.rgbHex('e5e8e8')}),
        dropShadow: {
          rotation: 90,
          distance: 8,
          blur: 10,
          color: Color.gray.withA(0.5)
        }
      },
      ".Window.inactive.minimized": {
        fill: LinearGradient.create({0: Color.rgbHex('f8f9f9'), 1: Color.rgbHex('e5e8e8')})
      },
      ".Window.active": {
        fill: LinearGradient.create({0: Color.rgbHex('e5e8e8'), .2: Color.rgbHex('b2babb')}),
        dropShadow: {
          rotation: 90,
          distance: 8,
          blur: 35,
          color: Color.black.withA(0.3),
          spread: 5
        }
      },
      ".Window.active.minimized": {
        fill: LinearGradient.create({0: Color.rgbHex('e5e8e8'), 1: Color.rgbHex('b2babb')})
      }
    });
  }

  static get properties() {
    return {
      controls: {
        after: ["submorphs"],
        initialize() {
          this.controls = this.getControls();
          if (this.resizable) this.controls.push(this.resizer());
          this.submorphs = [...this.submorphs, ...this.controls];
        }
      },
      styleClasses: {defaultValue: ["active"]},
      clipMode: {defaultValue: "hidden"},
      resizable: {defaultValue: true},

      title: {
        after: ["controls"],
        derived: true,
        get() {
          return this.titleLabel().textString;
        },
        set(title) {
          let textAndAttrs = typeof title === "string" ? [title, {}] : title, maxLength = 100, length = 0, truncated = [];
          for (var i = 0; i < textAndAttrs.length; i = i + 2) {
            let string = textAndAttrs[i], attr = textAndAttrs[i + 1];
            string = string.replace(/\n/g, "");
            var delta = string.length + length - maxLength;
            if (delta > 0) string = string.slice(0, -delta);
            truncated.push(string, attr || {});
            if (length >= maxLength) break;
          }
          this.titleLabel().value = truncated;
          this.relayoutWindowControls();
        }
      },

      targetMorph: {
        after: ["controls"],
        derived: true,
        get() {
          return arr.withoutAll(this.submorphs, this.controls)[0];
        },
        set(morph) {
          let ctrls = this.controls;
          arr.withoutAll(this.submorphs, ctrls).forEach(ea => ea.remove());
          if (morph) this.addMorph(morph, ctrls[0]);
          this.whenRendered().then(() => this.relayoutWindowControls());
        }
      },

      minimizedBounds: {serialize: false},
      nonMinizedBounds: {},
      nonMaximizedBounds: {},
      minimized: {},
      maximized: {}
    };
  }

  constructor(props = {}) {
    super(props);
    this.relayoutWindowControls();
    connect(this, "extent", this, "relayoutWindowControls");
    connect(this.titleLabel(), "extent", this, "relayoutWindowControls");
  }

  async openWindowMenu() {
    let menuItems = [
      [
        "Change Window Title",
        async () => {
          let newTitle = await $world.prompt("Enter New Name", {input: this.title});
          if (newTitle) this.title = newTitle;
        }
      ],
      {isDivider: true},
      ...(await this.targetMorph.menuItems())
    ];
    this.targetMorph.world().openMenu(menuItems);
  }

  get isWindow() {
    return true;
  }

  targetMorphBounds() {
    return new Rectangle(0, 25, this.width, this.height - 25);
  }

  relayoutWindowControls() {
    var innerB = this.innerBounds(),
        title = this.titleLabel(),
        labelBounds = innerB.withHeight(25),
        lastButtonOrWrapper = this.getSubmorphNamed("button wrapper") || arr.last(this.buttons()),
        buttonOffset = lastButtonOrWrapper.bounds().right() + 3,
        minLabelBounds = labelBounds.withLeftCenter(pt(buttonOffset, labelBounds.height / 2));

    // resizer
    this.resizer().bottomRight = innerB.bottomRight();

    // targetMorph
    if (!this.minimized && this.targetMorph && this.targetMorph.isLayoutable) this.targetMorph.setBounds(this.targetMorphBounds());

    // title
    title.textBounds().width < labelBounds.width - 2 * buttonOffset
      ? (title.center = labelBounds.center())
      : (title.leftCenter = minLabelBounds.leftCenter());
  }

  ensureNotOverTheTop() {
    let world = this.world();
    if (!world) return;
    let bounds = this.globalBounds();
    if (bounds.top() < world.innerBounds().top())
      this.moveBy(pt(0, world.innerBounds().top() - bounds.top()))
  }

  getControls() {
    return [
      morph({
        name: "button wrapper",
        styleClasses: ["buttonGroup"],
        submorphs: this.buttons()
      }),
      this.titleLabel(),
    ];
  }

  buttons() {
    let closeButton =
      this.getSubmorphNamed("close") ||
      morph({
        name: "close",
        styleClasses: ['windowButton', "closeButton"],
        tooltip: "close window",
        submorphs: [
          Label.icon("times", {
            styleClasses: ["defaultLabelStyle",'default'],
          })
        ]
      });
    connect(closeButton, "onMouseDown", this, "close");

    let minimizeButton =
      this.getSubmorphNamed("minimize") ||
      morph({
        name: "minimize",
        styleClasses: ['windowButton', "minimizeButton"],
        tooltip: "collapse window",
        submorphs: [
          Label.icon("minus", {
            styleClasses: ["defaultLabelStyle", 'default'],
          })
        ]
      });
    connect(minimizeButton, "onMouseDown", this, "toggleMinimize");

    if (this.resizable) {
      var maximizeButton =
        this.getSubmorphNamed("maximize") ||
        morph({
          name: "maximize",
          styleClasses: ['windowButton', "maximizeButton"],
          tooltip: "Open Window Menu",
          submorphs: [
            Label.icon("toggle-down", {
              styleClasses: ["defaultLabelStyle", 'default'],
            })
          ]
        });
      connect(maximizeButton, "onMouseDown", this, "openWindowMenu");
    }

    let buttons = arr.compact([closeButton, minimizeButton, maximizeButton]);
    buttons.forEach(b => {
      connect(b, "onHoverIn", b.submorphs[0], "styleClasses", {
        converter: () => ["defaultLabelStyle", "highlight"]
      });
      connect(b, "onHoverOut", b.submorphs[0], "styleClasses", {
        converter: () => ["defaultLabelStyle", "default"]
      });
    });
    return buttons;
  }

  titleLabel() {
    return (
      this.getSubmorphNamed("titleLabel") ||
      this.addMorph({
        padding: Rectangle.inset(0, 2, 0, 0),
        styleClasses: ["windowTitleLabel"],
        type: "label",
        name: "titleLabel",
        reactsToPointer: false,
        value: ""
      })
    );
  }

  resizer() {
    var win = this, resizer = this.getSubmorphNamed("resizer");
    if (resizer) return resizer;
    resizer = morph({
      name: "resizer",
      nativeCursor: "nwse-resize",
      extent: pt(20, 20),
      fill: Color.transparent,
      bottomRight: this.extent
    });
    connect(resizer, "onDrag", win, "resizeBy", {converter: evt => evt.state.dragDelta});
    return resizer;
  }

  async toggleMinimize() {
    let {nonMinizedBounds, minimized, width} = this,
        bounds = this.bounds(),
        duration = 200,
        collapseButton = this.getSubmorphNamed("minimize"),
        easing = Expo.easeOut;

    if (minimized) {
      this.minimized = false;
      this.minimizedBounds = bounds;
      this.targetMorph.visible = true;
      this.animate({bounds: nonMinizedBounds || bounds,
                    styleClasses: ['neutral', 'active'], duration, easing});
      collapseButton.tooltip = "collapse window";
    } else {
      this.minimized = true;
      this.nonMinizedBounds = bounds;
      var minimizedBounds = this.minimizedBounds || bounds.withExtent(pt(width, 28)),
          labelBounds = this.titleLabel().textBounds(),
          buttonOffset = this.get('button wrapper').bounds().right() + 3;
      if (labelBounds.width + 2 * buttonOffset < minimizedBounds.width)
        minimizedBounds = minimizedBounds.withWidth(labelBounds.width + buttonOffset + 5);
      this.minimizedBounds = minimizedBounds;
      collapseButton.tooltip = "uncollapse window";
      await this.animate({styleClasses: ['minimized', 'active'],
                          bounds: minimizedBounds, duration, easing});
      this.targetMorph.visible = false;
    }
    this.resizer().visible = !this.minimized;
  }

  toggleMaximize() {
    var easing = Expo.easeOut, duration = 200;
    if (this.maximized) {
      this.animate({bounds: this.nonMaximizedBounds, duration, easing});
      this.resizer().bottomRight = this.extent;
      this.maximized = false;
    } else {
      this.nonMaximizedBounds = this.bounds();
      this.animate({bounds: this.world().visibleBounds().insetBy(5), duration, easing});
      this.resizer().visible = true;
      this.maximized = true;
      this.minimized = false;
    }
  }

  close() {
    var world = this.world();
    this.deactivate();
    this.remove();

    var next = world.activeWindow() || arr.last(world.getWindows());
    next && next.activate();

    signal(this, "windowClosed", this);
    if (this.targetMorph && typeof this.targetMorph.onWindowClose === "function")
      this.targetMorph.onWindowClose();
  }

  onMouseDown(evt) {
    this.activate(evt);
  }

  onDrag(evt) {
    super.onDrag(evt);
    this.ensureNotOverTheTop();
  }

  focus() {
    var w = this.world(), t = this.targetMorph;
    if (!w || !t) return;
    if (w.focusedMorph && (w.focusedMorph === t || t.isAncestorOf(w.focusedMorph))) return;
    t.focus();
  }

  isActive() {
    var w = this.world();
    if (!w) return false;
    if (this.titleLabel().fontWeight != "bold") return false;
    return arr.last(w.getWindows()) === this;
  }

  activate(evt) {
    if (this.isActive()) {
      this.focus(evt);
      return this;
    }

    this.removeStyleClass('inactive');
    this.addStyleClass('active')

    if (!this.world()) this.openInWorldNearHand();
    else this.bringToFront();
    let w = this.world() || this.env.world;

    arr.without(w.getWindows(), this).forEach(ea => ea.deactivate());
    this.titleLabel().fontWeight = "bold";
    this.focus(evt);

    signal(this, "windowActivated", this);
    setTimeout(() => this.relayoutWindowControls(), 0);

    return this;
  }

  deactivate() {
    if (this.styleClasses.includes('inactive')) return;
    this.removeStyleClass('active');
    this.addStyleClass('inactive')
    this.titleLabel().fontWeight = "normal";
    this.relayoutWindowControls();
  }

  get keybindings() {
    return super.keybindings.concat([
      {
        keys: {
          mac: "Meta-Shift-L R E N",
          win: "Ctrl-Shift-L R E N",
        },
        command: "[window] change title"
      }
    ]);
  }

  get commands() {
    return super.commands.concat([
      {
        name: "[window] change title",
        exec: async (win, args = {}) =>  {
          let title = args.title ||
            (await win.world().prompt("Enter new title", {
                input: win.title,
                historyId: "lively.morphic-window-title-hist"
              }));
          if (title) win.title = title;
          return true;
        }
      }
    ])
  }
}
<|endoftext|>