Skip to content

Instantly share code, notes, and snippets.

@LouCypher
Created June 17, 2011 04:22
Show Gist options
  • Save LouCypher/1030865 to your computer and use it in GitHub Desktop.
Save LouCypher/1030865 to your computer and use it in GitHub Desktop.
Custom Buttons XML Exporter/Importer for Custom Buttons extension
/*
Changelog:
* 2011-06-26:
1. Fixed all known issues.
2. No longer using toolbaritems, the cause of all issues.
* 2011-06-15:
1. Includes button's image as XML favicon
2. No longer using <script>
3. Fixed: namespace bug.
* 2011-06-14:
1. Includes template and CSS as you can see if you click the green button above.
I need your inputs in comment section about the template.
2. Yep! I changed the name again.
* 2011-06-12: Added 'Export to XML' to CB contextmenu.
* 2011-06-11: No more third button (dropmarker).
* 2011-06-10:
1. Fixed: toolbaritem (2-button) wasn't removed when this button was deleted.
Thanks to Morat.
2. Fixed: toolbaritem (2-button) wasn't removed when this button was moved.
3. Import XML file that is opened in browser window. Open this XML file to test it.
4. Supports application/xml content type as well as text/xml.
* 2011-06-09:
1. Initial release
2. Fixed: error with Cyrillic characters
3. Changed its name from Save/Load to Export/Import.
4. CB contextmenu now works with the two buttons.
5. Check for valid CB XML before installing an XML file to a new button.
Credits:
Contextmenu Icon by PixelMixer
- http://pixelmixer.ru/
- http://pixel-mixer.com/
References:
- https://addons.mozilla.org/firefox/files/browse/93414/file/content/loadsaveutils.js
- https://developer.mozilla.org/en/nsIFilePicker
- https://developer.mozilla.org/en/Code_snippets:File_I/O
- https://developer.mozilla.org/en/Parsing_and_serializing_XML
- https://developer.mozilla.org/en/Using_the_Stylesheet_Service
*/
this.checkDocumentForCBXML(content.document);
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License
* Version 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* Original code is Export Button to XML File/Import XML File As New Button
* for Custom Buttons extension
*
* The Initial Developer of the Original Code is LouCypher.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* - LouCypher: original code
* - Morat: onDestroy event, bug report
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"),
* in which case the provisions of the GPL are applicable instead of those
* above.
*
* ***** END LICENSE BLOCK ***** */
const nsIFilePicker = Ci.nsIFilePicker;
const nsILocalFile = Ci.nsILocalFile;
function $(aId) {
return document.getElementById(aId);
}
var lastDirectory = {
_lastDir: null,
get path() {
if (!this._lastDir || !this._lastDir.exists()) {
try {
this._lastDir = cbu.ps.getComplexValue("custombuttons.XML.lastDir",
nsILocalFile);
if (!this._lastDir.exists())
this._lastDir = null;
}
catch(e) {}
}
return this._lastDir;
},
set path(val) {
if (!val || !val.exists() || !val.isDirectory())
return;
this._lastDir = val.clone();
cbu.ps.setComplexValue("custombuttons.XML.lastDir",
nsILocalFile, this._lastDir);
}
}
function saveFile(aFileName, aStrData) {
var fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
fp.appendFilters(nsIFilePicker.filterXML);
fp.init(window, "Export button to XML file", nsIFilePicker.modeSave);
fp.defaultString = aFileName;
fp.displayDirectory = lastDirectory.path;
var res = fp.show();
if (res == nsIFilePicker.returnOK || res == nsIFilePicker.returnReplace) {
lastDirectory.path = fp.file.parent.QueryInterface(nsILocalFile);
var ostream = Cc["@mozilla.org/network/file-output-stream;1"].
createInstance(Ci.nsIFileOutputStream);
ostream.init(fp.file, 0x02 | 0x08 | 0x20, 0664, 0);
var charset = "UTF-8";
var os = Cc["@mozilla.org/intl/converter-output-stream;1"].
createInstance(Ci.nsIConverterOutputStream);
os.init(ostream, charset, 4096, 0x0000);
os.writeString(aStrData);
os.close();
}
}
function readFile(file) {
var data = "";
var fstream = Cc["@mozilla.org/network/file-input-stream;1"].
createInstance(Ci.nsIFileInputStream);
fstream.init(file, -1, 0, 0);
var charset = "UTF-8";
const replacementChar = Ci.nsIConverterInputStream
.DEFAULT_REPLACEMENT_CHARACTER;
var is = Cc["@mozilla.org/intl/converter-input-stream;1"].
createInstance(Ci.nsIConverterInputStream);
is.init(fstream, charset, 1024, replacementChar);
var str = {};
while (is.readString(4096, str) != 0) {
data += str.value;
}
is.close();
return data;
}
function stringToDOM(aString) {
// https://developer.mozilla.org/en/Parsing_and_serializing_XML
var parser = new DOMParser();
var dom = parser.parseFromString(aString, "text/xml");
if (dom.documentElement.nodeName == "parsererror") {
return null;
} else {
return dom.documentElement;
}
}
function importXMLtoButton(aStrXMLData) {
loadURI("custombutton://" + escape(aStrXMLData));
}
this.checkDocumentForCBXML = function(aDocument) {
if (((aDocument.contentType == "text/xml") ||
(aDocument.contentType == "application/xml"))&&
(aDocument.documentElement.localName == "custombutton")) {
var serializer = new XMLSerializer();
var xml = serializer.serializeToString(aDocument);
importXMLtoButton(xml);
} else {
this.loadXML();
}
}
this.saveXML = function(aStrURI) {
var cbURI = (aStrURI != undefined) ? aStrURI : readFromClipboard();
if (!cbURI || !/^custombutton\:\/\//.test(cbURI)) {
custombuttons.uChelpButton(this);
return;
}
var cbXML = cbURI.replace(/^custombutton\:\/\//, "");
var decodeXML = unescape(cbXML);
var btnName = decodeXML.match(/\<name\/?.+/).toString();
var name = "untitled";
if (!/\<name\/\>/.test(btnName)) {
name = btnName.replace(/\<\/?\w+\>/g, "").toString();
}
var image = decodeXML.match(/\<image\/?.+/).toString();
var icon = "";
if (!/\<\image.*\[\].*\>$/.test(image)) {
icon = image.match(/[^\[\]]+/g)[2].toString()
.replace(/custombuttons\-stdicon\-\d/, "").toString();
}
var xmlTemplate = "custombuttons/\"\n\
xmlns:html=\"http://www.w3.org/1999/xhtml\">\n\
<html:head>\n\
<html:title><![CDATA[" + name + "]]></html:title>\n\
<html:link rel=\"shortcut icon\" href=\"" + icon + "\"/>\n\
<html:style type=\"text/css\"><![CDATA[\
body { font-size: medium; margin: 0; }\n\
body, code:before, help:before, initcode:before {\n\
font-family: \"Verdana\", sans-serif;\n\
} \n\
#wrapper { position: fixed; top: 1em; right: 1em; text-align: center; }\n\
p { font-size: small; text-align: center; }\n\
#button {\n\
background-image: -moz-linear-gradient(center top, rgb(147, 200, 94) 30%,\
rgb(85, 168, 2) 55%);\n\
border: 1px outset rgb(58, 116, 4);\n\
border-radius: 1em;\n\
-moz-border-radius: 1em;\n\
padding: 0;\n\
text-shadow: 0pt -1px 0pt rgb(58, 116, 4);\n\
margin-bottom: 1em;\n\
}\n\
#button a {\n\
color: rgb(255, 255, 255);\n\
padding: 1em;\n\
text-decoration: none;\n\
}\n\
#button a, code, code:before, initcode, initcode:before, help, help:before {\
\n display: block;\n\
}\n\
#credits { position: fixed; bottom: 1em; right: 1em; font-size: small; }\n\
custombutton { background-color: rgb(171, 171, 171); margin: 1em; }\n\
image, mode, accelkey { display: none; }\n\
name { font-weight: bold; font-size: x-large; }\n\
code:before, help:before, initcode:before {\n\
font-weight: bold;\n\
font-size: large;\n\
margin: 0 0 1em;\n\
padding: .5em;\n\
}\n\
code:before { content: \"CODE\"; }\n\
help:before { content: \"Help\"; }\n\
initcode:before { content: \"Initialization Code\"; }\n\
code, initcode, help {\n\
background-color: rgb(255, 255, 255);\n\
border: 1px inset rgb(170, 170, 170);\n\
font: medium monospace;\n\
margin: 1em 1em 2em 0;\n\
padding: 1em;\n\
text-align: left;\n\
width: 840px;\n\
white-space: pre-wrap;\n\
word-wrap: break-word;\n\
}\n\
.clear { clear: both; }\n\
]]></html:style>\n\
</html:head>\n\
<html:body>\n\
<html:div id=\"wrapper\">\n\
<html:div id=\"button\">\n\
<html:a href=\"" + cbURI + "\" rel=\"nofollow\" title=\"" +
name +"\">\n\
<![CDATA[Install this button]]>\n\
</html:a>\n\
</html:div>\n\
<html:a href=\"https://addons.mozilla.org/addon/custom-buttons/\">\n\
<![CDATA[What's this?]]>\n\
</html:a>\n\
<html:div id=\"credits\">\n\
<html:a href=\"http://custombuttons.mozdev.org/drupal/node/484\">\n\
<![CDATA[Custom Buttons XML]]><html:br/>\
<![CDATA[Exporter/Importer]]>\n\
</html:a>\n\
</html:div>\n\
</html:div>\n\
</html:body>\n";
decodeXML = decodeXML.replace(/custombuttons\/\"\>/, xmlTemplate);
name += ".xml";
saveFile(name, decodeXML);
}
this.loadXML = function() {
var fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
fp.init(window,
"Import an XML file and install it as a new button",
nsIFilePicker.modeOpen);
fp.appendFilters(nsIFilePicker.filterXML);
fp.appendFilter("All Files", "*.*");
fp.displayDirectory = lastDirectory.path;
if (fp.show() == nsIFilePicker.returnOK) {
if (fp.file && fp.file.exists()) {
lastDirectory.path = fp.file.parent.QueryInterface(nsILocalFile);
}
} else {
return;
}
var xmlData = readFile(fp.file);
var xmlDOM = stringToDOM(xmlData);
if (!xmlDOM) {
//Application.console.log(xmlDOM);
custombuttons.alertBox("Import Fail", "Not an XML file!");
return;
}
if ((xmlDOM.localName == "custombutton") &&
((xmlDOM.getAttribute("xmlns:cb") == "http://xsms.nm.ru/custombuttons/") ||
(xmlDOM.getAttribute("xmlns:cb") == "http://xsms.nm.ru/custombuttons") ||
(xmlDOM.getAttribute("xmlns") == "http://xsms.nm.ru/custombuttons/") ||
(xmlDOM.getAttribute("xmlns") == "http://xsms.nm.ru/custombuttons"))) {
importXMLtoButton(xmlData);
} else {
custombuttons.alertBox("Import Fail", "Not a valid Custom Buttons XML!");
}
}
//---------- Start initiating CB contextmenu ----------//
var saveImg = "data:image/x-icon;base64,\
AAABAAEAEBAAAAAAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAQAQAAAAAAAAAAAAAAAAA\
AAAAAAD///8B////Af///wH///8BAAAAHQAAACUXaE1dE55y/xOecv8XaE1dAAAAJQAAAB3///8B\
////Af///wH///8B////Af///wH///8B////Af///wEmrX85F6J2/xHEj/8RxI//GKF2/yatfzn/\
//8B////Af///wH///8B////Af///wH///8B////Af///wEmrX85Hqd6/xHHkv8Rx5L/EceS/xHH\
kv8ep3r/Jq1/Of///wH///8B////Af///wH///8B////Af///wEhs4RJJq1//xHHkv8Rx5L/EceS\
/xHHkv8Rx5L/EciT/yatgP8mrX85////Af///wH///8B////Af///wEjsYJBLLKE/xHNlv8RyJP/\
EciT/xHIk/8RyJP/EciT/xHKlf8Rzpn/LLOE/yatfzn///8B////Af///wEmrX85MbaH/xTcqP8V\
3qv/Fd2q/xHKlf8RypX/EcqV/xHKlf8W4a7/Fd6r/xTZpf8xtof/Jq1/Of///wEmrX1pMbaH/zG3\
iP8xt4j/MbeI/xfWov8RzJj/EcyY/xHMmP8RzJj/H8SR/zG3iP8xt4j/MbeI/zG2h/8mrX85////\
Af///wH///8B////Af///wEuuov/Ec+a/xHPmv8Rz5r/FNCc/xbUoPEisH7v////Af///wH///8B\
////Af///wH///8B////Af///wH///8BLrqL/xHTnv8R057/EdOe/xXUoP8a2KT3H7J/4ymaaSUp\
mmklKZppJSmaaSX///8B////Af///wH///8B////AS66i/8R1aH/EdWh/xHVof8U1qL/Idyp/ySh\
b+0koW/tJKJw6ySlc+0noG/5////Af///wH///8B////Af///wEuuov/Edej/xHXo/8R16P/Edej\
/yzgsP8ZsH//GrOB/xm6hv8ZwI3/JqFw6////wH///8B////Af///wH///8BLrqL/xHapf8R2qX/\
Edql/xHapf895bf/KbCC/yCpef8gsH7/Hb6M/yidbNP///8B////Af///wH///8B////ASPNmv8Y\
3ar/Edyn/xHcp/8V3aj/VerA/1DNpP8moHH/JqJz/yG7ifsvv5L/////Af///wH///8B////Af//\
/wEizJn1b+/J/2nux/9k7cX/b+/J/3Dvyf9r7cb/MJ9x/yype/8jt4XzL7+S/////wH///8B////\
Af///wH///8BFL+KOTjWp/9B6Lv/Oea4/zjmuP9G6Lz/WuzD/1rcs/9B0aT7L7+S/////wH///8B\
////Af///wH///8B////Af///wEXwo5lFMWQzxPFkNUTxZDZE8WQ0xXEkNcmxJPVQsie/////wH/\
//8BAAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA\
//8AAP//AAD//w==";
var cIDs = ["custombuttons-contextpopup-exportXML",
"custombuttons-contextpopup-exportXML-sub"];
var bIDs = ["custombuttons-contextpopup-bookmarkButton",
"custombuttons-contextpopup-bookmarkButton-sub"];
for (var i = 0; i < cIDs.length; i++) {
if ($(cIDs[i])) $(cIDs[i]).parentNode.removeChild($(cIDs[i]));
let item = cbu.makeXML(<menuitem xmlns={xulns}
id={cIDs[i]}
class="menuitem-iconic"
image={saveImg}
label="Export to XML"
oncommand={"document.getElementById('" + this.id +
"').saveXML(document.popupNode.URI);"}/>);
if (i == 0) {
item.setAttribute("observes", "custombuttons-contextbroadcaster-primary");
}
$(bIDs[i]).parentNode.insertBefore(item, $(bIDs[i]).nextSibling);
}
// Remove contextmenu item when this button is deleted
this.onDestroy = function(aReason) {
if (aReason == "delete") {
for (var j = 0; j < cIDs.length; j++) {
$(cIDs[j]) && $(cIDs[j]).parentNode.removeChild($(cIDs[j]));
}
}
}
//---------- End initiating CB contextmenu ----------//
//---------- Remove old traces if any ----------//
let tbitem = $(this.id + "-toolbaritem");
tbitem && tbitem.parentNode.removeChild(tbitem);
var css = "\
@namespace url(http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul);\
#navigator-toolbox:not([customizing=\"true\"]) #" + this.id + ",\
#navigator-toolbox[customizing=\"true\"] #" + this.id + "-toolbaritem\
{ display: none; }";
var sss = Cc["@mozilla.org/content/style-sheet-service;1"].
getService(Ci.nsIStyleSheetService);
var uri = makeURI("data:text/css," + encodeURIComponent(css), null, null);
if (sss.sheetRegistered(uri, sss.USER_SHEET)) {
sss.unregisterSheet(uri, sss.USER_SHEET);
}
////////////////////////////// Button updater //////////////////////////////
this.updateURL = "http://loucypher.googlecode.com/svn/custombuttons/xml/" +
"Custom%20Buttons%20XML%20Exporter-Importer.xml";
this.onclick = function(aEvent) {
if (!aEvent.shiftKey) return;
aEvent.preventDefault();
this.updater.checkForUpdate(this.updater.getUpdate);
}
var btnClick = this.onclick;
var btnIcon = this.image;
var Button = this;
this.updater = {
get bsyIcon() {
return Application.name == "Firefox"
? Application.version >= "4"
? "chrome://browser/skin/tabbrowser/connecting.png"
: "chrome://global/skin/icons/loading_16.png"
: Application.name == "SeaMonkey"
? "chrome://communicator/skin/icons/loading.gif"
: "chrome://custombuttons/skin/button.png";
},
isValidCbURI: function isValidCbURI(aURL) {
if (!aURL) return false;
return /^custombutton\:\/\//.test(aURL);
},
convertURItoDOM: function convertURItoDOM(aURL) {
if (!this.isValidCbURI(aURL)) {
custombuttons.alertBox(Button.name, "Not a Custom Buttons link!");
return;
}
var string = unescape(aURL.replace(/^custombutton\:\/\//, "").toString());
var parser = new DOMParser();
var dom = parser.parseFromString(string, "text/xml");
if (dom.documentElement.nodeName == "parsererror") {
return null;
} else {
return dom.documentElement;
}
},
getParamValue: function getParamValue(aDocument, aNodeName) {
var node = aDocument.getElementsByTagName(aNodeName)[0];
if (!node) return "";
if (!node.firstChild || (node.firstChild &&
(node.firstChild.nodeType == node.TEXT_NODE))) {
return node.textContent;
} else {
return node.firstChild.textContent;
}
},
getButtonParameters: function getButtonParameters(aButtonLink, aURL) {
var dom = this.convertURItoDOM(aURL);
var params = custombuttons.cbService.getButtonParameters(aButtonLink)
.wrappedJSObject;
params.name = this.getParamValue(dom, "name")
params.image = this.getParamValue(dom, "image") ||
this.getParamValue(dom, "stdicon");
params.code = this.getParamValue(dom, "code")
params.initCode = this.getParamValue(dom, "initcode")
params.help = this.getParamValue(dom, "help")
params.accelkey = this.getParamValue(dom, "accelkey")
params.mode = this.getParamValue(dom, "mode")
params.wrappedJSObject = params;
return params;
},
resetAttributes: function resetAttributes() {
Button.image = btnIcon;
Button.tooltipText = Button.name;
Button.removeAttribute("busy");
Button.onclick = btnClick;
},
checkForUpdate: function checkForUpdate(aCallback) {
var url = Button.updateURL + "?" + Date.now();
var req = new XMLHttpRequest();
req.open("GET", url, true);
if (Button.hasAttribute("busy")) {
this.resetAttributes();
return
}
var updater = this;
req.onreadystatechange = function (aEvent) {
Button.onclick = function(aEvent) {
aEvent.preventDefault();
req.abort();
this.updater.resetAttributes();
}
Button.image = updater.bsyIcon;
Button.setAttribute("busy", "");
Button.tooltipText = "Checking for update...\nClick to abort.";
if (req.readyState == 4 && req.status == 200) {
updater.resetAttributes();
aCallback(req.responseXML);
}
}
req.send(null);
},
getUpdate: function getUpdate(aDocument) {
if (aDocument.documentElement.localName != "custombutton") {
alert("Not a valid Custom Buttons XML file!");
return;
}
let button = aDocument.getElementById("button");
let link = button.getElementsByTagNameNS("http://www.w3.org/1999/xhtml",
"a")[0];
if (link.href == Button.URI) {
let as = Cc['@mozilla.org/alerts-service;1'].
getService(Ci.nsIAlertsService);
as.showAlertNotification(btnIcon, "No update found!",
"Finish checking", false, "", null);
return;
}
var install = custombuttons.confirmBox(Button.name, "Update found! " +
"Update this button?",
"Yes", "No");
if (!install) return;
let btnLink = custombuttons.makeButtonLink("update", Button.id);
let params = Button.updater.getButtonParameters(btnLink, link.href);
custombuttons.cbService.installButton(params);
custombuttons.alertBox(Button.name, "Button updated!");
}
}
///////////////////////////// End Button updater ////////////////////////////
this.label = "Import XML File As New Button";
this.tooltipText = this.Help;
To export:
1. Right click on any custom buttons
2. Select 'Export to XML'
To import:
1. Click this button
2. Select a Custom Buttons XML file
or just open any Custom Buttons XML file to import.
Shift+click: Find update for this button.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment