yslow-nodejs
Version:
yslow-nodejs is forked from yslow's (https://github.com/marcelduran/yslow) nodejs component and fixed its jsdom version to 3.1.2 and upgraded yslow to version 3.1.8
1,516 lines (1,336 loc) • 218 kB
JavaScript
/**
* Copyright (c) 2012, Yahoo! Inc. All rights reserved.
* Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
/*global YSLOW:true*/
/*jslint white: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true */
/**
* @module YSLOW
* @class YSLOW
* @static
*/
if (typeof YSLOW === 'undefined') {
YSLOW = {};
}
/**
* Enable/disable debbuging messages
*/
YSLOW.DEBUG = true;
/**
*
* Adds a new rule to the pool of rules.
*
* Rule objects must implement the rule interface or an error will be thrown. The interface
* of a rule object is as follows:
* <ul>
* <li><code>id</code>, e.g. "numreq"</li>
* <li><code>name</code>, e.g. "Minimize HTTP requests"</li>
* <li><code>url</code>, more info about the rule</li>
* <li><code>config</code>, configuration object with defaults</li>
* <li><code>lint()</code> a method that accepts a document, array of components and a config object and returns a reuslt object</li>
* </ul>
*
* @param {YSLOW.Rule} rule A new rule object to add
*/
YSLOW.registerRule = function (rule) {
YSLOW.controller.addRule(rule);
};
/**
*
* Adds a new ruleset (new grading algorithm).
*
* Ruleset objects must implement the ruleset interface or an error will be thrown. The interface
* of a ruleset object is as follows:
* <ul>
* <li><code>id</code>, e.g. "ydefault"</li>
* <li><code>name</code>, e.g. "Yahoo! Default"</li>
* <li><code>rules</code> a hash of ruleID => ruleconfig </li>
* <li><code>weights</code> a hash of ruleID => ruleweight </li>
* </ul>
*
* @param {YSLOW.Ruleset} ruleset The new ruleset object to be registered
*/
YSLOW.registerRuleset = function (ruleset) {
YSLOW.controller.addRuleset(ruleset);
};
/**
* Register a renderer.
*
* Renderer objects must implement the renderer interface.
* The interface is as follows:
* <ul>
* <li><code>id</code></li>
* <li><code>supports</code> a hash of view_name => 1 or 0 to indicate what views are supported</li>
* <li>and the methods</li>
* </ul>
*
* For instance if you define a JSON renderer that only render grade. Your renderer object will look like this:
* { id: 'json',
* supports: { reportcard: 1, components: 0, stats: 0, cookies: 0},
* reportcardView: function(resultset) { ... }
* }
*
* Refer to YSLOW.HTMLRenderer for the function prototype.
*
*
* @param {YSLOW.renderer} renderer The new renderer object to be registered.
*/
YSLOW.registerRenderer = function (renderer) {
YSLOW.controller.addRenderer(renderer);
};
/**
* Adds a new tool.
*
* Tool objects must implement the tool interface or an error will be thrown.
* The interface of a tool object is as follows:
* <ul>
* <li><code>id</code>, e.g. 'mytool'</li>
* <li><code>name</code>, eg. 'Custom tool #3'</li>
* <li><code>print_output</code>, whether this tool will produce output.</li>
* <li><code>run</code>, function that takes doc and componentset object, return content to be output</li>
* </ul>
*
* @param {YSLOW.Tool} tool The new tool object to be registered
*/
YSLOW.registerTool = function (tool) {
YSLOW.Tools.addCustomTool(tool);
};
/**
* Register an event listener
*
* @param {String} event_name Name of the event
* @param {Function} callback A function to be called when the event fires
* @param {Object} that Object to be assigned to the "this" value of the callback function
*/
YSLOW.addEventListener = function (event_name, callback, that) {
YSLOW.util.event.addListener(event_name, callback, that);
};
/**
* Unregister an event listener.
*
* @param {String} event_name Name of the event
* @param {Function} callback The callback function that was added as a listener
* @return {Boolean} TRUE is the listener was removed successfully, FALSE otherwise (for example in cases when the listener doesn't exist)
*/
YSLOW.removeEventListener = function (event_name, callback) {
return YSLOW.util.event.removeListener(event_name, callback);
};
/**
* @namespace YSLOW
* @constructor
* @param {String} name Error type
* @param {String} message Error description
*/
YSLOW.Error = function (name, message) {
/**
* Type of error, e.g. "Interface error"
* @type String
*/
this.name = name;
/**
* Error description
* @type String
*/
this.message = message;
};
YSLOW.Error.prototype = {
toString: function () {
return this.name + "\n" + this.message;
}
};
/**
* Copyright (c) 2012, Yahoo! Inc. All rights reserved.
* Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
YSLOW.version = '3.1.8';
/**
* Copyright (c) 2012, Yahoo! Inc. All rights reserved.
* Copyright (c) 2013, Marcel Duran and other contributors. All rights reserved.
* Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
/*global YSLOW,MutationEvent*/
/*jslint browser: true, continue: true, sloppy: true, maxerr: 50, indent: 4 */
/**
* ComponentSet holds an array of all the components and get the response info from net module for each component.
*
* @constructor
* @param {DOMElement} node DOM Element
* @param {Number} onloadTimestamp onload timestamp
*/
YSLOW.ComponentSet = function (node, onloadTimestamp) {
//
// properties
//
this.root_node = node;
this.components = [];
this.outstanding_net_request = 0;
this.component_info = [];
this.onloadTimestamp = onloadTimestamp;
this.nextID = 1;
this.notified_fetch_done = false;
};
YSLOW.ComponentSet.prototype = {
/**
* Call this function when you don't use the component set any more.
* A chance to do proper clean up, e.g. remove event listener.
*/
clear: function () {
this.components = [];
this.component_info = [];
this.cleared = true;
if (this.outstanding_net_request > 0) {
YSLOW.util.dump("YSLOW.ComponentSet.Clearing component set before all net requests finish.");
}
},
/**
* Add a new component to the set.
* @param {String} url URL of component
* @param {String} type type of component
* @param {String} base_href base href of document that the component belongs.
* @param {Object} obj DOMElement (for image type only)
* @return Component object that was added to ComponentSet
* @type ComponentObject
*/
addComponent: function (url, type, base_href, o) {
var comp, found, isDoc;
if (!url) {
if (!this.empty_url) {
this.empty_url = [];
}
this.empty_url[type] = (this.empty_url[type] || 0) + 1;
}
if (url && type) {
// check if url is valid.
if (!YSLOW.ComponentSet.isValidProtocol(url) ||
!YSLOW.ComponentSet.isValidURL(url)) {
return comp;
}
// Make sure url is absolute url.
url = YSLOW.util.makeAbsoluteUrl(url, base_href);
// For security purpose
url = YSLOW.util.escapeHtml(url);
found = typeof this.component_info[url] !== 'undefined';
isDoc = type === 'doc';
// make sure this component is not already in this component set,
// but also check if a doc is coming after a redirect using same url
if (!found || isDoc) {
this.component_info[url] = {
'state': 'NONE',
'count': found ? this.component_info[url].count : 0
};
comp = new YSLOW.Component(url, type, this, o);
if (comp) {
comp.id = this.nextID += 1;
this.components[this.components.length] = comp;
// shortcup for document component
if (!this.doc_comp && isDoc) {
this.doc_comp = comp;
}
if (this.component_info[url].state === 'NONE') {
// net.js has probably made an async request.
this.component_info[url].state = 'REQUESTED';
this.outstanding_net_request += 1;
}
} else {
this.component_info[url].state = 'ERROR';
YSLOW.util.event.fire("componentFetchError");
}
}
this.component_info[url].count += 1;
}
return comp;
},
/**
* Add a new component to the set, ignore duplicate.
* @param {String} url url of component
* @param {String} type type of component
* @param {String} base_href base href of document that the component belongs.
*/
addComponentNoDuplicate: function (url, type, base_href) {
if (url && type) {
// For security purpose
url = YSLOW.util.escapeHtml(url);
url = YSLOW.util.makeAbsoluteUrl(url, base_href);
if (this.component_info[url] === undefined) {
return this.addComponent(url, type, base_href);
}
}
},
/**
* Get components by type.
*
* @param {String|Array} type The type of component to get, e.g. "js" or
* ['js', 'css']
* @param {Boolean} include_after_onload If component loaded after onload
* should be included in the returned results, default is FALSE,
* don't include
* @param {Boolean} include_beacons If image beacons (1x1 images) should
* be included in the returned results, default is FALSE, don't
* include
* @return An array of matching components
* @type Array
*/
getComponentsByType: function (type, includeAfterOnload, includeBeacons) {
var i, j, len, lenJ, t, comp, info,
components = this.components,
compInfo = this.component_info,
comps = [],
types = {};
if (typeof includeAfterOnload === 'undefined') {
includeAfterOnload = !(YSLOW.util.Preference.getPref(
'excludeAfterOnload',
true
));
}
if (typeof includeBeacons === 'undefined') {
includeBeacons = !(YSLOW.util.Preference.getPref(
'excludeBeaconsFromLint',
true
));
}
if (typeof type === 'string') {
types[type] = 1;
} else {
for (i = 0, len = type.length; i < len; i += 1) {
t = type[i];
if (t) {
types[t] = 1;
}
}
}
for (i = 0, len = components.length; i < len; i += 1) {
comp = components[i];
if (!comp || (comp && !types[comp.type]) ||
(comp.is_beacon && !includeBeacons) ||
(comp.after_onload && !includeAfterOnload)) {
continue;
}
comps[comps.length] = comp;
info = compInfo[i];
if (!info || (info && info.count <= 1)) {
continue;
}
for (j = 1, lenJ = info.count; j < lenJ; j += 1) {
comps[comps.length] = comp;
}
}
return comps;
},
/**
* @private
* Get fetching progress.
* @return { 'total' => total number of component, 'received' => number of components fetched }
*/
getProgress: function () {
var i,
total = 0,
received = 0;
for (i in this.component_info) {
if (this.component_info.hasOwnProperty(i) &&
this.component_info[i]) {
if (this.component_info[i].state === 'RECEIVED') {
received += 1;
}
total += 1;
}
}
return {
'total': total,
'received': received
};
},
/**
* Event callback when component's GetInfoState changes.
* @param {Object} event object
*/
onComponentGetInfoStateChange: function (event_object) {
var comp, state, progress;
if (event_object) {
if (typeof event_object.comp !== 'undefined') {
comp = event_object.comp;
}
if (typeof event_object.state !== 'undefined') {
state = event_object.state;
}
}
if (typeof this.component_info[comp.url] === 'undefined') {
// this should not happen.
YSLOW.util.dump("YSLOW.ComponentSet.onComponentGetInfoStateChange(): Unexpected component: " + comp.url);
return;
}
if (this.component_info[comp.url].state === "NONE" && state === 'DONE') {
this.component_info[comp.url].state = "RECEIVED";
} else if (this.component_info[comp.url].state === "REQUESTED" && state === 'DONE') {
this.component_info[comp.url].state = "RECEIVED";
this.outstanding_net_request -= 1;
// Got all component detail info.
if (this.outstanding_net_request === 0) {
this.notified_fetch_done = true;
YSLOW.util.event.fire("componentFetchDone", {
'component_set': this
});
}
} else {
// how does this happen?
YSLOW.util.dump("Unexpected component info state: [" + comp.type + "]" + comp.url + "state: " + state + " comp_info_state: " + this.component_info[comp.url].state);
}
// fire event.
progress = this.getProgress();
YSLOW.util.event.fire("componentFetchProgress", {
'total': progress.total,
'current': progress.received,
'last_component_url': comp.url
});
},
/**
* This is called when peeler is done.
* If ComponentSet has all the component info, fire componentFetchDone event.
*/
notifyPeelDone: function () {
if (this.outstanding_net_request === 0 && !this.notified_fetch_done) {
this.notified_fetch_done = true;
YSLOW.util.event.fire("componentFetchDone", {
'component_set': this
});
}
},
/**
* After onload guess (simple version)
* Checkes for elements with src or href attributes within
* the original document html source
*/
setSimpleAfterOnload: function (callback, obj) {
var i, j, comp, doc_el, doc_comps, src,
indoc, url, el, type, len, lenJ,
docBody, doc, components, that;
if (obj) {
docBody = obj.docBody;
doc = obj.doc;
components = obj.components;
that = obj.components;
} else {
docBody = this.doc_comp && this.doc_comp.body;
doc = this.root_node;
components = this.components;
that = this;
}
// skip testing when doc not found
if (!docBody) {
YSLOW.util.dump('doc body is empty');
return callback(that);
}
doc_el = doc.createElement('div');
doc_el.innerHTML = docBody;
doc_comps = doc_el.getElementsByTagName('*');
for (i = 0, len = components.length; i < len; i += 1) {
comp = components[i];
type = comp.type;
if (type === 'cssimage' || type === 'doc') {
// docs are ignored
// css images are likely to be loaded before onload
continue;
}
indoc = false;
url = comp.url;
for (j = 0, lenJ = doc_comps.length; !indoc && j < lenJ; j += 1) {
el = doc_comps[j];
src = el.src || el.href || el.getAttribute('src') ||
el.getAttribute('href') ||
(el.nodeName === 'PARAM' && el.value);
indoc = (src === url);
}
// if component wasn't found on original html doc
// assume it was loaded after onload
comp.after_onload = !indoc;
}
callback(that);
},
/**
* After onload guess
* Checkes for inserted elements with src or href attributes after the
* page onload event triggers using an iframe with original doc html
*/
setAfterOnload: function (callback, obj) {
var ifrm, idoc, iwin, timer, done, noOnloadTimer,
that, docBody, doc, components, ret, enough, triggered,
util = YSLOW.util,
addEventListener = util.addEventListener,
removeEventListener = util.removeEventListener,
setTimer = setTimeout,
clearTimer = clearTimeout,
comps = [],
compsHT = {},
// get changed component and push to comps array
// reset timer for 1s after the last dom change
getTarget = function (e) {
var type, attr, target, src, oldSrc;
clearTimer(timer);
type = e.type;
attr = e.attrName;
target = e.target;
src = target.src || target.href || (target.getAttribute && (
target.getAttribute('src') || target.getAttribute('href')
));
oldSrc = target.dataOldSrc;
if (src &&
(type === 'DOMNodeInserted' ||
(type === 'DOMSubtreeModified' && src !== oldSrc) ||
(type === 'DOMAttrModified' &&
(attr === 'src' || attr === 'href'))) &&
!compsHT[src]) {
compsHT[src] = 1;
comps.push(target);
}
timer = setTimer(done, 1000);
},
// temp iframe onload listener
// - cancel noOnload timer since onload was fired
// - wait 3s before calling done if no dom mutation happens
// - set enough timer, limit is 10 seconds for mutations, this is
// for edge cases when page inserts/removes nodes within a loop
iframeOnload = function () {
var i, len, all, el, src;
clearTimer(noOnloadTimer);
all = idoc.getElementsByTagName('*');
for (i = 0, len = all.length; i < len; i += 1) {
el = all[i];
src = el.src || el.href;
if (src) {
el.dataOldSrc = src;
}
}
addEventListener(iwin, 'DOMSubtreeModified', getTarget);
addEventListener(iwin, 'DOMNodeInserted', getTarget);
addEventListener(iwin, 'DOMAttrModified', getTarget);
timer = setTimer(done, 3000);
enough = setTimer(done, 10000);
};
if (obj) {
that = YSLOW.ComponentSet.prototype;
docBody = obj.docBody;
doc = obj.doc;
components = obj.components;
ret = components;
} else {
that = this;
docBody = that.doc_comp && that.doc_comp.body;
doc = that.root_node;
components = that.components;
ret = that;
}
// check for mutation event support or anti-iframe option
if (typeof MutationEvent === 'undefined' || YSLOW.antiIframe) {
return that.setSimpleAfterOnload(callback, obj);
}
// skip testing when doc not found
if (!docBody) {
util.dump('doc body is empty');
return callback(ret);
}
// set afteronload properties for all components loaded after window onlod
done = function () {
var i, j, len, lenJ, comp, src, cmp;
// to avoid executing this function twice
// due to ifrm iwin double listeners
if (triggered) {
return;
}
// cancel timers
clearTimer(enough);
clearTimer(timer);
// remove listeners
removeEventListener(iwin, 'DOMSubtreeModified', getTarget);
removeEventListener(iwin, 'DOMNodeInserted', getTarget);
removeEventListener(iwin, 'DOMAttrModified', getTarget);
removeEventListener(ifrm, 'load', iframeOnload);
removeEventListener(iwin, 'load', iframeOnload);
// changed components loop
for (i = 0, len = comps.length; i < len; i += 1) {
comp = comps[i];
src = comp.src || comp.href || (comp.getAttribute &&
(comp.getAttribute('src') || comp.getAttribute('href')));
if (!src) {
continue;
}
for (j = 0, lenJ = components.length; j < lenJ; j += 1) {
cmp = components[j];
if (cmp.url === src) {
cmp.after_onload = true;
}
}
}
// remove temp iframe and invoke callback passing cset
ifrm.parentNode.removeChild(ifrm);
triggered = 1;
callback(ret);
};
// create temp iframe with doc html
ifrm = doc.createElement('iframe');
ifrm.style.cssText = 'position:absolute;top:-999em;';
doc.body.appendChild(ifrm);
iwin = ifrm.contentWindow;
// set a fallback when onload is not triggered
noOnloadTimer = setTimer(done, 3000);
// set onload and ifram content
if (iwin) {
idoc = iwin.document;
} else {
iwin = idoc = ifrm.contentDocument;
}
addEventListener(iwin, 'load', iframeOnload);
addEventListener(ifrm, 'load', iframeOnload);
idoc.open().write(docBody);
idoc.close();
addEventListener(iwin, 'load', iframeOnload);
}
};
/*
* List of valid protocols in component sets.
*/
YSLOW.ComponentSet.validProtocols = ['http', 'https', 'ftp'];
/**
* @private
* Check if url has an allowed protocol (no chrome://, about:)
* @param url
* @return false if url does not contain hostname.
*/
YSLOW.ComponentSet.isValidProtocol = function (s) {
var i, index, protocol,
validProtocols = this.validProtocols,
len = validProtocols.length;
s = s.toLowerCase();
index = s.indexOf(':');
if (index > 0) {
protocol = s.substr(0, index);
for (i = 0; i < len; i += 1) {
if (protocol === validProtocols[i]) {
return true;
}
}
}
return false;
};
/**
* @private
* Check if passed url has hostname specified.
* @param url
* @return false if url does not contain hostname.
*/
YSLOW.ComponentSet.isValidURL = function (url) {
var arr, host;
url = url.toLowerCase();
// all url is in the format of <protocol>:<the rest of the url>
arr = url.split(":");
// for http protocol, we want to make sure there is a host in the url.
if (arr[0] === "http" || arr[0] === "https") {
if (arr[1].substr(0, 2) !== "//") {
return false;
}
host = arr[1].substr(2);
if (host.length === 0 || host.indexOf("/") === 0) {
// no host specified.
return false;
}
}
return true;
};
/**
* Copyright (c) 2012, Yahoo! Inc. All rights reserved.
* Copyright (c) 2013, Marcel Duran and other contributors. All rights reserved.
* Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
/*global YSLOW*/
/*jslint white: true, onevar: true, undef: true, newcap: true, nomen: true, plusplus: true, bitwise: true, browser: true, maxerr: 50, indent: 4 */
/**
* @namespace YSLOW
* @class Component
* @constructor
*/
YSLOW.Component = function (url, type, parent_set, o) {
var obj = o && o.obj,
comp = (o && o.comp) || {};
/**
* URL of the component
* @type String
*/
this.url = url;
/**
* Component type, one of the following:
* <ul>
* <li>doc</li>
* <li>js</li>
* <li>css</li>
* <li>...</li>
* </ul>
* @type String
*/
this.type = type;
/**
* Parent component set.
*/
this.parent = parent_set;
this.headers = {};
this.raw_headers = '';
this.req_headers = null;
this.body = '';
this.compressed = false;
this.expires = undefined; // to be replaced by a Date object
this.size = 0;
this.status = 0;
this.is_beacon = false;
this.method = 'unknown';
this.cookie = '';
this.respTime = null;
this.after_onload = false;
// component object properties
// e.g. for image, image element width, image element height, actual width, actual height
this.object_prop = undefined;
// construction part
if (type === undefined) {
this.type = 'unknown';
}
this.get_info_state = 'NONE';
if (obj && type === 'image' && obj.width && obj.height) {
this.object_prop = {
'width': obj.width,
'height': obj.height
};
}
if (comp.containerNode) {
this.containerNode = comp.containerNode;
}
this.setComponentDetails(o);
};
/**
* Return the state of getting detail info from the net.
*/
YSLOW.Component.prototype.getInfoState = function () {
return this.get_info_state;
};
YSLOW.Component.prototype.populateProperties = function (resolveRedirect, ignoreImgReq) {
var comp, encoding, expires, content_length, img_src, obj, dataUri,
that = this,
NULL = null,
UNDEF = 'undefined';
// check location
// bookmarklet and har already handle redirects
if (that.headers.location && resolveRedirect && that.headers.location !== that.url) {
// Add a new component.
comp = that.parent.addComponentNoDuplicate(that.headers.location,
(that.type !== 'redirect' ? that.type : 'unknown'), that.url);
if (comp && that.after_onload) {
comp.after_onload = true;
}
that.type = 'redirect';
}
content_length = that.headers['content-length'];
// gzip, deflate
encoding = YSLOW.util.trim(that.headers['content-encoding']);
if (encoding === 'gzip' || encoding === 'deflate') {
that.compressed = encoding;
that.size = (that.body.length) ? that.body.length : NULL;
if (content_length) {
that.size_compressed = parseInt(content_length, 10) ||
content_length;
} else if (typeof that.nsize !== UNDEF) {
that.size_compressed = that.nsize;
} else {
// a hack
that.size_compressed = Math.round(that.size / 3);
}
} else {
that.compressed = false;
that.size_compressed = NULL;
if (content_length) {
that.size = parseInt(content_length, 10);
} else if (typeof that.nsize !== UNDEF) {
that.size = parseInt(that.nsize, 10);
} else {
that.size = that.body.length;
}
}
// size check/correction, @todo be more precise here
if (!that.size) {
if (typeof that.nsize !== UNDEF) {
that.size = that.nsize;
} else {
that.size = that.body.length;
}
}
that.uncompressed_size = that.body.length;
// expiration based on either Expires or Cache-Control headers
// always use max-age if exists following 1.1 spec
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
if (that.getMaxAge() !== undefined) {
that.expires = that.getMaxAge();
}
else if (that.headers.expires && that.headers.expires.length > 0) {
that.expires = new Date(that.headers.expires);
}
// compare image original dimensions with actual dimensions, data uri is
// first attempted to get the orginal dimension, if it fails (btoa) then
// another request to the orginal image is made
if (that.type === 'image' && !ignoreImgReq) {
if (typeof Image !== UNDEF) {
obj = new Image();
} else {
obj = document.createElement('img');
}
if (that.body.length) {
img_src = 'data:' + that.headers['content-type'] + ';base64,' +
YSLOW.util.base64Encode(that.body);
dataUri = 1;
} else {
img_src = that.url;
}
obj.onerror = function () {
obj.onerror = NULL;
if (dataUri) {
obj.src = that.url;
}
};
obj.onload = function () {
obj.onload = NULL;
if (obj && obj.width && obj.height) {
if (that.object_prop) {
that.object_prop.actual_width = obj.width;
that.object_prop.actual_height = obj.height;
} else {
that.object_prop = {
'width': obj.width,
'height': obj.height,
'actual_width': obj.width,
'actual_height': obj.height
};
}
if (obj.width < 2 && obj.height < 2) {
that.is_beacon = true;
}
}
};
obj.src = img_src;
}
};
/**
* Return true if this object has a last-modified date significantly in the past.
*/
YSLOW.Component.prototype.hasOldModifiedDate = function () {
var now = Number(new Date()),
modified_date = this.headers['last-modified'];
if (typeof modified_date !== 'undefined') {
// at least 1 day in the past
return ((now - Number(new Date(modified_date))) > (24 * 60 * 60 * 1000));
}
return false;
};
/**
* Return true if this object has a far future Expires.
* @todo: make the "far" interval configurable
* @param expires Date object
* @return true if this object has a far future Expires.
*/
YSLOW.Component.prototype.hasFarFutureExpiresOrMaxAge = function () {
var expires_in_seconds,
now = Number(new Date()),
minSeconds = YSLOW.util.Preference.getPref('minFutureExpiresSeconds', 2 * 24 * 60 * 60),
minMilliSeconds = minSeconds * 1000;
if (typeof this.expires === 'object') {
expires_in_seconds = Number(this.expires);
if ((expires_in_seconds - now) > minMilliSeconds) {
return true;
}
}
return false;
};
YSLOW.Component.prototype.getEtag = function () {
return this.headers.etag || '';
};
YSLOW.Component.prototype.getMaxAge = function () {
var index, maxage, expires,
cache_control = this.headers['cache-control'];
if (cache_control) {
index = cache_control.indexOf('max-age');
if (index > -1) {
maxage = parseInt(cache_control.substring(index + 8), 10);
if (maxage > 0) {
expires = YSLOW.util.maxAgeToDate(maxage);
}
}
}
return expires;
};
/**
* Return total size of Set-Cookie headers of this component.
* @return total size of Set-Cookie headers of this component.
* @type Number
*/
YSLOW.Component.prototype.getSetCookieSize = function () {
// only return total size of cookie received.
var aCookies, k,
size = 0;
if (this.headers && this.headers['set-cookie']) {
aCookies = this.headers['set-cookie'].split('\n');
if (aCookies.length > 0) {
for (k = 0; k < aCookies.length; k += 1) {
size += aCookies[k].length;
}
}
}
return size;
};
/**
* Return total size of Cookie HTTP Request headers of this component.
* @return total size of Cookie headers Request of this component.
* @type Number
*/
YSLOW.Component.prototype.getReceivedCookieSize = function () {
// only return total size of cookie sent.
var aCookies, k,
size = 0;
if (this.cookie && this.cookie.length > 0) {
aCookies = this.cookie.split('\n');
if (aCookies.length > 0) {
for (k = 0; k < aCookies.length; k += 1) {
size += aCookies[k].length;
}
}
}
return size;
};
/**
* Platform implementation of
* YSLOW.Component.prototype.setComponentDetails = function (o) {}
* goes here
/*
/**
* Copyright (c) 2012, Yahoo! Inc. All rights reserved.
* Copyright (c) 2013, Marcel Duran and other contributors. All rights reserved.
* Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
/**
* Parse details (HTTP headers, content, etc) from a
* given source and set component properties.
* @param o The object containing component details.
*/
YSLOW.Component.prototype.setComponentDetails = function (o) {
var comp = this,
parseEntry = function (entry) {
var i, header, cookie, len,
response = entry.response,
request = entry.request;
// copy from the response object
comp.status = response.status;
comp.headers = {};
comp.raw_headers = '';
for (i = 0, len = response.headers.length; i < len; i += 1) {
header = response.headers[i];
comp.headers[header.name.toLowerCase()] = header.value;
comp.raw_headers += header.name + ': ' + header.value + '\n';
}
comp.req_headers = {};
for (i = 0, len = request.headers.length; i < len; i += 1) {
header = request.headers[i];
comp.req_headers[header.name.toLowerCase()] = header.value;
}
comp.method = request.method;
if (response.content && response.content.text) {
comp.body = response.content.text;
} else {
// no body provided, getting size at least and mocking empty string content
comp.body = {
toString: function () {return '';},
length: response.content.size || 0
};
}
// for security checking
comp.response_type = comp.type;
comp.cookie = (comp.headers['set-cookie'] || '') + (comp.req_headers['cookie'] || '');
comp.nsize = parseInt(comp.headers['content-length'], 10) ||
response.bodySize || response.content.size;
comp.respTime = entry.time;
comp.after_onload = (new Date(entry.startedDateTime)
.getTime()) > comp.parent.onloadTimestamp;
// populate properties ignoring redirect
// resolution and image request
comp.populateProperties(false, true);
comp.get_info_state = 'DONE';
// notify parent ComponentSet that this component has gotten net response.
comp.parent.onComponentGetInfoStateChange({
'comp': comp,
'state': 'DONE'
});
};
parseEntry(o.entry);
};
/**
* Copyright (c) 2012, Yahoo! Inc. All rights reserved.
* Copyright (c) 2013, Marcel Duran and other contributors. All rights reserved.
* Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
/*global YSLOW*/
/*jslint white: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true */
/**
* YSlow context object that holds components set, result set and statistics of current page.
*
* @constructor
* @param {Document} doc Document object of current page.
*/
YSLOW.context = function (doc) {
this.document = doc;
this.component_set = null;
this.result_set = null;
this.onloadTimestamp = null;
// reset renderer variables
if (YSLOW.renderer) {
YSLOW.renderer.reset();
}
this.PAGE = {
totalSize: 0,
totalRequests: 0,
totalObjCount: {},
totalObjSize: {},
totalSizePrimed: 0,
totalRequestsPrimed: 0,
totalObjCountPrimed: {},
totalObjSizePrimed: {},
canvas_data: {},
statusbar: false,
overallScore: 0,
t_done: undefined,
loaded: false
};
};
YSLOW.context.prototype = {
defaultview: "ysPerfButton",
/**
* @private
* Compute statistics of current page.
* @param {Boolean} bCacheFull set to true if based on primed cache, false for empty cache.
* @return stats object
* @type Object
*/
computeStats: function (bCacheFull) {
var comps, comp, compType, i, len, size, totalSize, aTypes,
canvas_data, sType,
hCount = {},
hSize = {}, // hashes where the key is the object type
nHttpRequests = 0;
if (!this.component_set) {
/* need to run peeler first */
return;
}
comps = this.component_set.components;
if (!comps) {
return;
}
// SUMMARY - Find the number and total size for the categories.
// Iterate over all the components and add things up.
for (i = 0, len = comps.length; i < len; i += 1) {
comp = comps[i];
if (!bCacheFull || !comp.hasFarFutureExpiresOrMaxAge()) {
// If the object has a far future Expires date it won't add any HTTP requests nor size to the page.
// It adds to the HTTP requests (at least a condition GET request).
nHttpRequests += 1;
compType = comp.type;
hCount[compType] = (typeof hCount[compType] === 'undefined' ? 1 : hCount[compType] + 1);
size = 0;
if (!bCacheFull || !comp.hasOldModifiedDate()) {
// If we're doing EMPTY cache stats OR this component is newly modified (so is probably changing).
if (comp.compressed === 'gzip' || comp.compressed === 'deflate') {
if (comp.size_compressed) {
size = parseInt(comp.size_compressed, 10);
}
} else {
size = comp.size;
}
}
hSize[compType] = (typeof hSize[compType] === 'undefined' ? size : hSize[compType] + size);
}
}
totalSize = 0;
aTypes = YSLOW.peeler.types;
canvas_data = {};
for (i = 0; i < aTypes.length; i += 1) {
sType = aTypes[i];
if (typeof hCount[sType] !== "undefined") {
// canvas
if (hSize[sType] > 0) {
canvas_data[sType] = hSize[sType];
}
totalSize += hSize[sType];
}
}
return {
'total_size': totalSize,
'num_requests': nHttpRequests,
'count_obj': hCount,
'size_obj': hSize,
'canvas_data': canvas_data
};
},
/**
* Collect Statistics of the current page.
*/
collectStats: function () {
var stats = this.computeStats();
if (stats !== undefined) {
this.PAGE.totalSize = stats.total_size;
this.PAGE.totalRequests = stats.num_requests;
this.PAGE.totalObjCount = stats.count_obj;
this.PAGE.totalObjSize = stats.size_obj;
this.PAGE.canvas_data.empty = stats.canvas_data;
}
stats = this.computeStats(true);
if (stats) {
this.PAGE.totalSizePrimed = stats.total_size;
this.PAGE.totalRequestsPrimed = stats.num_requests;
this.PAGE.totalObjCountPrimed = stats.count_obj;
this.PAGE.totalObjSizePrimed = stats.size_obj;
this.PAGE.canvas_data.primed = stats.canvas_data;
}
},
/**
* Call registered renderer to generate Grade view with the passed output format.
*
* @param {String} output_format output format, e.g. 'html', 'xml'
* @return Grade in the passed output format.
*/
genPerformance: function (output_format, doc) {
if (this.result_set === null) {
if (!doc) {
doc = this.document;
}
YSLOW.controller.lint(doc, this);
}
return YSLOW.controller.render(output_format, 'reportcard', {
'result_set': this.result_set
});
},
/**
* Call registered renderer to generate Stats view with the passed output format.
*
* @param {String} output_format output format, e.g. 'html', 'xml'
* @return Stats in the passed output format.
*/
genStats: function (output_format) {
var stats = {};
if (!this.PAGE.totalSize) {
// collect stats
this.collectStats();
}
stats.PAGE = this.PAGE;
return YSLOW.controller.render(output_format, 'stats', {
'stats': stats
});
},
/**
* Call registered renderer to generate Components view with the passed output format.
*
* @param {String} output_format output format, e.g. 'html', 'xml'
* @return Components in the passed output format.
*/
genComponents: function (output_format) {
if (!this.PAGE.totalSize) {
// collect stats
this.collectStats();
}
return YSLOW.controller.render(output_format, 'components', {
'comps': this.component_set.components,
'total_size': this.PAGE.totalSize
});
},
/**
* Call registered renderer to generate Tools view with the passed output format.
*
* @param {String} output_format output format, e.g. 'html'
* @return Tools in the passed output format.
*/
genToolsView: function (output_format) {
var tools = YSLOW.Tools.getAllTools();
return YSLOW.controller.render(output_format, 'tools', {
'tools': tools
});
},
/**
* Call registered renderer to generate Ruleset Settings view with the passed output format.
*
* @param {String} output_format output format, e.g. 'html'
* @return Ruleset Settings in the passed output format.
*/
genRulesetEditView: function (output_format) {
return YSLOW.controller.render(output_format, 'rulesetEdit', {
'rulesets': YSLOW.controller.getRegisteredRuleset()
});
}
};
/**
* Copyright (c) 2012, Yahoo! Inc. All rights reserved.
* Copyright (c) 2013, Marcel Duran and other contributors. All rights reserved.
* Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
/*global YSLOW*/
/*jslint white: true, browser: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true */
/**
* @namespace YSLOW
* @class controller
* @static
*/
YSLOW.controller = {
rules: {},
rulesets: {},
onloadTimestamp: null,
renderers: {},
default_ruleset_id: 'ydefault',
run_pending: 0,
/**
* Init code. Add event listeners.
*/
init: function () {
var arr_rulesets, i, obj, value;
// listen to onload event.
YSLOW.util.event.addListener("onload", function (e) {
this.onloadTimestamp = e.time;
YSLOW.util.setTimer(function () {
YSLOW.controller.run_pending_event();
});
}, this);
// listen to onunload event.
YSLOW.util.event.addListener("onUnload", function (e) {
this.run_pending = 0;
this.onloadTimestamp = null;
}, this);
// load custom ruleset
arr_rulesets = YSLOW.util.Preference.getPrefList("customRuleset.", undefined);
if (arr_rulesets && arr_rulesets.length > 0) {
for (i = 0; i < arr_rulesets.length; i += 1) {
value = arr_rulesets[i].value;
if (typeof value === "string" && value.length > 0) {
obj = JSON.parse(value, null);
obj.custom = true;
this.addRuleset(obj);
}
}
}
this.default_ruleset_id = YSLOW.util.Preference.getPref("defaultRuleset", 'ydefault');
// load rule config preference
this.loadRulePreference();
},
/**
* Run controller to start peeler. Don't start if the page is not done loading.
* Delay the running until onload event.
*
* @param {Window} win window object
* @param {YSLOW.context} yscontext YSlow context to use.
* @param {Boolean} autorun value to indicate if triggered by autorun
*/
run: function (win, yscontext, autorun) {
var cset, line,
doc = win.document;
if (!doc || !doc.location || doc.location.href.indexOf("about:") === 0 || "undefined" === typeof doc.location.hostname) {
if (!autorun) {
line = 'Please enter a valid website address before running YSlow.';
YSLOW.ysview.openDialog(YSLOW.ysview.panel_doc, 389, 150, line, '', 'Ok');
}
return;
}
// Since firebug 1.4, onload event is not passed to YSlow if firebug
// panel is not opened. Recommendation from firebug dev team is to
// refresh the page before running yslow, which is unnecessary from
// yslow point of view. For now, just don't enforce running YSlow
// on a page has finished loading.
if (!yscontext.PAGE.loaded) {
this.run_pending = {
'win': win,
'yscontext': yscontext
};
// @todo: put up spining logo to indicate waiting for page finish loading.
return;
}
YSLOW.util.event.fire("peelStart", undefined);
cset = YSLOW.peeler.peel(doc, this.onloadTimestamp);
// need to set yscontext_component_set before firing peelComplete,
// otherwise, may run into infinite loop.
yscontext.component_set = cset;
YSLOW.util.event.fire("peelComplete", {
'component_set': cset
});
// notify ComponentSet peeling is done.
cset.notifyPeelDone();
},
/**
* Start pending run function.
*/
run_pending_event: function () {
if (this.run_pending) {
this.run(this.run_pending.win, this.run_pending.yscontext, false);
this.run_pending = 0;
}
},
/**
* Run lint function of the ruleset matches the passed rulset_id.
* If ruleset_id is undefined, use Controller's default ruleset.
* @param {Document} doc Document object of the page to run lint.
* @param {YSLOW.context} yscontext YSlow context to use.
* @param {String} ruleset_id ID of the ruleset to run.
* @return Lint result
* @type YSLOW.ResultSet
*/
lint: function (doc, yscontext, ruleset_id) {
var rule, rules, i, conf, result, weight, score,
ruleset = [],
results = [],
total_score = 0,
total_weight = 0,
that = this,
rs = that.rulesets,
defaultRuleSetId = that.default_ruleset_id;
if (ruleset_id) {
ruleset = rs[ruleset_id];
} else if (defaultRuleSetId && rs[defaultRuleSetId]) {
ruleset = rs[defaultRuleSetId];
} else {
// if no ruleset, take the first one available
for (i in rs) {
if (rs.hasOwnProperty(i) && rs[i]) {
ruleset = rs[i];
break;
}
}
}
rules = ruleset.rules;
for (i in rules) {
if (rules.hasOwnProperty(i) && rules[i] &&
this.rules.hasOwnProperty(i)) {
try {
rule = this.rules[i];
conf = YSLOW.util.merge(rule.config, rules[i]);
result = rule.lint(doc, yscontext.component_set, conf);
// apply rule weight to result.
weight = (ruleset.weights ? ruleset.weights[i] : undefined);
if (weight !== undefined) {
weight = parseInt(weight, 10);
}
if (weight === undefined || weight < 0 || weight > 100) {
if (rs.ydefault.weights[i]) {
weight = rs.ydefault.weights[i];
} else {
weight = 5;
}
}
result.weight = weight;
if (result.score !== undefined) {
if (typeof result.score !== "number") {
score = parseInt(result.score, 10);
if (!isNaN(score)) {
result.score = score;
}
}
if (typeof result.score === 'number') {
total_weight += result.weight;
if (!YSLOW.util.Preference.getPref('allowNegativeScore', false)) {
if (result.score < 0) {
result.score = 0;
}
if (typeof result.score !== 'number') {
// for backward compatibilty of n/a
re