eBay Auction Enhancer

By Joshua Kersey Last update Dec 3, 2005 — Installed 3,549 times. Daily Installs: 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 1, 0, 1, 4, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0
// eBay Auction Enhancer Greasemonkey script
// version 0.2
// 2005-12-03
// 
// Copyright 2005, Richard Gibson
// Released under the GPL license
// http://www.gnu.org/copyleft/gpl.html
// 
// Changelog:
// 0.1 (2005-09-06)
// 	original release
// 0.2 (2005-12-03)
//      Joshua Kersey updated to compensate for times greater than 24 hours. They are now properly formatted and separated by colons.
//      http://www.beyond-earth.net
// -------------------------------------------------------------------------------------------------
//
// This is a Greasemonkey user script.
//
// To install, you need Greasemonkey: http://greasemonkey.mozdev.org/
// Then restart Firefox and revisit this script.
// Under Tools, there will be a new menu item to "Install User Script".
// Accept the default configuration and install.
//
// To uninstall, go to Tools/Manage User Scripts, select this script,
// and click Uninstall.
//
// -------------------------------------------------------------------------------------------------
// 
// ==UserScript==
// @name           eBay Auction Enhancer
// @namespace      http://userscripts.org/people/336
// @description    Adds a countdown and in-place bidding to eBay auctions. Countdown is most accurate with LiveHTTPHeaders extension with request time information.
// @include        http*://*.ebay.*
// ==/UserScript==

// cannot yet do auction.co.kr or MercadoLibre/MercadoLivre

