diff --git a/.jsbeautifyrc b/.jsbeautifyrc new file mode 100644 index 0000000..b1a5718 --- /dev/null +++ b/.jsbeautifyrc @@ -0,0 +1,22 @@ +{ + "indent_size": 4, + "indent_char": " ", + "eol": "\n", + "indent_level": 0, + "indent_with_tabs": true, + "preserve_newlines": true, + "max_preserve_newlines": 10, + "jslint_happy": false, + "space_after_anon_function": false, + "brace_style": "collapse", + "keep_array_indentation": false, + "keep_function_indentation": false, + "space_before_conditional": false, + "break_chained_methods": false, + "eval_code": false, + "unescape_strings": false, + "wrap_line_length": 0, + "wrap_attributes": "auto", + "wrap_attributes_indent_size": 4, + "end_with_newline": false +} diff --git a/README.md b/README.md index 90a02eb..26773ab 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ GitHub Extension Installer - Mozilla Extension This Add-On allows you to install browser extensions straight from GitHub Repositories, the only requirement in the current version is that the extension files are placed in the root of the repo. If that is the case (ie, a install.rdf file is found there), a new button will be added in the right-bottom side of the page as shown in the next screenshot. -![SS](https://addons.cdn.mozilla.net/img/uploads/previews/full/113/113974.png?modified=1380155370) +![GitHub Extension Installer Screenshot](https://addons.cdn.mozilla.net/user-media/previews/full/113/113974.png) **Please note the installation is performed in the background, there won't be a confirmation dialog**, you'll just get a notification indicating whether the installation succeed or failed. diff --git a/bootstrap.js b/bootstrap.js index 3fa21dc..a9404be 100644 --- a/bootstrap.js +++ b/bootstrap.js @@ -6,10 +6,18 @@ * * Contributor(s): * Diego Casorran (Original Author) + * Jerone + * Zulkarnain K. + * Noitidart (Authored Contribution: "Install from Edit Page without Committing") * * ***** END LICENSE BLOCK ***** */ -let {classes:Cc,interfaces:Ci,utils:Cu,results:Cr} = Components, addon; +let { + classes: Cc, + interfaces: Ci, + utils: Cu, + results: Cr +} = Components, addon; Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource://gre/modules/Services.jsm"); @@ -17,9 +25,8 @@ Cu.import("resource://gre/modules/FileUtils.jsm"); Cu.import("resource://gre/modules/AddonManager.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -function LOG(m) (m = addon.name + ' Message @ ' - + (new Date()).toISOString() + "\n> " + m, - dump(m + "\n"), Services.console.logStringMessage(m)); +function LOG(m)(m = addon.name + ' Message @ ' + (new Date()).toISOString() + "\n> " + m, + dump(m + "\n"), Services.console.logStringMessage(m)); let i$ = { onOpenWindow: function(aWindow) { @@ -47,7 +54,10 @@ function iNotify(aAddon, aMsg, callback) { if(nme == 3) try { if(aAddon) { let info = { - installs: [{addon:aAddon,name:aAddon.name + ' ' + aAddon.version}], + installs: [{ + addon: aAddon, + name: aAddon.name + ' ' + aAddon.version + }], originatingWindow: Services.wm.getMostRecentWindow('navigator:browser').gBrowser.contentWindow, QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallInfo]) }; @@ -57,10 +67,9 @@ function iNotify(aAddon, aMsg, callback) { } catch(e) { Cu.reportError(e); } - showAlertNotification(addon.icon,addon.name,aMsg,!1,"", - (s,t) => t == "alertshow" || callback(t)); + showAlertNotification(addon.icon, addon.name, aMsg, !1, "", (s, t) => t == "alertshow" || callback(t)); } else { - if(nme) Services.prompt.alert(null,addon.name,aMsg); + if(nme) Services.prompt.alert(null, addon.name, aMsg); callback(); } @@ -70,48 +79,58 @@ function onClickHanlder(ev) { ev.preventDefault(); if(this.hasAttribute(addon.tag)) { - Services.prompt.alert(null,addon.name, + Services.prompt.alert(null, addon.name, "Don't click me more than once, reload the page to retry."); return; } - this.setAttribute(addon.tag,1); + if(this.classList.contains('disabled')) { //needed for when checking if editable file is installable + return; + } + + this.setAttribute(addon.tag, 1); this.className += ' danger disabled'; let d = this.ownerDocument, l = this.lastChild, f = this.firstChild; l.textContent = ' Installing...'; - f.className = f.className.replace('plus','hourglass'); - d.body.appendChild(d.createElement('style')).textContent = '@keyframes ' - +addon.tag+'{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}'; + f.className = f.className.replace(/(?:plus|check)/, 'hourglass'); + d.body.appendChild(d.createElement('style')).textContent = '@keyframes ' + addon.tag + '{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}'; f.style.animation = addon.tag + ' 3s infinite linear'; - xhr(this.href || this.getAttribute('href'),data => { + xhr(this.href || this.getAttribute('href'), data => { let iStream = Cc["@mozilla.org/io/arraybuffer-input-stream;1"] .createInstance(Ci.nsIArrayBufferInputStream); - iStream.setData(data,0,data.byteLength); + iStream.setData(data, 0, data.byteLength); let nFile = FileUtils.getFile("TmpD", [Math.random()]) - oStream = FileUtils.openSafeFileOutputStream(nFile); + oStream = FileUtils.openSafeFileOutputStream(nFile); NetUtil.asyncCopy(iStream, oStream, aStatus => { if(!Components.isSuccessCode(aStatus)) { - Services.prompt.alert(null,addon.name, - 'Error ' +aStatus+ ' writing to ' +nFile.path); + Services.prompt.alert(null, addon.name, + 'Error ' + aStatus + ' writing to ' + nFile.path); } else { let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"] - .createInstance(Ci.nsIZipReader), + .createInstance(Ci.nsIZipReader), zipWriter = Cc["@mozilla.org/zipwriter;1"] - .createInstance(Ci.nsIZipWriter); + .createInstance(Ci.nsIZipWriter); + + let oFile = FileUtils.getFile("TmpD", [addon.tag + '.xpi']); - let oFile = FileUtils.getFile("TmpD", [addon.tag+'.xpi']); zipReader.open(nFile); zipWriter.open(oFile, 0x2c); let p = (this.getAttribute('path') || "*/"), m = zipReader.findEntries(p + "*"); p = p.substr(2); + + if(this.hasAttribute('filepath')) { + var fileName = this.getAttribute('filepath'); + var useUncommitedFilePath = this.getAttribute('filepath').replace(this.getAttribute('path'), ''); //relative to path, because thats what is getting written to xpi + } + while(m.hasMore()) { let f = m.getNext(), e = zipReader.getEntry(f); @@ -119,47 +138,61 @@ function onClickHanlder(ev) { if(!(e instanceof Ci.nsIZipEntry)) continue; - let n = (e.name||f).replace(/^[^\/]+\//,'').replace(p,''); + let n = (e.name || f).replace(/^[^\/]+\//, '').replace(p, ''); if(!n) continue; if(e.isDirectory) { - zipWriter.addEntryDirectory(n,e.lastModifiedTime,!1); + zipWriter.addEntryDirectory(n, e.lastModifiedTime, !1); } else { - - zipWriter.addEntryStream(n, e.lastModifiedTime, - Ci.nsIZipWriter.COMPRESSION_FASTEST, - zipReader.getInputStream(f), !1); + if(useUncommitedFilePath && n == useUncommitedFilePath) { + let is = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream); + if (this.ownerDocument.querySelector('#blob_contents')) { + is.data = this.ownerDocument.querySelector('#blob_contents').value; + } else { + is.data = this.ownerDocument.querySelector('.js-blob-contents').textContent; + } + zipWriter.addEntryStream(n, Date.now(), Ci.nsIZipWriter.COMPRESSION_FASTEST, is, !1); + } else { + zipWriter.addEntryStream(n, e.lastModifiedTime, + Ci.nsIZipWriter.COMPRESSION_FASTEST, + zipReader.getInputStream(f), !1); + } } } zipReader.close(); zipWriter.close(); - AddonManager.getInstallForFile(oFile,aInstall => { - let done = (aMsg,aAddon) => { + AddonManager.getInstallForFile(oFile, aInstall => { + let done = (aMsg, aAddon) => { let c = 'check'; if(typeof aMsg === 'number') { l.textContent = 'Error ' + aMsg; - aMsg = 'Installation failed ('+aMsg+')'; + aMsg = 'Installation failed (' + aMsg + ')'; c = 'alert'; } else { - l.textContent = 'Succeed!'; - this.className = this.className.replace('danger',''); + if(!this.hasAttribute('filepath')) { + l.textContent = 'Succeed!'; + this.className = this.className.replace('danger', ''); + } else { + //it is uncommited file install so allow reclicking of button + l.textContent = 'Installed with Uncommitted File - Reinstall'; + this.classList.remove('danger'); + this.classList.remove('disabled'); + this.removeAttribute(addon.tag); //so allows reinstall + } } f.style.animation = null; - f.className = f.className.replace('hourglass',c); + f.className = f.className.replace('hourglass', c); iNotify(aAddon, aMsg, aResult => { oFile.remove(!1); - if(aResult !== null && aAddon && aAddon.pendingOperations) { - let m = aAddon.name + ' requires restart.\n\n' - + 'Would you like to restart ' - + Services.appinfo.name + ' now?'; + let m = aAddon.name + ' requires restart.\n\n' + 'Would you like to restart ' + Services.appinfo.name + ' now?'; m = Services.prompt.confirmEx(null, - addon.name,m,1027,0,0,0,null,{}); + addon.name, m, 1027, 0, 0, 0, null, {}); if(!m) { let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"] @@ -183,31 +216,31 @@ function onClickHanlder(ev) { }; aInstall.addListener({ - onInstallFailed : function(aInstall) { + onInstallFailed: function(aInstall) { aInstall.removeListener(this); done(aInstall.error); }, - onInstallEnded : function(aInstall,aAddon) { + onInstallEnded: function(aInstall, aAddon) { aInstall.removeListener(this); - done(aAddon.name + ' ' + aAddon.version - + ' has been installed successfully.',aAddon); + done(aAddon.name + ' ' + aAddon.version + ' has been installed successfully.', aAddon); } }); aInstall.install(); }); nFile.remove(!1); + } }); }); } -function addButton(n,u) { - if([n.nextElementSibling,n.previousElementSibling] - .some(e=>e&&~e.className.indexOf(addon.tag))) - return; +function addButton(n, u) { + if([n.nextElementSibling, n.previousElementSibling] + .some(e => e && ~e.className.indexOf(addon.tag))) + return; let p = n.parentNode; n = n.cloneNode(!0); @@ -229,26 +262,30 @@ function addButton(n,u) { if(u) { let b = n.ownerDocument.querySelector('div.breadcrumb'); - n.setAttribute('href', u ); + n.setAttribute('href', u); n.style.cursor = 'pointer'; - n.style.setProperty('box-shadow','none','important'); + n.style.setProperty('box-shadow', 'none', 'important'); n.setAttribute('path', b && b.textContent - .replace(" ",'','g').replace(/^[^/]+/,'*')||''); + .replace(" ", '', 'g').replace(/^[^/]+/, '*') || ''); if(typeof u !== 'object') { n.className += ' button primary pseudo-class-active'; } else { - n.className = 'minibutton pseudo-class-active'; + n.className = 'btn btn-sm minibutton pseudo-class-active'; n.firstChild.style.verticalAlign = 'baseline'; } } + + return n; } function onPageLoad(doc) { - if(doc.location.pathname.replace(/\/[^/]+$/,'').substr(-4) === 'pull') { + + var myElseIfVar; + if(doc.location.pathname.split('/')[3] === 'pull') { // Based on work by Jerone: https://github.com/jerone/UserScripts - let r = '' + doc.location.pathname.split('/').filter(String).slice(1,2), + let r = '' + doc.location.pathname.split('/').filter(String).slice(1, 2), v = addon.branch.getPrefType('prs') && addon.branch.getCharPref('prs') || ''; if(~v.toLowerCase().split(',').indexOf(r.toLowerCase())) { @@ -262,14 +299,13 @@ function onPageLoad(doc) { 'archive', b.join(':') + '.zip' ].join('/'); - addButton(n,u); + addButton(n, u); } - } - else if([].some.call(doc.querySelectorAll('table.files > tbody > tr > td.content'), - (n) => 'install.rdf' === n.textContent.trim())) { - - let c = 7, n, z; - while(c-- && !(n=doc.querySelector('a.minibutton:nth-child('+c+')'))); + } else if([].some.call(doc.querySelectorAll('table.files > tbody > tr > td.content'), (n) => 'install.rdf' === n.textContent.trim())) { + let c = 7, + n, z; + n = doc.querySelector('a.sidebar-button[href*=".zip"]'); + while(c-- && !(n = doc.querySelector('a.minibutton:nth-child(' + c + '),a.btn.btn-sm:nth-child(' + c + ')'))); if(n && n.textContent.trim() === 'Download ZIP') { c = doc.querySelector('div.only-with-full-nav'); @@ -286,9 +322,110 @@ function onPageLoad(doc) { n = doc.querySelector('div.file-navigation'); n = n && n.firstElementChild; - if( n ) { + if(n) { + addButton(n, z); + } + } + } else if(/github\.com\/.*?\/.*?\/edit\//.test(doc.location.href) && (myElseIfVar = doc.querySelector('.js-blob-form')) && myElseIfVar.hasAttribute('action')) { + var editForm = myElseIfVar; + var filePath = editForm.getAttribute('action'); + let c = 7, + n, z; + while(c-- && !(n = doc.querySelector('a.minibutton:nth-child(' + c + '),a.btn.btn-sm:nth-child(' + c + ')'))); + + if(n && n.textContent.trim() === 'Download ZIP') { + c = doc.querySelector('div.only-with-full-nav'); + z = n; + n = 0; + } + + if(!n) { + n = doc.querySelector('div.breadcrumb'); + n = n && n.firstElementChild; + + if(n) { + var btn = addButton(n, z); + btn.className += ' danger disabled' + var l = btn.lastChild; + var f = btn.firstChild; + l.textContent = ' Checking if Installable...'; + f.className = f.className.replace('plus', 'hourglass'); + + var breadcrumbs = doc.querySelectorAll('span[itemtype*=Breadcrumb]'); + var breads = ['*']; + for(var i = 1; i < breadcrumbs.length - 1; i++) { //start at i=1 because not possible to have */install.rdf or any files */ because zips off of github first hold a folder + breads.push(breadcrumbs[i].textContent); + } + + var lookFor = []; //array holding dir paths to look for install.rdf at. for in the zip + for(var i = 0; i < breads.length; i++) { + var thisLookFor = breads.slice(0, i + 1).join('/') + '/'; + lookFor.push(thisLookFor); + } + lookFor.reverse(); + + btn.setAttribute('path', breads.join('/') + '/'); + + var filenameEl = (doc.querySelector('input.filename') || doc.querySelector('.js-blob-filename')); + var filename = filenameEl.getAttribute('value'); //dont use .value here otherwise it gets renamed + breads.push(filename); + + btn.setAttribute('filepath', breads.join('/')); + + //////////////////////////////// + xhr(btn.href || btn.getAttribute('href'), data => { + let iStream = Cc["@mozilla.org/io/arraybuffer-input-stream;1"] + .createInstance(Ci.nsIArrayBufferInputStream); + + iStream.setData(data, 0, data.byteLength); + + let nFile = FileUtils.getFile("TmpD", [Math.random()]) + oStream = FileUtils.openSafeFileOutputStream(nFile); + + NetUtil.asyncCopy(iStream, oStream, aStatus => { + if(!Components.isSuccessCode(aStatus)) { + Services.prompt.alert(null, addon.name, + 'Error while checking if installable error was ' + aStatus + ' writing to ' + nFile.path); + } else { + let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"] + .createInstance(Ci.nsIZipReader); + + let oFile = FileUtils.getFile("TmpD", [addon.tag + '.xpi']); + + zipReader.open(nFile); + + var entries = zipReader.findEntries(null); + while(entries.hasMore()) { + let entryFileName = entries.getNext(); + let entryZipFile = zipReader.getEntry(entryFileName); + } + + for(var i = 0; i < lookFor.length; i++) { + var entries = zipReader.findEntries(lookFor[i] + 'install.rdf'); + if(entries.hasMore()) { + let entryFileName = entries.getNext(); + let entryZipFile = zipReader.getEntry(entryFileName); + btn.setAttribute('path', lookFor[i]); + btn.classList.remove('danger'); + btn.classList.remove('disabled'); + l.textContent = ' Install with Uncommitted File'; + f.className = f.className.replace('hourglass', 'plus'); + break; + } else { + if(i == lookFor.length - 1) { + btn.parentNode.removeChild(btn); + } + } + } + + zipReader.close(); + nFile.remove(!1); + } + }); + }); + ////////////////// + - addButton(n,z); } } } @@ -297,14 +434,13 @@ function onPageLoad(doc) { function loadIntoWindow(window) { if(window.document.documentElement .getAttribute("windowtype") != 'navigator:browser') - return; + return; - function onMutation(ms,doc) { + function onMutation(ms, doc) { for(let m of ms) { if('class' == m.attributeName) { - if(~m.oldValue.indexOf('loading') - || m.oldValue === 'context-loader') { - window.setTimeout(onPageLoad.bind(null,doc),820); + if(~m.oldValue.indexOf('loading') || m.oldValue === 'context-loader') { + window.setTimeout(onPageLoad.bind(null, doc), 820); } break; } @@ -317,27 +453,30 @@ function loadIntoWindow(window) { if(!(doc.location && doc.location.host == 'github.com')) return; - ['page-context-loader','context-loader'].forEach(e => { + ['page-context-loader', 'context-loader'].forEach(e => { e = doc.getElementsByClassName(e); for(let o of e) { - new doc.defaultView.MutationObserver(m => onMutation(m,doc)) - .observe(o,{attributes:!0,attributeOldValue:!0}); + new doc.defaultView.MutationObserver(m => onMutation(m, doc)) + .observe(o, { + attributes: !0, + attributeOldValue: !0 + }); } }); onPageLoad(doc); }; getBrowser(window).addEventListener('DOMContentLoaded', domload, false); - addon.wms.set(window,domload); + addon.wms.set(window, domload); } -function xhr(url,cb) { +function xhr(url, cb) { let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] .createInstance(Ci.nsIXMLHttpRequest); let handler = ev => { - evf(m => xhr.removeEventListener(m,handler,!1)); + evf(m => xhr.removeEventListener(m, handler, !1)); switch(ev.type) { case 'load': if(xhr.status == 200) { @@ -345,22 +484,19 @@ function xhr(url,cb) { break; } default: - Services.prompt.alert(null,addon.name, - 'Error Fetching Package: '+ xhr.statusText - + ' ['+ev.type+':' + xhr.status + ']'); + Services.prompt.alert(null, addon.name, + 'Error Fetching Package: ' + xhr.statusText + ' [' + ev.type + ':' + xhr.status + ']'); break; } }; - let evf = f => ['load','error','abort'].forEach(f); - evf(m => xhr.addEventListener( m, handler, false)); + let evf = f => ['load', 'error', 'abort'].forEach(f); + evf(m => xhr.addEventListener(m, handler, false)); xhr.mozBackgroundRequest = true; xhr.open('GET', url, true); xhr.channel.loadFlags |= - Ci.nsIRequest.LOAD_ANONYMOUS - | Ci.nsIRequest.LOAD_BYPASS_CACHE - | Ci.nsIRequest.INHIBIT_PERSISTENT_CACHING; + Ci.nsIRequest.LOAD_ANONYMOUS | Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_PERSISTENT_CACHING; xhr.responseType = "arraybuffer"; xhr.send(null); } @@ -398,22 +534,22 @@ function unloadFromWindow(window) { } function startup(data) { - AddonManager.getAddonByID(data.id,data=> { + AddonManager.getAddonByID(data.id, data => { addon = { id: data.id, name: data.name, version: data.version, icon: data.getResourceURI("icon.png").spec, - tag: data.name.toLowerCase().replace(/[^\w]/g,''), + tag: data.name.toLowerCase().replace(/[^\w]/g, ''), wms: new WeakMap() }; - addon.branch = Services.prefs.getBranch('extensions.'+addon.tag+'.'); + addon.branch = Services.prefs.getBranch('extensions.' + addon.tag + '.'); i$.wmf(loadIntoWindowStub); Services.wm.addListener(i$); if(!addon.branch.getPrefType('nme')) { - addon.branch.setIntPref('nme',2); + addon.branch.setIntPref('nme', 2); } addon.branch.setCharPref('version', addon.version); }); @@ -428,4 +564,5 @@ function shutdown(data, reason) { } function install(data, reason) {} + function uninstall(data, reason) {} diff --git a/install.rdf b/install.rdf index 8ec88f5..c77cf99 100644 --- a/install.rdf +++ b/install.rdf @@ -4,18 +4,19 @@ {86054B0A-BD85-42F9-8E58-8794EC6F6EA1} 2 GitHub Extension Installer - 1.5a2 + 1.6+0001 Diego Casorran <dcasorran@gmail.com> Install Browser Extensions straight from GitHub Repositories Jerone Zulkarnain K. + Noitidart <noitidart@gmail.com> true {ec8030f7-c20a-464f-9b0e-13a3a9e97384} 22.0 - 27.* + 31.* @@ -43,4 +44,4 @@ - \ No newline at end of file + diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..ce9d3d2 Binary files /dev/null and b/screenshot.png differ