blob: 61fa0c241eac29637403fa1cd0b16e2eabf1d74e [file] [log] [blame]
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Code for caching inlined resources in local storage on first
* view and fetching them therefrom on repeat view.
*
* @author matterbury@google.com (Matt Atterbury)
*/
goog.require('pagespeedutils');
// Exporting functions using quoted attributes to prevent js compiler from
// renaming them.
// See http://code.google.com/closure/compiler/docs/api-tutorial3.html#dangers
window['pagespeed'] = window['pagespeed'] || {};
var pagespeed = window['pagespeed'];
/**
* @constructor
*/
pagespeed.LocalStorageCache = function() {
/**
* Flag indicating that the cookie needs to be regenerated.
* @type {boolean}
* @private
*/
this.regenerate_cookie_ = true;
};
/**
* @return {boolean} True if the given object has expired.
* @param {*} obj is an object of the form '<expiry> <hash> <inline-data>'.
*/
pagespeed.LocalStorageCache.prototype.hasExpired = function(obj) {
var expiry = parseInt(obj.substring(0, obj.indexOf(' ')), 10);
return (!isNaN(expiry) && expiry <= pagespeedutils.now());
};
pagespeed.LocalStorageCache.prototype['hasExpired'] =
pagespeed.LocalStorageCache.prototype.hasExpired;
/**
* @return {string} The data part of the given object.
* @param {*} obj is an object of the form '<expiry> <hash> <inline-data>'.
*/
pagespeed.LocalStorageCache.prototype.getData = function(obj) {
var pos1 = obj.indexOf(' ');
var pos2 = obj.indexOf(' ', pos1 + 1);
return obj.substring(pos2 + 1);
};
pagespeed.LocalStorageCache.prototype['getData'] =
pagespeed.LocalStorageCache.prototype.getData;
/**
* Replaces the last script element in the DOM with the given element. The
* script element was added by our C++ code, replacing the element that was
* there, either an img or a style element. The script itself kicked off the
* call to this method to replace itself with the original img/style element
* using the inlined data from local storage.
* @param {Element} newElement The element to replace with.
*/
pagespeed.LocalStorageCache.prototype.replaceLastScript = function(newElement) {
var scripts = document.getElementsByTagName('script');
var lastElement = scripts[scripts.length - 1];
lastElement.parentNode.replaceChild(newElement, lastElement);
};
pagespeed.LocalStorageCache.prototype['replaceLastScript'] =
pagespeed.LocalStorageCache.prototype.replaceLastScript;
/**
* Inline the CSS with the given URL.
* @param {string} url is the URL of the CSS to inline.
*/
pagespeed.LocalStorageCache.prototype.inlineCss = function(url) {
var obj = window.localStorage.getItem('pagespeed_lsc_url:' + url);
var newNode = document.createElement(obj ? 'style' : 'link');
if (obj && !this.hasExpired(obj)) {
newNode.type = 'text/css';
newNode.appendChild(document.createTextNode(this.getData(obj)));
} else {
newNode.rel = 'stylesheet';
newNode.href = url;
this.regenerate_cookie_ = true;
}
this.replaceLastScript(newNode);
};
pagespeed.LocalStorageCache.prototype['inlineCss'] =
pagespeed.LocalStorageCache.prototype.inlineCss;
/**
* Inline the IMG with the given URL.
* @param {string} url is the URL of the image to inline.
* @param {string} hash is the hash of the image to inline.
*/
pagespeed.LocalStorageCache.prototype.inlineImg = function(url, hash) {
var obj = window.localStorage.getItem('pagespeed_lsc_url:' + url + ' ' +
'pagespeed_lsc_hash:' + hash);
var newNode = document.createElement('img');
if (obj && !this.hasExpired(obj)) {
newNode.src = this.getData(obj);
} else {
newNode.src = url;
this.regenerate_cookie_ = true;
}
// Copy over any other original attributes.
for (var i = 2, n = arguments.length; i < n; ++i) {
var pos = arguments[i].indexOf('=');
newNode.setAttribute(arguments[i].substring(0, pos),
arguments[i].substring(pos + 1));
}
this.replaceLastScript(newNode);
};
pagespeed.LocalStorageCache.prototype['inlineImg'] =
pagespeed.LocalStorageCache.prototype.inlineImg;
/**
* For each element of the given tag name, check if it has
* data-pagespeed-lsc-url and data-pagespeed-lsc-hash attributes and, if so,
* save the element's hash, expiry, and data in local
* storage. regenerate_cookie_ is set to true if any elements are saved, which
* later triggers regeneration of the cookie.
* @param {string} tagName Tag Name of elements to process.
* @param {boolean} isHashInKey True iff the hash is part of the lookup key.
* @param {(function ({Element})|function ({Image}))} dataFunc Function to get
* an element's data.
* @private
*/
pagespeed.LocalStorageCache.prototype.processTags_ = function(tagName,
isHashInKey,
dataFunc) {
var elements = document.getElementsByTagName(tagName);
for (var i = 0, n = elements.length; i < n; ++i) {
var element = elements[i];
var hash = element.getAttribute('data-pagespeed-lsc-hash');
var url = element.getAttribute('data-pagespeed-lsc-url');
if (hash && url) {
var urlkey = 'pagespeed_lsc_url:' + url;
if (isHashInKey) {
urlkey += ' pagespeed_lsc_hash:' + hash;
}
var expiry = element.getAttribute('data-pagespeed-lsc-expiry');
var millis = (expiry ? (new Date(expiry)).getTime() : '');
var data = dataFunc(element);
if (!data) {
// img.src is set to a data URI on the repeat view but is missing
// thereafter, and we must not forget it once we have it.
var obj = window.localStorage.getItem(urlkey);
if (obj) {
data = this.getData(obj);
}
}
if (data) {
window.localStorage.setItem(urlkey, millis + ' ' + hash + ' ' + data);
this.regenerate_cookie_ = true;
}
}
}
};
/**
* Save any inlined data to local storage.
* @private
*/
pagespeed.LocalStorageCache.prototype.saveInlinedData_ = function() {
this.processTags_('img', true /* isHashInKey */,
function(e) { return e.src; });
this.processTags_('style', false /* isHashInKey */,
function(e) {
return (e.firstChild ? e.firstChild.nodeValue : null);
});
};
/**
* Regenerate the cookie from the hash's of unexpired objects in local storage
* and remove all expired objects.
* @private
*/
pagespeed.LocalStorageCache.prototype.generateCookie_ = function() {
if (this.regenerate_cookie_) {
var deadUns = [];
var goodUns = [];
var minExpiry = 0;
var currentTime = pagespeedutils.now();
// Process every local storage object of ours.
for (var i = 0, n = window.localStorage.length; i < n; ++i) {
var key = window.localStorage.key(i);
if (key.indexOf('pagespeed_lsc_url:')) continue; // Not one of ours.
var obj = window.localStorage.getItem(key);
var pos1 = obj.indexOf(' ');
var expiry = parseInt(obj.substring(0, pos1), 10);
if (!isNaN(expiry)) {
if (expiry <= currentTime) {
deadUns.push(key);
continue;
} else if (expiry < minExpiry || minExpiry == 0) {
minExpiry = expiry;
}
}
var pos2 = obj.indexOf(' ', pos1 + 1);
var hash = obj.substring(pos1 + 1, pos2);
goodUns.push(hash);
}
// Set the cookie.
var expires = '';
if (minExpiry) expires = '; expires=' + (new Date(minExpiry)).toUTCString();
document.cookie = '_GPSLSC=' + goodUns.join('!') + expires;
// Remove all expired objects.
for (var i = 0, n = deadUns.length; i < n; ++i) {
window.localStorage.removeItem(deadUns[i]);
}
this.regenerate_cookie_ = false;
}
};
/**
* Initializes the local storage cache module.
*/
pagespeed.localStorageCacheInit = function() {
// Do nothing if any required API is missing.
if (window.localStorage) {
var temp = new pagespeed.LocalStorageCache();
pagespeed['localStorageCache'] = temp;
pagespeedutils.addHandler(window, 'load',
function() {
temp.saveInlinedData_();
});
pagespeedutils.addHandler(window, 'load',
function() {
temp.generateCookie_();
});
}
};
pagespeed['localStorageCacheInit'] = pagespeed.localStorageCacheInit;