From 07af7cd4490c16a72cd9155758d6e2dfc7a19efc Mon Sep 17 00:00:00 2001 From: Andrew Lisowski Date: Sat, 28 Apr 2018 19:58:12 -0700 Subject: [PATCH 1/3] allow user to enable on a domain. enables github enterprise --- package.json | 5 +- src/background.js | 6 +- src/manifest.json | 10 +- src/vendor/webext-domain-permission-toggle.js | 49 ++++++++ src/vendor/webext-dynamic-content-scripts.js | 113 ++++++++++++++++++ 5 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 src/vendor/webext-domain-permission-toggle.js create mode 100644 src/vendor/webext-dynamic-content-scripts.js diff --git a/package.json b/package.json index bca73c5..04699c0 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,10 @@ "version": "1.0.0", "description": "Show github contributor's stats (# of Issues/PRs)", "main": "index.js", - "dependencies": {}, + "dependencies": { + "webext-domain-permission-toggle": "0.0.2", + "webext-dynamic-content-scripts": "^5.0.1" + }, "devDependencies": { "chrome-webstore-upload-cli": "^1.0.3" }, diff --git a/src/background.js b/src/background.js index 0286869..df55384 100644 --- a/src/background.js +++ b/src/background.js @@ -1,4 +1,8 @@ -/* global chrome */ +/* global chrome DCE DCS */ + +// GitHub Enterprise support +DCE.addContextMenu(); +DCS.addToFutureTabs(); // launch options page on first run chrome.runtime.onInstalled.addListener((details) => { diff --git a/src/manifest.json b/src/manifest.json index 6e200ac..b2a2e4f 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -11,11 +11,19 @@ "permissions": [ "https://github.com/*/*", "storage", - "identity" + "identity", + "contextMenus", + "activeTab" + ], + "optional_permissions": [ + "http://*/*", + "https://*/*" ], "background": { "persistent": false, "scripts": [ + "vendor/webext-domain-permission-toggle.js", + "vendor/webext-dynamic-content-scripts.js", "background.js" ] }, diff --git a/src/vendor/webext-domain-permission-toggle.js b/src/vendor/webext-domain-permission-toggle.js new file mode 100644 index 0000000..bea7d07 --- /dev/null +++ b/src/vendor/webext-domain-permission-toggle.js @@ -0,0 +1,49 @@ +// https://github.com/bfred-it/webext-domain-permission-toggle + +const DCE = { + addContextMenu(options) { + const contextMenuId = 'webext-domain-permission-toggle:add-permission'; + + const {name: extensionName} = chrome.runtime.getManifest(); + + options = Object.assign({ + title: `Enable ${extensionName} on this domain`, + reloadOnSuccess: `Do you want to reload this page to apply ${extensionName}?` + }, options); + + // This has to happen onInstalled in Event Pages + chrome.runtime.onInstalled.addListener(() => { + chrome.contextMenus.create({ + id: contextMenuId, + title: options.title, + contexts: ['page_action', 'browser_action'], + documentUrlPatterns: [ + 'http://*/*', + 'https://*/*' + ] + }); + }); + + chrome.contextMenus.onClicked.addListener(async ({menuItemId}, {tabId, url}) => { + if (menuItemId !== contextMenuId) { + return; + } + chrome.permissions.request({ + origins: [ + `${new URL(url).origin}/*` + ] + }, granted => { + if (chrome.runtime.lastError) { + console.error(`Error: ${chrome.runtime.lastError.message}`); + alert(`Error: ${chrome.runtime.lastError.message}`); + } else if (granted && options.reloadOnSuccess && confirm(options.reloadOnSuccess)) { + chrome.tabs.reload(tabId); + } + }); + }); + } +}; + +if (typeof module === 'object') { + module.exports = DCE; +} diff --git a/src/vendor/webext-dynamic-content-scripts.js b/src/vendor/webext-dynamic-content-scripts.js new file mode 100644 index 0000000..62b748f --- /dev/null +++ b/src/vendor/webext-dynamic-content-scripts.js @@ -0,0 +1,113 @@ +// https://github.com/bfred-it/webext-dynamic-content-scripts +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (factory((global.DCS = global.DCS || {}))); +}(this, (function (exports) { 'use strict'; + +function interopDefault(ex) { + return ex && typeof ex === 'object' && 'default' in ex ? ex['default'] : ex; +} + +function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; +} + +var webextContentScriptPing = createCommonjsModule(function (module) { +// https://github.com/bfred-it/webext-content-script-ping + +/** + * Ping responder + */ +document.__webextContentScriptLoaded = true; + +/** + * Pinger + */ +function pingContentScript(tab) { + return new Promise((resolve, reject) => { + chrome.tabs.executeScript(tab.id || tab, { + code: 'document.__webextContentScriptLoaded', + runAt: 'document_start' + }, hasScriptAlready => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError); + } else { + resolve(Boolean(hasScriptAlready[0])); + } + }); + }); +} + +if (typeof module === 'object') { + module.exports = pingContentScript; +} +}); + +var pingContentScript = interopDefault(webextContentScriptPing); + +async function p(fn, ...args) { + return new Promise((resolve, reject) => fn(...args, r => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError); + } else { + resolve(r); + } + })); +} + +async function addToTab(tab, contentScripts) { + if (typeof tab !== 'object' && typeof tab !== 'number') { + throw new TypeError('Specify a Tab or tabId'); + } + + if (!contentScripts) { + // Get all scripts from manifest.json + contentScripts = chrome.runtime.getManifest().content_scripts; + } else if (!Array.isArray(contentScripts)) { + // Single script object, make it an array + contentScripts = [contentScripts]; + } + + try { + const tabId = tab.id || tab; + if (!await pingContentScript(tabId)) { + const injections = []; + for (const group of contentScripts) { + const allFrames = group.all_frames; + const runAt = group.run_at; + for (const file of group.css) { + injections.push(p(chrome.tabs.insertCSS, tabId, {file, allFrames, runAt})); + } + for (const file of group.js) { + injections.push(p(chrome.tabs.executeScript, tabId, {file, allFrames, runAt})); + } + } + return Promise.all(injections); + } + } catch (err) { + // Probably the domain isn't permitted. + // It's easier to catch this than do 2 queries + } +} + +function addToFutureTabs(scripts) { + chrome.tabs.onUpdated.addListener((tabId, {status}) => { + if (status === 'loading') { + addToTab(tabId, scripts); + } + }); +} + +var index = { + addToTab, + addToFutureTabs +}; + +exports.addToTab = addToTab; +exports.addToFutureTabs = addToFutureTabs; +exports['default'] = index; + +Object.defineProperty(exports, '__esModule', { value: true }); + +}))); From b3c196abf6721ee2a228a4bf8789ce5418842c73 Mon Sep 17 00:00:00 2001 From: Andrew Lisowski Date: Sat, 28 Apr 2018 20:22:09 -0700 Subject: [PATCH 2/3] have to use the right api on enterprise urls --- package-lock.json | 26 ++++++++++++++++++++++++++ src/content.js | 7 +++++-- 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..509cbbf --- /dev/null +++ b/package-lock.json @@ -0,0 +1,26 @@ +{ + "name": "github-contributor-extension", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "webext-content-script-ping": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/webext-content-script-ping/-/webext-content-script-ping-2.0.1.tgz", + "integrity": "sha512-37oaeT0GwwkIC8JUD5aToOr0ncdoml/kjZdsn5ckyKyZqpNQHYxhP3NhnqsSx25LpxGfJm8er/IPv9pzCLcfUA==" + }, + "webext-domain-permission-toggle": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/webext-domain-permission-toggle/-/webext-domain-permission-toggle-0.0.2.tgz", + "integrity": "sha512-obPAEocq/5uw0jaKc1N2bwux/HXDaYz9/8ltvxTRqr0iR6JaTh6/h56FL9uoAZYXcXxjFyE+PRrJicoQrJ8T6g==" + }, + "webext-dynamic-content-scripts": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/webext-dynamic-content-scripts/-/webext-dynamic-content-scripts-5.0.1.tgz", + "integrity": "sha512-R0TPOV9B5hDnSlOTaxxEkuX3L8rm+8KQSWpdxAqE+yYv/YqaDXip+8M39v00jXGTkpz36YdAnGjRdnzHw7taOw==", + "requires": { + "webext-content-script-ping": "2.0.1" + } + } + } +} diff --git a/src/content.js b/src/content.js index 77390fd..5d4f223 100644 --- a/src/content.js +++ b/src/content.js @@ -8,6 +8,9 @@ const getCurrentUser = () => $(".js-menu-target img").attr("alt").slice(1) || "" const isPrivate = () => $(".label-private").length > 0; let statsScope = "repo"; +const githubURLBase = window.location.hostname === 'github.com' ? 'api.github.com' : `${window.location.hostname}/api/v3`; +const githubURL = `https://${githubURLBase}` + function getContributor() { let $contributor = $(".timeline-comment-wrapper .timeline-comment-header-text strong a"); if ($contributor.length) { @@ -71,10 +74,10 @@ function contributorCount({access_token, contributor, user, repoPath, old = {}, repo = undefined; repoPath = "__self"; } - + let searchURL = buildUrl({ access_token, - base: "https://api.github.com/search/issues", + base: `${githubURL}/search/issues`, order: "asc", per_page: "1", q: { From 807d847b61b44f1970bc2a7f37adcbf41c3eb72f Mon Sep 17 00:00:00 2001 From: Andrew Lisowski Date: Sat, 28 Apr 2018 20:31:36 -0700 Subject: [PATCH 3/3] need to have seperate field for enterprise token. might want to expand this in the future to allow for an access token for each domain --- src/content.js | 15 ++++++++++----- src/options.html | 2 ++ src/options.js | 13 ++++++++++--- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/content.js b/src/content.js index 5d4f223..bc1c9ce 100644 --- a/src/content.js +++ b/src/content.js @@ -8,8 +8,9 @@ const getCurrentUser = () => $(".js-menu-target img").attr("alt").slice(1) || "" const isPrivate = () => $(".label-private").length > 0; let statsScope = "repo"; -const githubURLBase = window.location.hostname === 'github.com' ? 'api.github.com' : `${window.location.hostname}/api/v3`; -const githubURL = `https://${githubURLBase}` +const isEnterprise = window.location.hostname !== 'github.com'; +const githubURLBase = isEnterprise ? `${window.location.hostname}/api/v3` : 'api.github.com'; +const githubURL = `https://${githubURLBase}`; function getContributor() { let $contributor = $(".timeline-comment-wrapper .timeline-comment-header-text strong a"); @@ -333,11 +334,15 @@ function update({ contributor, repoPath, currentNum, user }) { if (storageRes.prs || storageRes.issues) { updateTextNodes(appendPRText(currentNum, storageRes)); } else { - getSyncStorage({ "access_token": null }) + const token = isEnterprise ? "enterprise_access_token" : "access_token"; + + getSyncStorage({ [token]: null }) .then((res) => { + const access_token = isEnterprise ? res.enterprise_access_token : res.access_token; + Promise.all([ - contributorCount({ old: storageRes, user, access_token: res.access_token, type: "pr", contributor, repoPath}), - contributorCount({ old: storageRes, user, access_token: res.access_token, type: "issue", contributor, repoPath}) + contributorCount({ old: storageRes, user, access_token, type: "pr", contributor, repoPath}), + contributorCount({ old: storageRes, user, access_token, type: "issue", contributor, repoPath}) ]) .then(([prInfo, issueInfo]) => { let repoInfo = Object.assign(prInfo, issueInfo); diff --git a/src/options.html b/src/options.html index 8b6d697..839f2b9 100644 --- a/src/options.html +++ b/src/options.html @@ -42,6 +42,8 @@
Github's Search API Rate Limit