(function() {
	// constants
	const DOM_LOAD_TIME = (new Date()).valueOf();
	const MS_PER_HOUR = 3600000;
	const PLUS_MINUS = "\u00B1";
	const UPGRADE_STR = "Install LiveHTTPHeaders extension version 0.10 or higher (with request time information) to see accuracy";
	const UPGRADE_URI = "http://mozdev.org/bugs/attachment.cgi?id=3364"; //"http://livehttpheaders.mozdev.org/installation.html";
	const BAD_DATE_STR = "Unable to determine accuracy (probably from a bad or missing Date header)";
	const SPACE = "(?:\\s|&(?:(?:nb|en|em|thin)sp|#(?:160|8194|8195|8201|x0*(?:a0|200(?:2|3|9))));)+";
	const XPATH_END_TIME_NODES = "//span[contains(concat(' ',normalize-space(@class),' '),' help ')"
			+ " or contains(concat(' ',normalize-space(@class),' '),' nowrap ')]";
	var SPACE_RE = new RegExp(SPACE, "ig");
	var END_TIME_RE = new RegExp(
			"(\\d{2,}" + "(.)" + "(?:\\d?\\d|[^0-9\\s]+)" + "\\2" + "\\d{2,}"				// ymd/dmy
			+ "|(?:\d?\d|[^0-9\\s]+)" + "(.)" + "\\d{2}" + "\\3" + "\\d{2,})"				//  | mdy
			+ SPACE + "([^0-9\\s]?\\d{2}[^0-9\\s]\\d{2}[^0-9\\s]\\d{2}[^0-9\\s]?"		// hh:mm:ss
			+ "(" + SPACE + "[AaPp][Mm])?"															// am/pm
			+ "(" + SPACE + "\\S+)?)"																	// time zone
			);
	var BID_RE = /^https?:\/\/(offer(\.[^\/.]+)*\.ebay\..+\/ws\/eBayISAPI\.dll|[^\/]+\.ebay\..+\/viSubmit(Bid|Bin)?)/i;
	var l10n = [
		{ re: /^(cgi\d*|www)\.ebay\.com$/i,
			// USA, New Zealand: mmm-dd-yy hh:mm:ss PST|PDT
			date: function (strDate) {
				var match = /^([a-z]{3,})\W+(\d?\d)\W+(\d{2,})/i.exec(strDate);
				return (match ? [match[2], match[1], getFullYear(match[3])].join(" ") : strDate);
			},
			time: function (strTimeWithZone) { return strTimeWithZone.replace("PST", "GMT-8")
					.replace("PDT", "GMT-7"); }
		},
		{ re: /^(cgi\d*|www)\.ebay\.(com\.au|be|ca|fr|in|ie|it|nl|es|co\.uk)$/i,
			// Australia, Belgium, Canada, France, Ireland, Italy, Netherlands, Spain, UK:
			// 	dd-mmm-yy hh:mm:ss AEST|AEDT|CEST|CEDT|CET|EST|EDT|Paris|BST|H.Esp
			// India: dd-mmm-yyyy hh:mm:ss IST
			date: function (strDate) {
				var match = /^(\d?\d)\W+(\w{3,}\.?)\W+(\d{2,})/i.exec(strDate);
				if (!match) return strDate;
				var retVal = [match[1], match[2], getFullYear(match[3])].join(" ");
				var date = Date.parse(retVal);
				// check for Summer Time by EU rules (excluding the duplicated hour before 1 am)
				if (date >= (getLastSunday(parseInt(getFullYear(match[3]),10), 2 /* March */)
								+ MS_PER_HOUR)
						&& date < getLastSunday(parseInt(getFullYear(match[3]),10), 9 /* October */)) {
					this.zones.Paris = this.zones["H.Esp"] = "+2";
				}
				return retVal;
			},
			time: function (strTimeWithZone) {
				var t = strTimeWithZone, tz = this.zones;
				for (var z in tz) t = t.replace(z, "GMT" + tz[z]);
				return t;
			},
			zones: {UTC: "", AEST: "+10", AEDT: "+11", CEST: "+2", CEDT: "+2", CET: "+1", Paris: "+1",
					IST: "+5:30", BST: "+1", WET: "", "H.Esp": "+1"}
		},
		{ re: /^(cgi\d*|www)\.ebay\.(at|de|ch)$/i,
			// Austria, Germany, Switzerland: dd.mm.yy hh:mm:ss MEZ|MESZ
			date: function (strDate) {
				var match = /^(\d?\d)\W+(\d?\d)\W+(\d{2,})/i.exec(strDate);
				return (match ? (new Date(getFullYear(match[3]), match[2] - 1, match[1])).toDateString()
						: strDate);
			},
			time: function (strTimeWithZone) { return strTimeWithZone.replace("MESZ", "GMT+2")
					.replace("MEZ", "GMT+1"); }
		},
		{ re: /((^(cgi\d*|www)\.ebay\.(pl|se))|tw\.ebay\.com)$/i,
			// Poland, Sweden: yyyy-mm-dd hh:mm:ss CEST|CEDT
			// Taiwan: yyyy.mm.dd hh:mm:ss, TW
			date: function (strDate) {
				var match = /^(\d{2,})\W+(\d?\d)\W+(\d?\d)/i.exec(strDate);
				return (match ? (new Date(getFullYear(match[1]), match[2] - 1, match[3])).toDateString()
						: strDate);
			},
			time: function (strTimeWithZone) { return strTimeWithZone.replace("CEST", "GMT+2")
					.replace("CEDT", "GMT+2").replace("CET", "GMT+1").replace(/,\s+TW\s*$/i, " GMT+8"); }
		},
		{ re: /^(cgi\d*|www)\.ebay\.(com\.(my|sg)|ph)$/i,
			// Malaysia, Philippines, Singapore: dd-mm-yyyy hh:mm:ss AM|PM MYT|PHT|SGT
			date: function (strDate) {
				var match = /^(\d?\d)\W+(\d?\d)\W+(\d{2,})/i.exec(strDate);
				return (match ? (new Date(getFullYear(match[3]), match[2] - 1, match[1])).toDateString()
						: strDate);
			},
			time: function (strTimeWithZone) {
				return strTimeWithZone.replace(/(\d?\d)(\W+\d\d\W+\d\d)\s+(am|pm)\s+(MYT|PHT|SGT)/i,
						function (s, hh, mmss, ap, tz) {
							return (parseInt(hh,10) + (/am/i.test(ap) ? 0 : 12)) + mmss + " GMT"
									+ this.zones[tz.toUpperCase()];
						});
			},
			zones: {MYT: "+8", PHT: "+8", SGT: "+8"}
		},
		{ re: /^(cgi\d*|www)\.ebay\.com\.hk$/i,
			// Hong Kong: yyyy-mm-dd hh:mm:ss AM|PM HKT
			date: function (strDate) {
				var match = /^(\d{2,})\W+(\d?\d)\W+(\d?\d)/i.exec(strDate);
				return (match ? (new Date(getFullYear(match[1]), match[2] - 1, match[3])).toDateString()
						: strDate);
			},
			time: function (strTimeWithZone) {
				return strTimeWithZone.replace(/(\d?\d)(\W+\d\d\W+\d\d)\s+(am|pm)\s+HKT/i,
						function (s, hh, mmss, ap) {
							return (parseInt(hh,10) + (/am/i.test(ap) ? 0 : 12)) + mmss + " GMT+8";
						});
			}
		},
		{ re: /^(cgi\d*|www)\.ebay\.com\.cn$/i,
			// China: yyyy-mm-dd hh?mm?ss?
			date: function (strDate) {
				var match = /^(\d{2,})\W+(\d?\d)\W+(\d?\d)/i.exec(strDate);
				return (match ? (new Date(getFullYear(match[1]), match[2] - 1, match[3])).toDateString()
						: strDate);
			},
			time: function (strTimeWithZone) {
				var match = /(\d\d)[^0-9]+(\d\d)[^0-9]+(\d\d)/.exec(strTimeWithZone);
				return (match ? [match[1], match[2], match[3]].join(":") + " GMT+8" : strTimeWithZone);
			}
		}
	];
	
	// global variables
	var topTableCountdown = null;
	
	// utility functions
	function getFullYear (strYY) {
		if (("" + strYY).length >= 4 || ("" + strYY).length != 2) return strYY;
		var yyyy = parseInt((new Date()).getFullYear());
		return (Math.abs(yyyy - ("" + yyyy).replace(/..$/, strYY)) < 50
			? ("" + yyyy).replace(/..$/, strYY)
			: ("" + (yyyy + ((yyyy % 100 < 50) ? -100 : 100))).replace(/..$/, strYY)
		);
	};
	
	function getLastSunday (intYear, intMonth) {
		// first day of next month    Sun Mon Tue Wed Thu Fri Sat
		// getDay()                   0   1   2   3   4   5   6
		// days to subtract           7   1   2   3   4   5   6
		// ((getDay() + 6) % 7) + 1   7   1   2   3   4   5   6
		var firstDayOfNextMonth = new Date(intYear, intMonth + 1, 1);
		return firstDayOfNextMonth.valueOf()
				- MS_PER_HOUR * 24 * (((firstDayOfNextMonth.getDay() + 6) % 7) + 1);
	};
	
	function msToHMS (intMS) {
		if (isNaN(intMS)) return intMS;
		var s = Math.abs(Math.round(parseInt(intMS,10)/1000));
		var hms = ("0" + Math.floor(s/(3600*24))).replace(/^0*(\d{2})/, "$1") + ":";
		s = s % (3600 * 24)
		hms += ("0" + Math.floor(s/3600)).replace(/^0*(\d{2})/, "$1") + ":";
		s = s % 3600;
		hms += ("0" + Math.floor(s/60)).slice(-2) + ":";
		s = s % 60;
		return (intMS < 0 ? "-" : "") + hms + ("0" + s).slice(-2);
	};
	
	function createElement (strType) {
		try {
			return document.__proto__.createElement.call(document, strType);
		}
		catch (ex) {
			try {
				return document.wrappedJSObject.__proto__.createElement.call(document, strType);
			} catch (ex) {}
		}
		throw new Error("Unable to create element");
	};
	
	function isAncestorOf (elAncestor, elTest, blnMatchOnEqual) {
		try {
			if (elTest.isSameNode(elAncestor)) return (blnMatchOnEqual ? true : false);
			while (!elTest.isSameNode(elAncestor)) elTest = elTest.parentNode;
			return true;
		}
		catch (ex) {
			return false;
		}
	};
	
	function iframeOnLoad () {
		try {
			var doc = this.contentDocument, html, head, body, element;
			
			// get the basic elements, creating a basic page if necessary
			html = doc.documentElement || (doc.documentElement = (doc.getElementsByTagName("html")[0]
					|| doc.appendChild(doc.createElement("html"))));
			head = doc.getElementsByTagName("head")[0] || html.appendChild(doc.createElement("head")
					).appendChild(doc.createElement("title")).parentNode;
			body = doc.body || (doc.body = html.appendChild(doc.createElement("body")));
			
			// add the styling
			for (var sel in {margin: 0, border: 0, padding: 0}) html.style[sel] = body.style[sel] = 0;
			body.className = "ebay";
			for (var i = 0; i < document.styleSheets.length; i++) {
				element = document.styleSheets[i].ownerNode;
				if (element) head.appendChild(element.cloneNode(true));
			}
		} catch (ex) {}
	};
	
	// "real work" functions
	function initAuctionPage (objHeaders) {
		// calculate time offset
		var h = objHeaders || window.headers, timeOffset = 3000, accuracy = null,
				upgradeLiveHTTPHeaders = true;
		h = (typeof h == "object" && h);
		if (h && h.isFromCache) {
			return location.reload();
		}
		try {
			upgradeLiveHTTPHeaders = !(("responseStartTime" in h) && ("requestTime" in h));
			if (isNaN(Date.parse(h.responseHeaders.Date))) throw new Error();
			// server time is given by the Date header
			// local time is in between request time and response time (assume halfway between)
			// 	or fall back on 3 seconds before page processing
			if (h.responseStartTime > 0 && h.requestTime > 0) {	// works for all variable types
				timeOffset = Date.parse(h.responseHeaders.Date)
						- Math.round(h.responseStartTime/2 + h.requestTime/2);
				// Date header has only 1 second resolution
				accuracy = h.responseStartTime/2 - h.requestTime/2 + 1000;
			}
			else {
				timeOffset += Date.parse(h.responseHeaders.Date) - DOM_LOAD_TIME;
			}
		}
		catch (ex) {	// no headers object
			// try to get the relevant data using XMLHttpRequest, if we haven't already
			if (arguments.length == 0) {
				try {
					h = {responseHeaders: {}, requestTime: (new Date()).valueOf()};
					GM_xmlhttpRequest({
						method: "GET",
						url: location.href.toString().substring(0,
								location.href.toString().indexOf(location.pathname))
								+ "/aw-cgi/eBayISAPI.dll?TimeShow",
						onreadystatechange: function (result) {
							if (result.readyState == 3 && result.responseText)	{
								// request sent & (some) data received
								h.responseStartTime = h.responseStartTime || (new Date()).valueOf();
							}
							else if (result.readyState == 4)	{	// request complete
								try {
									h.responseHeaders.Date = /Date:\s*(.*?)$/m.exec(
											result.responseHeaders)[1];
								} catch (ex) {}
								initAuctionPage(h);
							}
						}
					});
					return;
				} catch (ex) {}	// no GM_xmlhttpRequest
			}
		}
		
		// find the auction end time element
		var endTimeNode = null, endTimeREMatch = null;
		try {
			var result = document.evaluate(XPATH_END_TIME_NODES, document, null,
					XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
			for (var i = 0; endTimeNode = result.snapshotItem(i); i++) {
				if (endTimeREMatch = END_TIME_RE.exec((endTimeNode.textContent	// thanks DOM3
						|| endTimeNode.innerHTML || endTimeNode.nodeValue).replace(SPACE_RE, " "))) {
					while (endTimeNode && endTimeNode.nodeType != Node.ELEMENT_NODE
							&& endTimeNode.nodeType != Node.DOCUMENT_NODE) {
						endTimeNode = endTimeNode.parentNode;
					}
					break;
				}
			}
		} catch (ex) {}
		
		// create the countdown and prep in-place bidding
		if (endTimeREMatch) {
			topTableCountdown = endTimeNode.parentNode.insertBefore(createElement("strong"),
					endTimeNode.parentNode.firstChild);
			topTableCountdown.style.display = "block";
			topTableCountdown.style.fontSize = "larger !important";
			startCountdown(createCountdown(topTableCountdown, accuracy, upgradeLiveHTTPHeaders),
					timeOffset, endTimeREMatch[1], endTimeREMatch[4]);
		}
		activateInPlaceBidding();
	};
	
	function createCountdown (elContainer, intAccuracyMS, blnUpgrade) {
		// empty the container and add a class for user stylesheets
		while (elContainer.firstChild) elContainer.removeChild(elContainer.lastChild);
		elContainer.className = "ePreyCountdownContainer";
		
		// create the countdown element
		elContainer.txtCountdown = elContainer.appendChild(createElement("a"));
		elContainer.txtCountdown.className = "ePreyCountdown";
		elContainer.txtCountdown = elContainer.txtCountdown.appendChild(document.createTextNode(""));
		
		// create the accuracy element
		var elAcc = elContainer.appendChild(createElement("span"));
		elAcc.className = "ePreyAccuracyContainer";
		elAcc.style.fontSize = "smaller";
		elAcc.appendChild(document.createTextNode(" " + PLUS_MINUS + " "));
		elAcc = elAcc.appendChild(createElement("a"));
		elAcc.className = "ePreyAccuracy";
		elAcc.appendChild(document.createTextNode((intAccuracyMS ? msToHMS(intAccuracyMS)
				: "??:??:??")));
		if (blnUpgrade) {
			elAcc.appendChild(document.createTextNode(" *"));
			elAcc.setAttribute("title", UPGRADE_STR);
			elAcc.setAttribute("href", UPGRADE_URI);
			elAcc.setAttribute("target", "_blank");
		}
		else if (!intAccuracyMS) {
			elAcc.setAttribute("title", BAD_DATE_STR);
		}
		
		// return the container
		return elContainer;
	};
	
	function startCountdown (elContainer, intTimeOffsetMS, strEndDate, strEndTimeWithZone) {
		var endTime = strEndTimeWithZone.replace(/^[^0-9\s]?\d{2}[^0-9\s](\d{2})[^0-9\s](\d{2}).*$/,
				function(f,m,s){return Math.floor((new Date()).valueOf() / MS_PER_HOUR) * MS_PER_HOUR
				+ m * 60000 + s * 1000;});
		var localized = false;
		var temp;
		
		// try to localize the update function
		try {
			for (var i = 0; i < l10n.length; i++) {
				var local = l10n[i];
				if (local.re.test(location.hostname)) {
					temp = Date.parse(local.date(strEndDate) + " " + local.time(strEndTimeWithZone))
							- intTimeOffsetMS;
					if (localized = !isNaN(temp)) endTime = temp;
					break;
				}
			}
		} catch (ex) {}
		
		// attach the update function
		window.setInterval(function(){
			var rem = endTime - (new Date()).valueOf();
			if (!localized) rem = ((rem % MS_PER_HOUR) + MS_PER_HOUR) % MS_PER_HOUR;
			elContainer.txtCountdown.nodeValue =
					msToHMS(rem).replace((localized ? "??:" : /^-?\d+:/), "??:");
			elContainer.style.color = (rem < 0 ? "black" : "rgb("
					+ Math.min(255, Math.round(255 * Math.exp(-rem/(MS_PER_HOUR)))) + ",0,0)");
		}, 100);
	};
	
	function activateInPlaceBidding () {
		var form, elements, element, inPlace;
		
		// create "in-place" submit links
		for (var i = document.forms.length - 1; i >= 0; i--) {
			form = document.forms[i];
			elements = form.elements;
			
			// only work with bid submission forms
			if (BID_RE.test(form.action)) {
				for (var listener, j = elements.length - 1; j >= 0; j--) {
					element = elements[j];
					if (/^input:(submit|image)$/i.test(element.nodeName + ":" + element.type)) {
						inPlace = element.parentNode.insertBefore(createElement("span"),
								element.nextSibling);
						inPlace.className = "ePreyInPlace";
						inPlace.appendChild(document.createTextNode(" "));
						inPlace = inPlace.appendChild(createElement("a"));
						inPlace.appendChild(document.createTextNode("(in place)"));
						inPlace.style.fontSize = "smaller";
						inPlace.setAttribute("href", "#");
						inPlace.setAttribute("title", element.value.replace(/\s*\W*\s*$/,
								" (in place). Does not leave this page, but may break the back button."));
						listener = bidInPlaceWrapper(element);
						inPlace.addEventListener("click", listener, true);
						inPlace.addEventListener("DOMActivate", listener, true);
					}
				}
			}
		}
	};
	
	function bidInPlaceWrapper (elTarget) {
		return function(evt){return bidInPlace(evt, elTarget);};
	}
	
	function bidInPlace (evt, elTarget) {
		var form = elTarget.form, box = form, width, height, frame = createElement("iframe"),
				src = form.action, get = [], temp;
		
		frame.addEventListener("load", iframeOnLoad, true);
		
		// bubble up from subordinate elements and determine what size the frame should be
			// normal case
		if (/^t(head|foot|body)$/i.test(box.parentNode.nodeName)) box = box.parentNode;
		if (box.parentNode.nodeName.toLowerCase() == "table") box = box.parentNode;
		width = box.offsetWidth || 0;
		height = box.offsetHeight || 0;
		for (var i = box.childNodes.length - 1; i >= 0; i--) {
			width = Math.max(width,
					(box.childNodes[i].offsetLeft + box.childNodes[i].offsetWidth) || 0);
			height = Math.max(height,
					(box.childNodes[i].offsetTop + box.childNodes[i].offsetHeight) || 0);
		}
			// top table (limit height to just show form elements, with 20px reserved for a scrollbar)
		if (isAncestorOf(box, topTableCountdown)) {
			height = 0;
			for (var inputs = form.elements, i = 0; i < inputs.length; i++) {
				temp = [inputs[i].offsetParent, inputs[i].offsetTop + inputs[i].offsetHeight];
				while (isAncestorOf(box, temp[0])) {
					temp[1] += temp[0].offsetTop;
					temp[0] = temp[0].offsetParent;
				}
				height = Math.max(height, temp[1] + 20);
			}
		}
		
		// submit the form
		if (form.method == "get") {	// determine the src corresponding to the submission
			if (src.slice(-1) != "?") src += "?";
			for (var inputs = form.elements, i = 0; i < inputs.length; i++) {
				if (inputs[i].nodeName.toLowerCase() != "input"
						|| !/^(submit|reset|image)$/i.test(inputs[i].type)
						|| inputs[i].isSameNode(elTarget)) {
					get.push(encodeURIComponent(inputs[i].name) + "="
							+ encodeURIComponent(inputs[i].value));
				}
			}
			try {
				frame.contentWindow.location.replace(src + get.join("&"));
			}
			catch (ex) {
				frame.src = src + get.join("&");
			}
		}
		else {	// fake it by cloning the form
			frame.className = "postSubmit";
			frame.addEventListener("load", function(){
				if (/(^|\s)postSubmit(\s|$)/.test(this.className)) {
					this.className = this.className.replace(/(^|\s)postSubmit(\s|$)/g, "$1$2");
					var newForm = this.contentDocument.body.appendChild(form.cloneNode(true));
					
					// make sure all elements are represented
					for (var inputs = form.elements, i = 0; i < inputs.length; i++) {
						if (!isAncestorOf(form, inputs[i])) {
							newForm.appendChild(inputs[i].cloneNode(true));
						}
					}
					
					// submit
					newForm.appendChild(elTarget.cloneNode(true)).click();
				}
			}, false);
			try {
				frame.contentWindow.location.reload();
			}
			catch (ex) {
				frame.src = "about:blank";
			}
		}
		
		// style and place the frame
		temp = {position: "absolute", backgroundColor: "white",
				width: Math.max(width, 100) + "px", height: Math.max(height, 50) + "px",
				margin: 0, border: 0, padding: 0};
		for (var sel in temp) try { frame.style[sel] = temp[sel]; } catch (ex) {}
		box.parentNode.insertBefore(frame, box);
		
		// cancel the event
		evt.preventDefault();
		evt.stopPropagation();
		return false;
	};
	
	// activate based on the page location
	//if (/\/aw-cgi\/eBayISAPI\.dll\?TimeShow/i.test(location.href)) {}
	if (/^https?:\/\/[^\/]+\.ebay\..+\/(viItem\?|.*ViewItem)/i.test(location.href)) {
		window.addEventListener("load", function(){initAuctionPage();}, true);
	}
})();