From 345b49df28eb8a78b1230abcc4c3122870fb749e Mon Sep 17 00:00:00 2001 From: Matěj Cepl Date: Wed, 13 Jan 2010 00:21:24 +0100 Subject: Information for offline capability --- offline-submit/jquery.malsup.com.htm | 583 +++++ offline-submit/lazarus.js | 4418 ++++++++++++++++++++++++++++++++++ 2 files changed, 5001 insertions(+) create mode 100644 offline-submit/jquery.malsup.com.htm create mode 100644 offline-submit/lazarus.js diff --git a/offline-submit/jquery.malsup.com.htm b/offline-submit/jquery.malsup.com.htm new file mode 100644 index 0000000..b0008d7 --- /dev/null +++ b/offline-submit/jquery.malsup.com.htm @@ -0,0 +1,583 @@ + + + +JavaScript Form Serialization Comparison + + + + + + + + + + + + + + + + + + + + + + + +

JavaScript Form Serialization Comparison:   jQuery • dojo • Prototype • YUI • MochiKit

+
+ This page tests two things about JavaScript form serialization: quality and speed. + +
If you believe the tests or comments on this page are inaccurate + or unfair - please let me know. It is not my intent to misrepresent any of these libraries. + Feedback can be sent to: forms at malsup dot com. +
+
»» Special thanks to Marius Feraru for adding support for native browser output and the EOL test!
+
+
+ Timer Profiler + Iterations: + +

+ + + + + + + +

+
+
+
Output (errors in red) + + + + + + + + + + +
NATIVE
jQuery
jQuery Form
dojo
YUI
MochiKit
Prototype
+
+
+ +

+ The following form contains each of the possible elements that can be submitted (with the exception of <input type="file">). + Which elements, and how they should be submitted, is specified here: + http://www.w3.org/TR/html401/interact/forms.html#h-17.13 +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Most enabled input elements and textareas are handled correctly by each of the libraries.
<textarea name="T3" rows="2" cols="15">?
Z</textarea>
<input type="hidden" name="H1" value="x" />
<input type="hidden" name="H2" />
<input type="password" name="PWD" />
<input type="text" name="T1" />
<input type="text" name="T2" value="YES" readonly="readonly" />
<input type="checkbox" name="C1" value="1" />
<input type="checkbox" name="C2" />
<input type="radio" name="R1" value="1" />
<input type="radio" name="R1" value="2" />
<input type="text" name="My Name" value="me" />
The reset input should never be serialized. Prototype and MochiKit get this wrong.
<input type="reset" name="reset" value="NO" />
Normal Single select elements are handled correctly by each of the libraries.
<select name="S1"> (100 options) + +
Normal multiple select elements are handled correctly by each of the libraries.
<select name="S2" multiple="multiple" size="3"> (100 options) +
The following select element has a selected option with no value attribute. The option text value should be submitted. + S3=YES is correct. S3= is incorrect. dojo gets this wrong in IE.
<select name="S3"><option selected="selected">YES</option></select>
The following select element has a selected option with a value attribute set to "". S4= is correct. S4=NO is incorrect. +
<select name="S4"><option value="" selected="selected">NO</option></select>
The following select element has its selectedIndex property set to -1 before serialization. + It should not be submitted. dojo and MochiKit get this wrong.
<select name="S5"><option value="NO">NO</option></select>
Submit elements should only appear in the query string when they are the element used to submit the form. Only jQuery and dojo get this right. + YUI always includes the first submit element. Prototype sends all the input submit elements (but not button submit elements). + MochiKit sends them all.
<input type="submit" name="sub1" value="NO" />
<input type="submit" name="sub2" value="NO" />
<input type="image" name="sub3" src="submit.gif" value="NO" />
<button type="submit" name="sub4" value="NO">NO</button>
Disabled elements should never be submitted. MochiKit and mootools get this wrong.
<input type="text" name="D1" value="NO" disabled="disabled" />
<input type="checkbox" name="D2" value="NO" checked="checked" disabled="disabled" />
<input type="radio" name="D3" value="NO" checked="checked" disabled="disabled" />
<select name="D4"><option selected="selected" value="NO" disabled="disabled">NO</option>
Inputs with a type attribute of something other than
+ PASSWORD | CHECKBOX | RADIO | SUBMIT | RESET | FILE | HIDDEN | IMAGE | BUTTON
+ should be treated as TEXT inputs. +
<input type="bogus" name="TYPE-TEST" value="YES" />
<input type="search" name="SEARCH" value="YES" />
+
+

+

Notes

+ This test page does not expose serialization problems that occur during an actual form submit. These include: +

+ +

+

+

Encoding