Access Token

+

Enterprise Access Token

+

diff --git a/src/options.js b/src/options.js index 43ec1cf..8368312 100644 --- a/src/options.js +++ b/src/options.js @@ -2,13 +2,15 @@ document.addEventListener("DOMContentLoaded", () => { const accessTokenInput = document.getElementById("token-input"); + const enterpriseAccessTokenInput = document.getElementById("enterprise-token-input"); const oauthLink = document.getElementById("use-oauth"); const clearCacheLink = document.getElementById("clear-cache"); const showPrivateReposInput = document.getElementById("show-private-repos"); - getSyncStorage({ "access_token": null, "_showPrivateRepos": null }) - .then(({ access_token, _showPrivateRepos }) => { + getSyncStorage({ "access_token": null, "enterprise_access_token": null, "_showPrivateRepos": null }) + .then(({ access_token, enterprise_access_token, _showPrivateRepos }) => { if (access_token) accessTokenInput.value = access_token; + if (enterprise_access_token) enterpriseAccessTokenInput.value = enterprise_access_token; if (_showPrivateRepos) showPrivateReposInput.checked = _showPrivateRepos; }); @@ -16,14 +18,19 @@ document.addEventListener("DOMContentLoaded", () => { setSyncStorage({ "access_token": accessTokenInput.value }); }); + enterpriseAccessTokenInput.addEventListener("change", () => { + setSyncStorage({ "enterprise_access_token": enterpriseAccessTokenInput.value }); + }); + oauthLink.addEventListener("click", () => { getTokenFromOauth(); }); clearCacheLink.addEventListener("click", () => { let temp = accessTokenInput.value; + let temp2 = enterpriseAccessTokenInput.value; chrome.storage.sync.clear(() => { - setSyncStorage({ "access_token": temp }); + setSyncStorage({ "access_token": temp, "enterprise_access_token": temp2 }); document.querySelector("#feedback").textContent = "Storage Cleared"; }); });