+ Each of the libraries tested use JavaScript's encodeURIComponent[2] function to encode form data. + Technically, this does not conform to the HTML spec which + states the following + about the application/x-www-form-urlencoded content type (emphasis added): +
+ + This is the default content type. Forms submitted with this content type must be encoded as follows:
+
    +
  1. Control names and values are escaped. Space characters are replaced by `+', and then reserved + characters are escaped as described in [RFC1738], + section 2.2: Non-alphanumeric characters are + replaced by `%HH', a percent sign and two hexadecimal digits representing the ASCII code of the character. + Line breaks are represented as "CR LF" pairs (i.e., `%0D%0A').
  2. +
  3. The control names/values are listed in the order they appear in the document. The name is separated + from the value by `=' and name/value pairs are separated from each other by '&'.
  4. + +
+
+ While this does not seem to cause any problems, it is interesting and noteworthy. +

+

Links

+ + +
+ + diff --git a/offline-submit/lazarus.js b/offline-submit/lazarus.js new file mode 100644 index 0000000..9db59b0 --- /dev/null +++ b/offline-submit/lazarus.js @@ -0,0 +1,4418 @@ + +//declare namespace +this.Lazarus = this.Lazarus || {} + +Lazarus.STATE_UNINITALIZED = 0; +Lazarus.STATE_DISABLED = 1; +Lazarus.STATE_PASSWORD_REQUIRED = 2; +Lazarus.STATE_ENABLED = 3; +Lazarus.STATE_DISABLED_FOR_DOMAIN = 4; +Lazarus.STATE_PRIVATE_BROWSING = 5; +Lazarus.STATE_GENERATING_KEYS = 6; + + +Lazarus.LOGIN_HOSTNAME = 'chrome://lazarus'; +Lazarus.LOGIN_REALM = 'Private Key Password'; +Lazarus.LOGIN_USERNAME = 'lazarus-private-key'; + +Lazarus.IFRAME_NAME = ''; +Lazarus.MIN_TEXT_NEEDED_TO_SHOW_NOTIFICATION = 1024; //characters + +//flag to indicate if this browser is ready yet. +Lazarus.initalized = false; + +//timers +Lazarus.cleanupSavedFormsTimer = 0; +Lazarus.autoSaveFormTimer = 0; + +//pointer to last autosave form. +Lazarus.currAutoSaveForm = null; + +//pointer to the last editor (txetbox or iframe) that a user put input into. +Lazarus.currentEditor = null; + +//flag to say if context menu is currently being shown +Lazarus.isContextMenuShowing = false; + +/* known input types +case "text": +case "textarea": +case "file": +case "radio": +case "checkbox": +case "select": +case "password": +case "hidden": +case "submit": +case "reset": +case "button": +case "image": +*/ + + + +//array of editor infos that the user has typed into this session +Lazarus.editorInfos = []; + + +/** +* window has loaded +*/ +Lazarus.init = function(){ + + Lazarus.Global = Components.utils.import("resource://lazarus/global.js").Global; + Lazarus.Crypto = Components.utils.import("resource://lazarus/crypto.js").Crypto; + + Lazarus.initalized = true; + Lazarus.initDevEnviroment(); + + //set loading icon + Lazarus.refreshIcon(); + Lazarus.repositionNotification(); + + //Update UI elements + Lazarus.$("lazarus-statusbaricon-tooltip-title").setAttribute("value", Lazarus.getString("lazarus.statusbarpanel.image.tooltip", Lazarus.getVersionStr() +" ["+ Lazarus.build +"]")); + Lazarus.refreshMenuIcons(); + Lazarus.Pref.addObserver('extensions.lazarus.showContextMenuIcons', Lazarus.refreshMenuIcons); + + if (Lazarus.getPref('extensions.lazarus.showDonateNotification')){ + Lazarus.checkForDonateThanks(); + } + + + //check crypto component (breaks a lot with various Linux builds) + if (Lazarus.checkCrypto()){ + //open the database + if (Lazarus.initDB()){ + if (Lazarus.initEncryptionKeys()){ + if (Lazarus.loadPublicKey()){ + Lazarus.enable(); + } + + Lazarus.refreshIcon(); + + Lazarus.saveAutoSaveText(); + + //remove expired forms + Lazarus.cleanupSavedForms(); + + //hmmm, interesting. Cleaning the database makes little difference to the database size (33MB -> 32MB), + // unless a lot of deletions have been made + // + if (Lazarus.getPref('extensions.lazarus.cleanDatabaseAtStartup')){ + Lazarus.cleanDB(); + } + } + //else we are generating new encryption keys, do nothing + } + else { + //errors *should* have been thrown in the error console. + //inform the user something went wrong :( + Lazarus.error("Failed to load database"); + Lazarus.disable(); + } + } + else { + Lazarus.error("Failed to load crypto component"); + Lazarus.disable(); + } + Lazarus.refreshIcon(); +} + +/** +* turn off the donate notifications if this user has donated. +*/ +Lazarus.checkForDonateThanks = function(){ + + var onload = function(uri){ + if (uri && uri.spec && uri.spec.indexOf('//lazarus.interclue.com/donate-thanks.html') > -1){ + Lazarus.setPref('extensions.lazarus.showDonateNotification', false); + Lazarus.Event.remove("location-change", onload); + } + } + Lazarus.Event.add("location-change", onload); +} + +/** +* check to make sure the crypto component has loaded correctly +*/ +Lazarus.checkCrypto = function(){ + try { + var crypto = Components.classes["@labs.mozilla.com/Weave/Crypto;1"].createInstance(Components.interfaces.IWeaveCrypto); + return true; + } + catch(e){ + return false; + } +} + +/** +* reposition the notification bar so that it's always the last element +*/ +Lazarus.repositionNotification = function(){ + //#124: Zotero compatiilty problem + var notif = document.getElementById('lazarus-notification'); + notif.parentNode.appendChild(notif); +} + + + +/** +* enables Lazarus for this browser +*/ +Lazarus.enable = function(){ + + Lazarus.saveAutoSavedForms(); + + //add preference observers + Lazarus.Pref.addObserver("extensions.lazarus.expireSavedForms", Lazarus.startCleanupTimer); + Lazarus.Pref.addObserver("extensions.lazarus.expireSavedFormsInterval", Lazarus.startCleanupTimer); + Lazarus.Pref.addObserver("extensions.lazarus.showInStatusbar", Lazarus.refreshIcon); + + //we need to capture any onsubmit event from forms within a webpage + gBrowser.addEventListener("submit", Lazarus.onFormSubmit, false); + gBrowser.addEventListener("submit", Lazarus.saveLastSubmittedForm, false); + gBrowser.addEventListener("DOMContentLoaded", Lazarus.autofillEvent, true); + gBrowser.addEventListener("DOMContentLoaded", Lazarus.initRecoverForm, true); + gBrowser.addEventListener("reset", Lazarus.onFormReset, false); + gBrowser.addEventListener("change", Lazarus.onFormChange, false); + //we also need to save forms if people are typing into them. + gBrowser.addEventListener("keyup", Lazarus.onKeyUp, false); + + //kjd: removing lazarus icon for now + //gBrowser.addEventListener("keydown", Lazarus.onKeyDown, false); + + + //clear the saved forms if user wants to when "clear private data" is hit. + Lazarus.$("Tools:Sanitize").addEventListener("command", Lazarus.fireClearPrivateDataIfNoPrompt, false); + + //add handlers to the context menu + Lazarus.$("contentAreaContextMenu").addEventListener("popupshowing", Lazarus.onContextMenuShowing, false); + Lazarus.$("contentAreaContextMenu").addEventListener("popuphidden", Lazarus.onContextMenuHide, false); + + //need events when a user changes the current document + Lazarus.Event.add("location-change", Lazarus.onLocationChange); + + Lazarus.Event.add("extension-uninstall", Lazarus.onUninstall); + Lazarus.Event.add("extension-uninstall-request", Lazarus.onUninstallRequest); + Lazarus.Event.add("application-startup", Lazarus.onStartUp); + Lazarus.Event.add("application-startup", Lazarus.removeOldForms); + Lazarus.Event.add("application-shutdown", Lazarus.fireClearPrivateDataOnShutdown); + Lazarus.Event.add("application-shutdown", Lazarus.onShutdown); + Lazarus.Event.add("clear-private-data", Lazarus.onClearPrivateData); + + Lazarus.startCleanupTimer(); + + //show the statusbar image (if user wants it) + Lazarus.refreshIcon(); +} + + +Lazarus.onKeyDown = function(evt){ + var ele = evt.target; + + //ignore if we have already attached an onblur handler to this element + if (!ele.lazarusIconAdded){ + switch(Lazarus.getElementType(ele)){ + case "text": + case "password": + case "textarea": + //are we saving this element? + var form = Lazarus.findFormFromElement(ele); + var editor = Lazarus.findEditorFromElement(ele); + if ((form && Lazarus.shouldSaveForm(form)) || (editor && Lazarus.shouldSaveEditorInfo(form))){ + //highlight element + Lazarus.addBackgroundIcon(ele); + } + else { + //ignore furthur keypresses on this element? + } + + default: + //ignore this element + } + } +} + + +Lazarus.addBackgroundIcon = function(ele){ + + if (!ele.lazarusIconAdded){ + ele.lazarusIconAdded = true; + + var doc = ele.ownerDocument; + if (!doc.lazarusIcon){ + var div = doc.createElement('div'); + div.style.width = "11px"; + div.style.height = "14px"; + div.style.position = "absolute"; + div.style.background = "url("+ Lazarus.icon +") no-repeat center center"; + div.title = Lazarus.getString('Lazarus.savingText'); + doc.body.appendChild(div); + doc.lazarusIcon = div; + } + //now position the image over the end of the textbox + var rect = ele.getBoundingClientRect(); + doc.lazarusIcon.style.top = rect.top +"px"; + doc.lazarusIcon.style.left = (rect.right - parseInt(doc.lazarusIcon.style.width)) +"px"; + doc.lazarusIcon.style.display = "block"; + + var onBlur = function(){ + //hide the icon + doc.lazarusIcon.style.display = "none"; + ele.lazarusIconAdded = false; + } + + //when adding the icon, we should hide it on blur + ele.addEventListener("blur", onBlur, false); + } +} + + +/** +* restart the browser +*/ +Lazarus.restart = function(){ + var nsIAppStartup = Components.interfaces.nsIAppStartup; + Components.classes["@mozilla.org/toolkit/app-startup;1"].getService(nsIAppStartup).quit(nsIAppStartup.eRestart | nsIAppStartup.eAttemptQuit); +} + +/** +* turns Lazarus off for this browser +*/ +Lazarus.disable = function(){ + + //add preference observers + Lazarus.Pref.addObserver("extensions.lazarus.expireSavedForms", Lazarus.startCleanupTimer); + Lazarus.Pref.addObserver("extensions.lazarus.expireSavedFormsInterval", Lazarus.startCleanupTimer); + Lazarus.Pref.addObserver("extensions.lazarus.showInStatusbar", Lazarus.refreshIcon); + + //we need to capture any onsubmit event from forms within a webpage + gBrowser.removeEventListener("submit", Lazarus.onFormSubmit, false); + gBrowser.removeEventListener("submit", Lazarus.saveLastSubmittedForm, false); + gBrowser.removeEventListener("DOMContentLoaded", Lazarus.autofillEvent, true); + gBrowser.removeEventListener("DOMContentLoaded", Lazarus.initRecoverForm, true); + gBrowser.removeEventListener("reset", Lazarus.onFormReset, false); + gBrowser.removeEventListener("change", Lazarus.onFormChange, false); + //we also need to save forms if people are typing into them. + gBrowser.removeEventListener("keyup", Lazarus.onKeyUp, false); + + //need events when a user changes the current document + Lazarus.Event.remove("location-change", Lazarus.onLocationChange); + + //clear the saved forms if user wants to when "clear private data" is hit. + Lazarus.$("Tools:Sanitize").removeEventListener("command", Lazarus.fireClearPrivateDataIfNoPrompt, false); + + Lazarus.stopCleanupTimer(); + + //and close the database + Lazarus.db.close(); + + //update the statusbar image + Lazarus.refreshIcon(); +} + +/** +* return TRUE if we can encrypt a string (ie the Crypto component is working, and the public key exists) +*/ +Lazarus.canEncrypt = function(){ + return Lazarus.Crypto.publicKey ? true : false; +} + +Lazarus.canDecrypt = function(){ + return Lazarus.Crypto.privateKey ? true : false; +} + + + +/** +* initalizes the lazarus recover-form page +*/ +Lazarus.initRecoverForm = function(evt){ + var doc = evt.originalTarget; + if (Lazarus.isDocRecoveryForm(doc)){ + doc.title = Lazarus.getString("recoverform.title"); + + //Lazarus.$('heading', doc).innerHTML = Lazarus.getString("recoverform.title"); + Lazarus.$('description', doc).innerHTML = Lazarus.getString("recoverform.description"); + Lazarus.$('notes', doc).innerHTML= Lazarus.getString("recoverform.notes"); + + Lazarus.$('form-url-label', doc).innerHTML= Lazarus.getString("recoverform.form.url"); + Lazarus.$('form-action-label', doc).innerHTML= Lazarus.getString("recoverform.form.action"); + Lazarus.$('notes', doc).innerHTML= Lazarus.getString("recoverform.notes"); + + var m = doc.URL.match(/[\?&]id=(\d+)/) + var id = m ? parseInt(m[1]) : -1; + if (id > -1){ + var row = Lazarus.db.getRow("SELECT * FROM forms WHERE id = ?1", id); + if (row){ + if (Lazarus.canDecrypt()){ + var formInfo = Lazarus.JSON.decode(Lazarus.decrypt(row["forminfo"])); + + Lazarus.$('form-url', doc).innerHTML = formInfo.origURL ? Lazarus.generateLinkFromURL(formInfo.origURL) : ''; + Lazarus.$('form-action', doc).innerHTML = formInfo.action ? Lazarus.generateLinkFromURL(formInfo.action) : ''; + + var form = Lazarus.buildForm(formInfo, doc); + form.setAttribute("lazarus-form-id", row["formid"]); + Lazarus.$('form-box', doc).appendChild(form); + Lazarus.restoreForm(form, formInfo); + } + else { + //need to explain to user why we cannot fill in the form + Lazarus.showNotificationBox("password-required"); + } + } + else { + Lazarus.$('form-box', doc).innerHTML = Lazarus.getString("error.form.not.found"); + Lazarus.$('form-box', doc).className = "warning"; + } + } + else { + Lazarus.$('form-box', doc).innerHTML = Lazarus.getString("error.form.not.found"); + Lazarus.$('form-box', doc).className = "warning"; + } + } +} + +/** +* return TRUE if the given document is the lazarus Recover Form page. +*/ +Lazarus.isDocRecoveryForm = function(doc){ + return (doc && doc.URL && doc.URL.indexOf("chrome://lazarus/content/recover-form.html") == 0); +} + +/** +* generate an HTML link given a url. +* if url is too long, then truncate url to maxChars +*/ +Lazarus.generateLinkFromURL = function(url, maxChars){ + + maxChars = maxChars || 50; + + if (/^(file|http|https):/.test(url)){ + var text = url; + if (text.length > maxChars){ + text = text.substring(0, maxChars -3) +"..."; + } + + return ''+ Lazarus.htmlEncode(text) +''; + } + else { + return Lazarus.htmlEncode(url); + } +} + +/** +* encode a string for display in an html page +*/ +Lazarus.htmlEncode = function(str){ + str = str.replace(/&/g, "&"); + str = str.replace(//g, ">"); + str = str.replace(/"/g, """); + return str; +} + +/** +* builds an HTML form from a formInfo object +*/ +Lazarus.buildForm = function(formInfo, doc){ + var form = doc.createElement("form"); + form.setAttribute("method", formInfo.method || "get"); + form.setAttribute("enctype", formInfo.enctype || ""); + //support for AJAX textareas + form.isTextarea = formInfo.isTextarea; + + for(var name in formInfo.fields){ + for (var i=0; i= 1 && fieldInfo.value && typeof fieldInfo.value.valueAttr != "undefined"){ + ele = doc.createElement("input"); + ele.setAttribute("type", fieldInfo.type); + ele.setAttribute("value", fieldInfo.value.valueAttr); + eleLabel = name +"["+ fieldInfo.value.valueAttr +"]"; + } + break; + + case "password": + case "hidden": + case "file": + case "text": + ele = doc.createElement("input"); + ele.setAttribute("type", fieldInfo.type); + break; + + case "textarea": + ele = doc.createElement("textarea"); + break; + + case "select": + if (formInfo.version && formInfo.version >= 1 && Lazarus.isArray(fieldInfo.value) && fieldInfo.value.length){ + ele = doc.createElement("select"); + + for (var i=0; i 1){ + ele.setAttribute("size", fieldInfo.value.length); + ele.setAttribute("multiple", "true"); + } + } + break; + + //no saved + case "submit": + case "reset": + case "button": + case "image": + break; + + case "iframe": + //ignore iframes for now? + break; + + default: + Lazarus.error("Unknown element type ["+ fieldInfo.type +"]"); + } + + if (ele){ + if (fieldInfo.type == "hidden"){ + form.appendChild(ele); + } + else { + var box = doc.createElement("div"); + box.setAttribute("class", "form-field-box"); + + var label = doc.createElement("label"); + var text = doc.createTextNode(eleLabel || name); + label.appendChild(text); + box.appendChild(label); + + ele.setAttribute("name", fieldInfo.name); + ele.setAttribute("class", "form-field "+ fieldInfo.type); + box.appendChild(ele); + form.appendChild(box); + } + } + } + } + + return form; +} + +/** +* save the last submitted form id for use in auto restore template +*/ +Lazarus.saveLastSubmittedForm = function(evt){ + var form = Lazarus.findFormFromElement(evt.target); + if (form){ + Lazarus.lastSubmittedFormId = Lazarus.getFormId(form); + } +} + +/** +* call autofill if this is a valid html document. +*/ +Lazarus.autofillEvent = function(evt){ + if (evt.originalTarget instanceof HTMLDocument){ + Lazarus.autofillDoc(evt.originalTarget); + } +} + + +/** +* convert a string of HTML into human readable text +*/ +Lazarus.htmlToText = function(html){ + + //replace headings () and paragraph ends with 2 line breaks + var text = html.replace(/\s*<\/((h\d)|p)\s*>\s*/ig," \n\n"); + + //replace divs blocks with single line breaks + text = text.replace(/\s*<(\/div)\b[^>]*>\s*/ig,"\n"); + + //replace list items with line breaks and dots + text = text.replace(/\s*<(li)\b[^>]*>\s*/ig,"\n * "); + + //replace line breaks + text = text.replace(/<(br)\b[^>]*>/ig,"\n"); + + //strip all other tags + text = text.replace(/<(\/|\w)[^>]*>/g,' '); + + //convert html spaces into normal spaces + text = text.replace(/ /g, ' '); + + //compress whitespace + text = text.replace(/[ \t\f\v]+/g, ' '); + + //never have more than 2 line breaks in a row. + text = text.replace(/\n\s*?\n(\s*?\n)*/g, "\n\n"); + + //and finally trim. + text = text.replace(/^\s+/, '').replace(/\s+$/, ''); + + return text; +} + + +/** +* return TRUE if forms on the given document should be saved +*/ +Lazarus.isValidDoc = function(doc){ + return (doc && doc.URL && /^(file|http|https):/.test(doc.URL)); +} + +/** +* autofills forms found in the given document with there respective templates if they exist +*/ +Lazarus.autofillDoc = function(doc){ + + if (Lazarus.isValidDoc(doc) && doc.forms && doc.forms.length){ + var rsAutoFillTemplates = Lazarus.db.rs("SELECT id, formid FROM forms WHERE autofill = 1 AND savetype = "+ Lazarus.FORM_TYPE_TEMPLATE +" ORDER BY created DESC"); + if (rsAutoFillTemplates.length > 0){ + var templates = {}; + //convert the template id's to a hash table + //TODO: there should only ever be one autofill template for a given form + //raise a warning if this is not the case + for (var i=0; i= 4){ + Lazarus.$('lazarus-statusbar-menuitem-test').hidden = false; + } +} + +/** +* handle text within the address bar changing +*/ +Lazarus.onLocationChange = function(evt){ + //only close the notification if current page was not the result of submitting a form. + if (!Lazarus.lastSubmittedFormId){ + Lazarus.closeNotificationBox(true); + } + Lazarus.refreshIcon(); +} + +/** +* fire the "clear private data" event +*/ +Lazarus.fireClearPrivateDataIfNoPrompt = function(){ + if (!Lazarus.getPref("privacy.sanitize.promptOnSanitize", true)){ + Lazarus.Event.fire("clear-private-data"); + } +} + + +/** +* return the current state of lazarus +*/ +Lazarus.getState = function(){ + if (!Lazarus.initalized){ + return Lazarus.STATE_UNINITALIZED; + } + else if (Lazarus.Crypto.generatingKeys || Lazarus.cleaningDatabase){ + return Lazarus.STATE_GENERATING_KEYS; + } + else if (!Lazarus.canEncrypt()){ + return Lazarus.STATE_DISABLED; + } + else if (Lazarus.isDisabledByPrivateBrowsing()){ + return Lazarus.STATE_PRIVATE_BROWSING; + } + else if (Lazarus.isPageDisabled()){ + return Lazarus.STATE_DISABLED_FOR_DOMAIN; + } + else if (!Lazarus.canDecrypt()){ + return Lazarus.STATE_PASSWORD_REQUIRED; + } + //all good? + else { + return Lazarus.STATE_ENABLED; + } +} + +/** +* fix for multiline xul:description elements +*/ +Lazarus.setDescriptionText = function(ele, text){ + for (var i=ele.childNodes.length-1; i>=0; i--){ + ele.removeChild(ele.childNodes[i]); + } + ele.appendChild(ele.ownerDocument.createTextNode(text)); +} + +/** +* updates the statusbar icon +*/ +Lazarus.refreshIcon = function(){ + + Lazarus.$("lazarus-statusbarpanel").hidden = !Lazarus.getExtPref("showInStatusbar"); + + var iconURL = ""; + var tooltipId = ""; + + switch (Lazarus.getState()){ + case Lazarus.STATE_ENABLED: + iconURL = "chrome://lazarus/skin/lazarus.png"; + tooltipId = "lazarus-statusbaricon-tooltip-enabled" + break; + + case Lazarus.STATE_GENERATING_KEYS: + iconURL = 'chrome://lazarus/skin/lazarus-loading.gif'; + tooltipId = 'lazarus-statusbaricon-tooltip-generatingkeys'; + break; + + case Lazarus.STATE_PASSWORD_REQUIRED: + iconURL = "chrome://lazarus/skin/lazarus-login.png"; + tooltipId = "lazarus-statusbaricon-tooltip-passwordrequired"; + break; + + case Lazarus.STATE_DISABLED_FOR_DOMAIN: + iconURL = "chrome://lazarus/skin/lazarus-disabled.png"; + tooltipId = "lazarus-statusbaricon-tooltip-disabledfordomain"; + break; + + case Lazarus.STATE_PRIVATE_BROWSING: + iconURL = "chrome://lazarus/skin/lazarus-disabled.png"; + tooltipId = "lazarus-statusbaricon-tooltip-private-browsing"; + break; + + case Lazarus.STATE_UNINITALIZED: + case Lazarus.STATE_DISABLED: + default: + iconURL = "chrome://lazarus/skin/lazarus-disabled.png"; + tooltipId = "lazarus-statusbaricon-tooltip-disabled"; + } + + //this appears to screw over firefox, missing images and such, if called during startup? + //NOTE: firfox must be completely closed for this effect, using "restart" will NOT recreate it. + //Lazarus.setDescriptionText(Lazarus.$("lazarus-statusbaricon-tooltip-description"), tooltip); + + Lazarus.$("lazarus-statusbarpanel-image").setAttribute("src", iconURL); + Lazarus.$("lazarus-statusbarpanel-image").setAttribute("tooltip", tooltipId); +} + +/** +* displays the lazarus welcome message +*/ +Lazarus.showWelcome = function(){ + //dont show the welcome message immediately, need to wait a sec so the + //browser is open, and we can center the dialog relative to it. + //~ setTimeout(function(){ + //~ Lazarus.openOptionsDialog("welcome-pane"); + //~ }, 100); + + //hmmm, session recovery is happening after this event, + //which is replacing our tab with the recovered sessions tabs + setTimeout(function(){ + Lazarus.openLazarusWebsite("oninstall.html?ver="+ Lazarus.getVersionStr()); + }, 3000); +} + +/** +* opens the lazarus onupdate page for this version of lazarus +*/ +Lazarus.showUpdatePage = function(){ + //hmmm, session recovery is happening after this event, + //which is replacing our tab with the recovered sessions tabs + setTimeout(function(){ + Lazarus.openLazarusWebsite("onupdate.html?ver="+ Lazarus.getVersionStr()); + }, 3000); +} + + +/** +* handle when the first browser window is opened. +*/ +Lazarus.onStartUp = function(){ + +} + +/** +* onUninstall +*/ +Lazarus.onUninstall = function(ext){ + + if (ext.id == Lazarus.guid){ + + //var msg = ''; + if (Lazarus.getExtPref("uninstall.removeSavedForms", false)){ + //remove database + //unable to disconnect the database, so the best we can do is empty it. + Lazarus.emptyDB(); + + } + if (Lazarus.getExtPref("uninstall.removeUserSettings", false)){ + //cleanup prefs + Lazarus.killPref("extensions.lazarus"); + Lazarus.Pref.savePrefFile(); + } + } +} + +/** +* +*/ +Lazarus.onUninstallRequest = function(ext){ + if (ext.id == Lazarus.guid){ + //ask the user if we should remove their preferences/saved forms + //we need this dialog to appear from the addons dialog (if it exists) + //~ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].getService(Components.interfaces.nsIWindowMediator); + //~ var win = wm.getMostRecentWindow("Extension:Manager"); + + //~ //if theres no extension manager window (eg all-in-one-sidebar) + //~ //use this window to open the dialog + //~ if (!win){ + //~ win = window; + //~ } + //disabling dialog, uninstall process is stuffed up + //win.openDialog("chrome://lazarus/content/uninstall.xul", "LazarusUninstallOptions", "chrome,dialog,modal,resizable=yes,titlebar=yes"); + } +} + + +/** +* return TRUE if the given editor still exists in the browser +*/ +Lazarus.editorExists = function(info){ + try { + //KLUDGE: + //if a page is refreshed then the page remains, even if you then navigate away from it. + //it appears to still exist, even though the user cannot see it. + if (Lazarus.isIframe(info.editor) && (!info.editor.contentWindow || !info.editor.contentWindow.document)){ + return false; + } + else { + return (info.editor.ownerDocument.defaultView && (info.url == info.editor.ownerDocument.defaultView.top.location.href)); + } + } + catch(e){ + return false; + } +} + + +Lazarus.saveEditorInfo = function(info, saveType){ + + + if (Lazarus.shouldSaveEditorInfo(info)){ + + var textHash = Lazarus.md5(info.text); + + if (saveType == Lazarus.FORM_TYPE_AUTOSAVE){ + var record = Lazarus.db.getRow("SELECT id, text_hash FROM textdata WHERE domain_hash = ?1 AND savetype = ?2 LIMIT 1", info.domainHash, Lazarus.FORM_TYPE_AUTOSAVE); + //if form hasn't changed + if (record && record.text_hash == textHash){ + //do nothing... + } + //if it has changed, or doesn't exist yet, save the changes + else { + Lazarus.debug("Saving textdata - autosave", info); + if (record){ + Lazarus.db.exe("DELETE FROM textdata WHERE id = ?1", record.id); + //and get rid of any fulltext index as well + Lazarus.db.exe("DELETE FROM textdata_fulltext WHERE docid = ?1", record.id); + } + var encText = Lazarus.encrypt(info.text); + var encSummary = Lazarus.encrypt(info.summary); + + var id = Lazarus.db.insert("INSERT INTO textdata (text_encrypted, summary_encrypted, created, domain_hash, url_encrypted, text_hash, text_length, savetype) \ + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + encText, encSummary, info.created, info.domainHash, info.urlEncrypted, textHash, info.text.length, Lazarus.FORM_TYPE_AUTOSAVE); + + //and update the full text index, + if (!Lazarus.getPref('extensions.lazarus.disableSearch')){ + //we're going to add domain info into this as well + var hashedText = Lazarus.hashText(info.text); + hashedText += ' '+ Lazarus.hashText(info.domain.replace(/\./g, ' ')); + Lazarus.db.exe("INSERT INTO textdata_fulltext (docid, hashed_text) VALUES (?1, ?2)", id, hashedText); + } + } + } + else { + + //if the info object already exists, with exactly the same text, then just update the timestamp + var id = Lazarus.db.getInt("SELECT id FROM textdata WHERE domain_hash = ?1 AND text_hash = ?2 LIMIT 1", info.domainHash, textHash); + if (id){ + Lazarus.debug("Updating textdata - perm", info); + Lazarus.db.exe("UPDATE textdata SET created = ?1, savetype = ?2 WHERE id = ?3", info.created, Lazarus.FORM_TYPE_NORMAL, id); + } + //otherwise, insert a new info object + else { + Lazarus.debug("Saving textdata - perm", info); + var encText = Lazarus.encrypt(info.text); + var encSummary = Lazarus.encrypt(info.summary); + + var id = Lazarus.db.insert("INSERT INTO textdata (text_encrypted, summary_encrypted, created, domain_hash, url_encrypted, text_hash, text_length, savetype) \ + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + encText, encSummary, info.created, info.domainHash, info.urlEncrypted, textHash, info.text.length, Lazarus.FORM_TYPE_NORMAL); + + if (!Lazarus.getPref('extensions.lazarus.disableSearch')){ + var hashedText = Lazarus.hashText(info.text); + hashedText += ' '+ Lazarus.hashText(info.domain.replace(/\./g, ' ')); + Lazarus.db.exe("INSERT INTO textdata_fulltext (docid, hashed_text) VALUES (?1, ?2)", id, hashedText); + } + } + } + } +} + + +/** +* genereate a random seed for use in the hashing function +*/ +Lazarus.generateRandomHashSeed = function(){ + var rnd = Math.random().toString() +':'+ Lazarus.timestamp(true).toString(); + return Lazarus.FNV1a(rnd); +} + + +/** +* hashes individual words in a bunch of text +*/ +Lazarus.hashText = function(text){ + + //we'll need to grab the seed from the database. + var seed = Lazarus.db.getStr("SELECT value FROM settings WHERE name = 'hash-seed'"); + + if (!seed){ + seed = Lazarus.generateRandomHashSeed(); + Lazarus.db.exe("INSERT INTO settings (name, value) VALUES ('hash-seed', ?1)", seed); + //and delete the full text index, because none of the others will work any more + Lazarus.db.exe("DELETE FROM textdata_fulltext"); + } + + //var p = new Profiler("hashedText: "+ text.length); + var map = {}; + //p.mark("setup"); + + //remove all non-text characters (strip HTML?) + text = Lazarus.trim(text.toLowerCase().replace(/[^\s\w\-_]+/g, ' ')); + //p.mark("remove non-text: "+ text.length); + + var words = text.split(/\s+/g); + //p.mark("split into words : "+ words.length); + //we should also remove useless words (like "the", "and", "as" etc...) + var len = words.length; + var hashedWords = []; + for (var i=0; i=0; i--){ + var info = Lazarus.editorInfos[i]; + //the page may have been submitted, or closed without submitting + //if the editor is suddenly empty, we should assume it has been submitted (possibly via AJAX) + if (!Lazarus.editorExists(info) || Lazarus.getEditorInfo(info.editor).isEmpty){ + removed.push(info); + Lazarus.editorInfos.splice(i, 1); + } + } + + //and save any that have been removed + for (var i=0; i]*>)|( )/g, '').match(/^\s*$/) ? true : false; + } + else { + throw Error("Unknown editor type: "+ type); + } +} + +/** +* return the text/html from an editable iframe or textarea +*/ +Lazarus.extractText = function(ele){ + var text = ''; + if (Lazarus.isTextarea(ele)){ + text = (typeof ele.value == "string") ? ele.value : ''; + } + else if (Lazarus.isIframe(ele)){ + text = (typeof ele.contentWindow.document.body.innerHTML) ? ele.contentWindow.document.body.innerHTML : ''; + } + return Lazarus.trim(text); +} + + +/** +* generate a safe summary for display within a XUL:menuitem or XUL:tooltip +*/ +Lazarus.generateSummary = function(text){ + //strip html + text = Lazarus.htmlToText(text); + return (text.length > 255) ? (text.substr(0, 252) +"...") : text; +} + +Lazarus.restartEditorAutoSaveTimer = function(){ + Lazarus.stopEditorAutoSaveTimer(); + Lazarus.editorAutoSaveFormTimer = setInterval(Lazarus.autoSaveEditors, Lazarus.getExtPref("autoSaveInterval", 2000)); +} +Lazarus.stopEditorAutoSaveTimer = function(){ + if (Lazarus.editorAutoSaveFormTimer){ + clearInterval(Lazarus.editorAutoSaveFormTimer); + } +} + + +/** +* start the autosave timer +*/ +Lazarus.restartAutoSaveTimer = function(form){ + Lazarus.stopAutoSaveTimer(); + //we'll save the form here, so we can retrieve it when the timer fires + Lazarus.currAutoSaveForm = form; + Lazarus.autoSaveFormTimer = setTimeout(Lazarus.autoSaveForm, Lazarus.getExtPref("autoSaveInterval", 2000)); +} + +/** +* stops the autosave timer. +*/ +Lazarus.stopAutoSaveTimer = function(){ + if (Lazarus.autoSaveFormTimer){ + clearTimeout(Lazarus.autoSaveFormTimer); + } +} + +/** +* run cleanup for this window +*/ +Lazarus.cleanup = function(){ + gBrowser.removeEventListener("keyup", Lazarus.onKeyUp, false); + gBrowser.removeEventListener("submit", Lazarus.onFormSubmit, false); + gBrowser.removeEventListener("reset", Lazarus.onFormReset, false); + gBrowser.removeEventListener("change", Lazarus.onFormChange, false); + Lazarus.$("Tools:Sanitize").removeEventListener("command", Lazarus.fireCLearPrivateDataIfNoDialog, false); + Lazarus.stopCleanupTimer(); +} + +/** +* handle last browser window shutting down +*/ +Lazarus.onShutdown = function(){ + +} + +/** +* +*/ +Lazarus.onContextMenuHide = function(){ + Lazarus.isContextMenuShowing = false; +} + + +/** +* show or hide the menu item depending on what item caused the context menu to appear. +*/ +Lazarus.onContextMenuShowing = function(evt){ + + //we need to set a flag to prevent new autosaves whilst the context menu is shown + Lazarus.isContextMenuShowing = true; + + //this event is fired whenever a submenu is shown, as well as when the main context menu it shown + //bugfix: only re-calculate the popup menu when it's first opened. + //trying to alter the initial menu when a submenu is opened can cause the browser to hang. + + //hmmm this is causing a noticable hang when the form contains a lot (30k) of info + //as a workaround, we will not calculate any submenu until the submenu is opened. + var evtTargetId = evt.target.id; + + //quick check + if (evtTargetId != "contentAreaContextMenu" && evtTargetId != "lazarus-restoreform-submenu-menupopup" && evtTargetId != "lazarus-restoretext-submenu-menupopup"){ + return; + } + + + //did the user click on a form? + var form = Lazarus.findFormFromElement(gContextMenu.target); + var editor = Lazarus.findEditorFromElement(gContextMenu.target); + + if (evtTargetId == "contentAreaContextMenu"){ + + //assume we should not show any menuitems + var showMainMenu = false; + var showSubMenu = false; + var showLogin = false; + var showSaveForm = false; + var showPageDisabled = false; + var showPrivateBrowsing = false; + + var showRestoreText = false; + var showRestoreTextDisabled = false; + + + if (!form && !editor){ + //dont show anything + } + else if (Lazarus.isDisabledByPrivateBrowsing()){ + showPrivateBrowsing = true; + } + else if (Lazarus.isPageDisabled(form.ownerDocument.URL)){ + showPageDisabled = true; + } + else if (!Lazarus.canDecrypt()){ + showLogin = true; + } + else { + if (form){ + + showSaveForm = true; + var savedForms = Lazarus.getFormInfo(form, "id, formid, created, savetype, forminfohash, formtext, formname"); + + //just show the one menu item, but dont show it if the form is identical to this one. + if (savedForms.length == 1){ + showMainMenu = true; + var info = Lazarus.formInfo(form); + + var infoHash = Lazarus.generateHash(info.fields); + + if (infoHash == savedForms[0]["forminfohash"]){ + Lazarus.$('lazarus-restoreform-contextmenuitem').setAttribute('src', "chrome://lazarus/skin/lazarus-disable.png"); + Lazarus.$('lazarus-restoreform-contextmenuitem').setAttribute('disabled', "true"); + Lazarus.$('lazarus-restoreform-contextmenuitem').setAttribute('tooltiptext', Lazarus.getString("form.is.equal")); + } + else { + var savedForm = savedForms[0]; + Lazarus.$('lazarus-restoreform-contextmenuitem').setAttribute('src', "chrome://lazarus/skin/lazarus.png"); + Lazarus.$('lazarus-restoreform-contextmenuitem').setAttribute('lazarus-forms-id', savedForm["id"]); + Lazarus.$('lazarus-restoreform-contextmenuitem').setAttribute('tooltiptext', Lazarus.generateSavedFormTooltip(savedForm)); + Lazarus.$('lazarus-restoreform-contextmenuitem').setAttribute('disabled', ""); + } + } + //show submenu + else if (savedForms.length > 1){ + showSubMenu = true; + } + } + + if (editor){ + showRestoreTextDisabled = true; + //do we have any text saved for this domain/basedomain? + var domain = Lazarus.getDomainFromElement(editor); + if (domain){ + var domainHash = Lazarus.md5(domain); + if (Lazarus.db.getInt("SELECT count(id) FROM textdata WHERE domain_hash = ?1 LIMIT 1", domainHash)){ + showRestoreText = true; + showRestoreTextDisabled = false; + } + } + } + } + + //only show our menu item if over a form. + Lazarus.$('lazarus-restoretextdisabled-contextmenuitem').hidden = !showRestoreTextDisabled; + Lazarus.$('lazarus-restoretext-submenu').hidden = !showRestoreText; + Lazarus.$('lazarus-restoreform-contextmenuitem').hidden = !showMainMenu; + Lazarus.$('lazarus-restoreform-submenu').hidden = !showSubMenu; + Lazarus.$('lazarus-enterpassword-contextmenuitem').hidden = !showLogin; + Lazarus.$('lazarus-domaindisabled-contextmenuitem').hidden = !showPageDisabled; + Lazarus.$('lazarus-privatebrowsing-contextmenuitem').hidden = !showPrivateBrowsing; + Lazarus.$('lazarus-saveform-contextmenuitem').hidden = !(showSaveForm && Lazarus.getExtPref("includeExperimental", false)); + } + else if (evtTargetId == "lazarus-restoreform-submenu-menupopup" && form){ + Lazarus.buildSubMenu(form); + } + else if (evtTargetId == "lazarus-restoretext-submenu-menupopup" && editor){ + Lazarus.buildRestoreTextSubMenu(editor); + } +} + + + +Lazarus.getDomainFromElement = function(ele){ + try { + return ele.ownerDocument.defaultView.top.location.host; + } + catch(e){ + return null; + } +} + +/** +* builds the list of items that can be restored for this editor element +*/ +Lazarus.buildRestoreTextSubMenu = function(editor){ + // + var menu = Lazarus.$("lazarus-restoretext-submenu-menupopup"); + //remove all the current submenu items + while(menu.lastChild){ + menu.removeChild(menu.lastChild); + } + + var domainHash = Lazarus.md5(Lazarus.getDomainFromElement(editor)); + + //and build the new ones + var items = Lazarus.db.rs("SELECT id, text_hash, summary_encrypted, created FROM textdata WHERE domain_hash = ?1 ORDER BY created DESC LIMIT ?2", domainHash, Lazarus.getPref('extensions.lazarus.maxTextItemsInSubmenu', 20)); + + var text = Lazarus.extractText(editor); + var textHash = Lazarus.md5(text); + + for (var i=0; i Lazarus.MIN_TEXT_NEEDED_TO_SHOW_NOTIFICATION) && Lazarus.getPref("extensions.lazarus.showDonateNotification") && Lazarus.getPref("extensions.lazarus.restoreFormCount", 0) > 3){ + var msgId = Math.floor(Math.random() * 4); + Lazarus.showNotificationBox("form-restored", Lazarus.getFormRestoredNotificationText(text.length, msgId), "msgid-"+ msgId); + } + } + else { + alert(Lazarus.getString("error.form.db.corrupt")); + } + } + else { + Lazarus.error("Unable to find textdata: "+ id); + alert(Lazarus.getString("error.form.not.found")); + } + } + else { + //should never get here + alert(Lazarus.getString("error.form.object.not.found")); + } +} + + +/** +* generates an md5 hash of a javascript object +*/ +Lazarus.generateHash = function(obj){ + return Lazarus.md5(Lazarus.JSON.encode(obj)); +} + +/** +* return the current unix timestamp +*/ +Lazarus.timestamp = function(asFloat){ + var s = new Date().getTime() / 1000; + return asFloat ? s : Math.floor(s); +} + +/** +* return text usable by a menuitem (XUL Label) +*/ +Lazarus.getMenuItemText = function(text){ + + //strip any html found in the text + text = Lazarus.trim(Lazarus.cleanText(text)); + + //labels only handle a single line of text + text = text.split(/\n/, 2)[0]; + //and we dont want it to be too long + if (text.length > 48){ + text = text.substr(0, 48) +"..."; + } + return Lazarus.trim(text); +} + + +/** +* ask the user for their Lazarus password +*/ +Lazarus.showEnterPasswordDialog = function(){ + + var prompts = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(Components.interfaces.nsIPromptService); + + //keep asking until they hit cancel + while(true){ + var password = {value: ""}; + var check = {value: false}; + //if a user has a master password set, then allow the lazarus password to be saved in the SSD + var checkText = Lazarus.isMasterPasswordSet() ? Lazarus.getString('password.dialog.checkbox.label') : null; + // + if (prompts.promptPassword(null, Lazarus.getString("password.dialog.title"), Lazarus.getString("password.dialog.label"), password, checkText, check)){ + if (Lazarus.loadPrivateKey(password.value)){ + if (check.value){ + Lazarus.savePassword(password.value); + } + return true; + } + } + else { + break; + } + } + + return false; +} + + +Lazarus.savePassword = function(password){ + //remove the existing password first. + Lazarus.removePassword(); + var nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", Components.interfaces.nsILoginInfo, "init"); + var loginInfo = new nsLoginInfo(Lazarus.LOGIN_HOSTNAME, null, Lazarus.LOGIN_REALM, Lazarus.LOGIN_USERNAME, password, '', ''); + var nsLoginManager = Components.classes["@mozilla.org/login-manager;1"].getService(Components.interfaces.nsILoginManager); + nsLoginManager.addLogin(loginInfo); +} + + +Lazarus.removePassword = function(){ + + // Get Login Manager + var nsLoginManager = Components.classes["@mozilla.org/login-manager;1"].getService(Components.interfaces.nsILoginManager); + // Find users for this extension + var logins = nsLoginManager.findLogins({}, Lazarus.LOGIN_HOSTNAME, null, Lazarus.LOGIN_REALM); + for (var i = 0; i < logins.length; i++) { + if (logins[i].username == Lazarus.LOGIN_USERNAME){ + nsLoginManager.removeLogin(logins[i]); + } + } +} + +/** +* load the password from the loginManager +*/ +Lazarus.loadPassword = function(){ + + if (Lazarus.isMasterPasswordSet() && !Lazarus.isMasterPasswordRequired()){ + // Get Login Manager + var nsLoginManager = Components.classes["@mozilla.org/login-manager;1"].getService(Components.interfaces.nsILoginManager); + + // Find users for the given parameters + var logins = nsLoginManager.findLogins({}, Lazarus.LOGIN_HOSTNAME, null, Lazarus.LOGIN_REALM); + + // Find user from returned array of nsILoginInfo objects + for (var i = 0; i < logins.length; i++) { + if (logins[i].username == Lazarus.LOGIN_USERNAME){ + return logins[i].password; + } + } + } + //we default to an empty password + return ''; +} + +Lazarus.logout = function(){ + Lazarus.unloadPrivateKey(); + Lazarus.refreshIcon(); +} + +/** +* generate a label for saved form menuitem +*/ +Lazarus.generateSavedFormLabel = function(savedForm, time){ + var label = ''; + + //template should ALWAYS use their template name as a label + if (savedForm["savetype"] == Lazarus.FORM_TYPE_TEMPLATE){ + label = savedForm["formname"]; + } + //use summary + else if (Lazarus.getExtPref("showFormTextInSubMenu") && Lazarus.canDecrypt()){ + label = Lazarus.getMenuItemText(Lazarus.decrypt(savedForm["formtext"])) || ("["+ Lazarus.getString("untitled") +"]"); + } + //use timestamps if we are unable to decrypt the forms + else { + time = time || Lazarus.timestamp(); + label = Lazarus.getTimeString(time - savedForm["created"]); + } + + //mark autosaved forms so users can easily tell the difference. + switch(savedForm["savetype"]){ + case Lazarus.FORM_TYPE_AUTOSAVE: + case Lazarus.FORM_TYPE_STALE_AUTOSAVE: + label += " "+ Lazarus.getString("menuitem.label.append.autosave"); + break; + + case Lazarus.FORM_TYPE_TEMPLATE: + label += " "+ Lazarus.getString("menuitem.label.append.template"); + break; + + default: + label += " "+ Lazarus.getString("menuitem.label.append.normal"); + break; + } + + return label; +} + +/** +* generate a tooltip for saved form menuitem +*/ +Lazarus.generateSavedFormTooltip = function(savedForm, /**/time){ + + time = time || Lazarus.timestamp(); + + var tooltip = ''; + + //show time sting as tooltip + if (Lazarus.getExtPref("showFormTextInSubMenu")){ + + tooltip = Lazarus.getTimeString(time - savedForm["created"]); + } + //show date and time of save + else { + var date = new Date(savedForm["created"] * 1000); + tooltip = Lazarus.formatDate(date); + } + + //mark autosaved forms so users can easily tell the difference. + switch(savedForm["savetype"]){ + case Lazarus.FORM_TYPE_AUTOSAVE: + case Lazarus.FORM_TYPE_STALE_AUTOSAVE: + tooltip += " "+ Lazarus.getString("menuitem.tooltip.append.autosave"); + break; + + case Lazarus.FORM_TYPE_TEMPLATE: + tooltip += " "+ Lazarus.getString("menuitem.tooltip.append.template"); + break; + + default: + tooltip += " "+ Lazarus.getString("menuitem.tooltip.append.normal"); + break; + //do nothing + } + + return tooltip; +} + +/** +* builds the list of available restore point into the submenu +*/ +Lazarus.buildSubMenu = function(form){ + + + var menu = Lazarus.$("lazarus-restoreform-submenu-menupopup"); + //remove all the current submenu items + for (var i=menu.childNodes.length-1; i>=0; i--){ + var node = menu.childNodes[i]; + if (node.getAttribute("lazarus-dynamic-submenu")){ + menu.removeChild(node); + } + } + + var time = Lazarus.timestamp(); + var separator = Lazarus.$("lazarus-submenu-separator"); + //now build the new ones + var lastSaveType = -1; + var savedForms = Lazarus.getFormInfo(form, "id, formid, created, savetype, forminfohash, formtext, formname, forminfo"); + var info = Lazarus.formInfo(form); + var infoHash = Lazarus.generateHash(info.fields); + + for (var i=0; i 0) ? (Lazarus.timestamp() - expires) : 0; + + fields = fields || "*"; + //we need to get ALL the templates for this form first + var forms = []; + forms = forms.concat(Lazarus.db.rs("SELECT "+ fields +" FROM forms WHERE formid = ?1 AND created >= ?2 AND savetype = "+ Lazarus.FORM_TYPE_TEMPLATE +" ORDER BY savetype DESC, created DESC", formId, cutoffTime)); + //and then the autosaves + var maxAutoSaves = Lazarus.getExtPref("maxAutosavesPerForm", 3); + forms = forms.concat(Lazarus.db.rs("SELECT "+ fields +" FROM forms WHERE formid = ?1 AND created >= ?2 AND savetype IN ("+ Lazarus.FORM_TYPE_AUTOSAVE +","+ Lazarus.FORM_TYPE_STALE_AUTOSAVE +") ORDER BY created DESC LIMIT ?3", formId, cutoffTime, maxAutoSaves)); + //and then add normal saved forms + var maxForms = Lazarus.getExtPref("maxSavesPerForm", 10); + forms = forms.concat(Lazarus.db.rs("SELECT "+ fields +" FROM forms WHERE formid = ?1 AND created >= ?2 AND savetype = "+ Lazarus.FORM_TYPE_NORMAL +" ORDER BY created DESC LIMIT ?3", formId, cutoffTime, maxForms)); + return forms; +} + + +/** +* return a fieldInfo object filled with information about the given field +*/ +Lazarus.fieldInfo = function(ele){ + + var getPassword = Lazarus.getExtPref("savePasswordFields"); + var getHidden = Lazarus.getExtPref("saveHiddenFields"); + + var info = {}; + info.name = ele.getAttribute("name"); + info.type = Lazarus.getElementType(ele); + info.value = Lazarus.getElementValue(ele); + if (info.type == "password" && !getPassword){ + info.value = null; + } + if (info.type == "hidden" && !getHidden){ + info.value = null; + } + + switch (info.type){ + case "text": + case "textarea": + case "file": + case "password": + case "iframe": + if (info.value && Lazarus.trim(info.value)){ + info.text = Lazarus.trim(info.value); + } + break; + + default: + } + + return info; +} + +/** +* clean HTML tags and entities from an html string +*/ +Lazarus.cleanText = function(text){ + return text.replace(/<[^>]*>/g, ' ').replace(/&\w+;?/g, ' ').replace(/ +/g, ' '); +} + +/** +* return a formInfo object filled with details about a form +*/ +Lazarus.formInfo = function(form){ + + var info = {}; + info.version = Lazarus.FORM_INFO_VERSION; + info.formid = Lazarus.getFormId(form); + info.action = Lazarus.getUrlPage(form.action || form.ownerDocument.URL); + info.origURL = form.ownerDocument.URL; + info.domain = Lazarus.getDomainFromElement(form); + info.method = (form.method && form.method.toLowerCase()) == "post" ? "post" : "get"; + info.enctype = form.enctype ? form.enctype.toLowerCase() : ''; + info.fields = {}; + info.formtext = []; + info.textLen = 0; + for (var i=0; i 0){ + info.fields[Lazarus.IFRAME_NAME] = []; + info.isIframe = true; for (var i=0; i= 1){ + alert(Lazarus.getString("error.restore.partial")); + } + return (iRestored / iNamedElements); + } + else { + return true; + } +} + +/** +* handle the onselectmenuitem event +*/ +Lazarus.onRestoreFormMenuItem = function(menuitem){ + + if (!Lazarus.canDecrypt()){ + Lazarus.debug("Unable to restore form, password required"); + Lazarus.showNotificationBox("password-required"); + return; + } + + var id = parseInt(menuitem.getAttribute("lazarus-forms-id")); + + Lazarus.debug("Attempting to restore form "+ id); + var form = Lazarus.findFormFromElement(gContextMenu.target, "form"); + if (form){ + var row = Lazarus.db.getRow("SELECT * FROM forms WHERE id = ?1", id); + if (row){ + var formInfo; + try { + formInfo = Lazarus.JSON.decode(Lazarus.decrypt(row["forminfo"])); + } + catch(e){ + Lazarus.error(e); + } + + if (formInfo){ + //if we fully restore a form, then show the "donate" popup + if (Lazarus.restoreForm(form, formInfo) === true){ + Lazarus.incPref("extensions.lazarus.restoreFormCount"); + if ((formInfo.textLen > Lazarus.MIN_TEXT_NEEDED_TO_SHOW_NOTIFICATION) && Lazarus.getPref("extensions.lazarus.showDonateNotification") && Lazarus.getPref("extensions.lazarus.restoreFormCount", 0) > 3){ + var msgId = Math.floor(Math.random() * 4); + Lazarus.showNotificationBox("form-restored", Lazarus.getFormRestoredNotificationText(formInfo.textLen, msgId), "msgid-"+ msgId); + } + } + } + else { + alert(Lazarus.getString("error.form.db.corrupt")); + } + } + else { + alert(Lazarus.getString("error.form.not.found")); + } + } + else { + alert(Lazarus.getString("error.form.object.not.found")); + } +} + +/** +* return TRUE if form is a form we should save +*/ +Lazarus.shouldSaveForm = function(form){ + //only save forms on file/http/https sites + var doc = form.ownerDocument; + if (doc && doc instanceof HTMLDocument && doc.URL && /^(file|http|https):/.test(doc.URL) && !Lazarus.isPageDisabled(doc.URL) && !Lazarus.isDisabledByPrivateBrowsing()){ + return (Lazarus.isSearchForm(form)) ? Lazarus.getPref("extensions.lazarus.saveSearchForms") : true; + } + else { + return false; + } +} + +/** +* return TRUE if we should be saving this info +*/ +Lazarus.shouldSaveEditorInfo = function(info){ + if (Lazarus.isPageDisabled(info.url)){ + return false; + } + else if (Lazarus.isDisabledByPrivateBrowsing()){ + return false; + } + else { + return true; + } +} + + + +Lazarus.disabledDomains = null; + +Lazarus.isPageDisabled = function(url){ + if (!url && content.document && content.document.URL){ + url = content.document.URL; + } + if (url){ + var domainId = Lazarus.urlToDomainId(url); + if (domainId){ + var domains = Lazarus.getDisabledDomains(); + return (typeof domains[domainId] !== "undefined") ? domains[domainId] : false; + } + } + return false; +} + +/** +* return TRUE if lazarus can save forms from this site +* return FALSE for non-valid sites (eg about:config, chrome://... etc..) +*/ +Lazarus.isValidSite = function(url){ + //default to the current documents url + if (!url && content.document && content.document.URL){ + url = content.document.URL; + } + return (url && Lazarus.urlToDomainId(url)) ? true : false; +} + +Lazarus.getDisabledDomains = function(){ + var domainlist = Lazarus.getPref('extensions.lazarus.domainBlacklist', ''); + + if (!Lazarus.disabledDomains || Lazarus.disabledDomains['__origList__'] != domainlist){ + //rebuild the list + Lazarus.disabledDomains = { + '__origList__': domainlist + } + var domains = domainlist.split(/\s*,\s*/g); + for (var i=0; i -1 && uri.port != 80) ? uri.port : '') +'//'+ Lazarus.getBaseDomain(uri.host) +'/') : ''; +} + + +/** +* return TRUE if form appears to be a search form +*/ +Lazarus.isSearchForm = function(form){ + //search forms are forms that have exactly one textbox, and may contain a number of other non-text fields + var MAX_TEXT_FIELDS = 1; + var MAX_NON_TEXT_FIELDS = 5; + + var iTextFields = 0; + var iNonTextFields = 0; + + for (var i=0; i (TEXT_DIFFERENCE * 2)){ + var str = oldForm.formtext.substr(0, oldForm.formtext.length - TEXT_DIFFERENCE); + //if new form is a continuation of old form (ie the first (X - TEXT_DIFFERENCE) characters are still the same), + //then overwrite current autosave, + //otherwise create a new autosave. + return (newForm.formtext.indexOf(str) == -1); + } + //form contains little text, overwrite the current autosave + else { + return false; + } +} + +/** +* automatically save the form the user is working on. +*/ +Lazarus.autoSaveForm = function(){ + //does the form still exist? + if (Lazarus.currAutoSaveForm && !Lazarus.isContextMenuShowing){ + Lazarus.saveForm(Lazarus.currAutoSaveForm, Lazarus.FORM_TYPE_AUTOSAVE); + } +} + +/** +* start the cleanup timer +*/ +Lazarus.startCleanupTimer = function(){ + + Lazarus.stopCleanupTimer(); + + if (Lazarus.getExpiryTime()){ + Lazarus.cleanupSavedFormsTimer = setInterval(Lazarus.cleanupSavedForms, 1000 * 60); + } +} + +/** +* stop the cleanup timer +*/ +Lazarus.stopCleanupTimer = function(){ + if (Lazarus.cleanupSavedFormsTimer){ + clearInterval(Lazarus.cleanupSavedFormsTimer); + } +} + +/** +* remove old forms from the database +*/ +Lazarus.cleanupSavedForms = function(){ + + //dont cleanup any forms if a user might be trying to retore them + if (Lazarus.isContextMenuShowing){return} + + var expires = Lazarus.getExpiryTime(); + + if (expires > 0){ + //add a couple of minutes to the expiry time, so we don't accidentally + //remove a form whilst we are current restoring it. + var cutoffTime = Lazarus.timestamp() - (expires + 120); + var ids = Lazarus.db.getColumn("SELECT id FROM forms WHERE created < ?1 AND savetype != "+ Lazarus.FORM_TYPE_TEMPLATE, cutoffTime); + Lazarus.removeForms(ids); + + var ids = Lazarus.db.getColumn("SELECT id FROM textdata WHERE created < ?1", cutoffTime); + if (ids.length > 0){ + Lazarus.db.exe("DELETE FROM textdata WHERE id IN ("+ ids.join(",") +")"); + Lazarus.db.exe("DELETE FROM textdata_fulltext WHERE docid IN ("+ ids.join(",") +")"); + } + } + else { + Lazarus.stopCleanupTimer(); + } +} + +/** +* return an identifier for a url +*/ +Lazarus.getUrlPage = function(url){ + var uri = Lazarus.urlToURI(url); + if (uri){ + return uri.scheme +"://"+ uri.hostPort + uri.path.replace(/[\?#].*$/, ''); + } + //for testing on file systems + else if ((Lazarus.getExtPref("debugMode") >= 3) && form.action.match(/^file:\/\//i)){ + return "file://just/testing"; + } + else { + return null; + } +} + +/** +* return the user editable fields within a form. +*/ +Lazarus.getEditableFields = function(form){ + var fields = []; + var saveHidden = Lazarus.getExtPref("saveHiddenFields"); + var savePassword = Lazarus.getExtPref("savePasswordFields"); + + for (var i=0; i= 3) && action.match(/^file:\/\//i)){ + formId = "file://just/testing"; + } + + //and point to the same place + if (formId){ + + //support for ajax textareas + if (form.isTextarea){ + formId += "