diff --git a/gitbeaker b/gitbeaker index b57b8b5..b3ecb93 160000 --- a/gitbeaker +++ b/gitbeaker @@ -1 +1 @@ -Subproject commit b57b8b5e43349bfa5103348c0bda40c40aa0a439 +Subproject commit b3ecb9321b277cccd689bdb44eda997b569b64d1 diff --git a/source/Features.ts b/source/Features.ts index f2659dd..2ee715b 100644 --- a/source/Features.ts +++ b/source/Features.ts @@ -1,19 +1,29 @@ import domLoaded from "dom-loaded"; + +import { pageLoaded } from "./utils/pageLoaded"; // eslint-disable-next-line import/no-cycle import { Config, getConfig } from "./utils/config"; +// eslint-disable-next-line import/no-cycle +import { needsToWaitForApi } from "./utils/needsToWaitForApi"; +// eslint-disable-next-line import/no-cycle +import { apiLoaded } from "./utils/apiLoaded"; -export type Feature = (config: Config) => any; +export type FeatureProps = Config & {}; + +export type Feature = (props: FeatureProps) => any; export interface FeatureDescription { id: string; feature: Feature; waitForDomLoaded?: boolean; + waitForPageLoaded?: boolean; + needsApi?: boolean; } class Features { private __addedFeatures: FeatureDescription[] = []; - add(ctx: FeatureDescription) { + add(ctx: FeatureDescription): void { this.__addedFeatures.push(ctx); } @@ -24,21 +34,37 @@ class Features { loadAll(): void { const config = getConfig(); - for (const { id, feature, waitForDomLoaded } of this.getAll()) { + for (const { id, feature, waitForDomLoaded, waitForPageLoaded, needsApi } of this.getAll()) { if (config.features[id] === false) { console.log(`⏭ skipping feature because it's disabled in config, id: \`${id}\``); continue; } try { + const featureProps: FeatureProps = { ...config }; + + const requirements: Promise[] = []; + if (waitForDomLoaded) { - (async () => { - await domLoaded; - feature(config); + requirements.push(domLoaded); + } + + if (waitForPageLoaded) { + requirements.push(pageLoaded); + } + + if (needsApi && needsToWaitForApi()) { + requirements.push(apiLoaded); + } + + if (requirements.length > 0) { + (async (): Promise => { + await Promise.all(requirements); + feature(featureProps); console.log(`✅ (⏱) feature loaded (after dom loaded), id: \`${id}\``); })(); } else { - feature(config); + feature(featureProps); console.log(`✅ feature loaded (instantly), id: \`${id}\``); } } catch (e) { diff --git a/source/background.ts b/source/background.ts deleted file mode 100644 index fb2d93d..0000000 --- a/source/background.ts +++ /dev/null @@ -1 +0,0 @@ -import "./options-storage"; diff --git a/source/features/add-custom-label-pickers.tsx b/source/features/add-custom-label-pickers.tsx index c4974e0..9e37aa0 100644 --- a/source/features/add-custom-label-pickers.tsx +++ b/source/features/add-custom-label-pickers.tsx @@ -72,4 +72,5 @@ features.add({ id: "add-custom-label-pickers", feature: addCustomLabelPickers, waitForDomLoaded: true, + needsApi: true, }); diff --git a/source/index.ts b/source/index.ts deleted file mode 100644 index 68b12e1..0000000 --- a/source/index.ts +++ /dev/null @@ -1 +0,0 @@ -import "./refined-gitlab"; diff --git a/source/manifest.json b/source/manifest.json index e83186a..45fa638 100644 --- a/source/manifest.json +++ b/source/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 2, "name": "Refined GitLab", "version": "0.0.0", - "description": "Makes GitLab just a tad bit better", - "homepage_url": "https://github.com/kiprasmel/refined-gitlab", + "description": "🏷 Make your GitLab experience better!", + "homepage_url": "https://gitlab.com/kiprasmel/refined-gitlab", "minimum_chrome_version": "74", "applications": { "gecko": { @@ -12,19 +12,31 @@ } }, "icons": {}, - "permissions": ["storage"], + "permissions": [ + "storage", + "cookies", + "tabs", + "" + ], "options_ui": { "chrome_style": true, "page": "options.html" }, "background": { - "persistent": false, - "scripts": ["browser-polyfill.min.js", "background.js"] + "scripts": [ + "browser-polyfill.min.js", + "background.js" + ] }, "content_scripts": [ { - "matches": [""], - "js": ["index.js"] + "matches": [ + "" + ], + "js": [ + "browser-polyfill.min.js", + "content.js" + ] } ] } diff --git a/source/options.ts b/source/options.ts index 95a3ebc..364839b 100644 --- a/source/options.ts +++ b/source/options.ts @@ -1,9 +1,13 @@ -import optionsStorage from "./options-storage"; +import optionsStorage from "./scripts-background/options-storage"; optionsStorage.syncForm("#options-form"); -const rangeInputs = [...document.querySelectorAll('input[type="range"][name^="color"]')]; -const numberInputs = [...document.querySelectorAll('input[type="number"][name^="color"]')]; +// eslint-disable-next-line @typescript-eslint/ban-ts-ignore +// @ts-ignore +const rangeInputs = [...document.querySelectorAll('input[type="range"][name^="color"]')]; +// eslint-disable-next-line @typescript-eslint/ban-ts-ignore +// @ts-ignore +const numberInputs = [...document.querySelectorAll('input[type="number"][name^="color"]')]; const output = document.querySelector(".color-output"); function updateColor(): void { diff --git a/source/scripts-background/background.ts b/source/scripts-background/background.ts new file mode 100644 index 0000000..651b79e --- /dev/null +++ b/source/scripts-background/background.ts @@ -0,0 +1,3 @@ +// import "./options-storage"; +import "./gitlab-session-cookie-sync"; +// import "./set-global-var"; diff --git a/source/scripts-background/gitlab-session-cookie-sync.ts b/source/scripts-background/gitlab-session-cookie-sync.ts new file mode 100644 index 0000000..ca0b967 --- /dev/null +++ b/source/scripts-background/gitlab-session-cookie-sync.ts @@ -0,0 +1,69 @@ +/** + * Retrieve any previously set cookie and send to content script + * + * https://github.com/mdn/webextensions-examples/blob/master/cookie-bg-picker/background_scripts/background.js + */ + +async function getActiveTab(): Promise { + return await browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => tabs[0]); +} + +interface MessagePayload { + gitlabSessionToken: string | undefined; + success: boolean; + reason?: string; +} + +async function sendMessage( + tabId: number, + messagePayload: MessagePayload, + options?: browser.tabs._SendMessageOptions | undefined +): Promise { + await browser.tabs.sendMessage(tabId, messagePayload, options); +} + +async function gitlabSessionCookieSync(): Promise { + try { + const tab = await getActiveTab(); + + if (tab.id === undefined) { + return; + } + + if (!tab?.url) { + await sendMessage(tab.id, { + gitlabSessionToken: undefined, + success: false, + reason: "Cannot update cookies (tab's **URL** not found)", + }); + + return; + } + + /** get any previously set cookie for the current tab */ + const cookie: browser.cookies.Cookie | null = await browser.cookies.get({ + url: tab.url, + name: "_gitlab_session", + }); + + if (!cookie) { + await sendMessage(tab.id, { + gitlabSessionToken: undefined, + success: false, + reason: "Cannot update cookies (cookie was falsy)", + }); + + return; + } + + await sendMessage(tab.id, { gitlabSessionToken: cookie.value, success: true }); + } catch (e) { + console.error(e); + throw e; + } +} + +/** update when the tab is updated */ +browser.tabs.onUpdated.addListener(gitlabSessionCookieSync); +/** update when the tab is activated */ +browser.tabs.onActivated.addListener(gitlabSessionCookieSync); diff --git a/source/options-storage.ts b/source/scripts-background/options-storage.ts similarity index 100% rename from source/options-storage.ts rename to source/scripts-background/options-storage.ts diff --git a/source/scripts-content/content.ts b/source/scripts-content/content.ts new file mode 100644 index 0000000..7c46d04 --- /dev/null +++ b/source/scripts-content/content.ts @@ -0,0 +1,2 @@ +import "./inject-native-auth-into-api"; +import "./refined-gitlab"; diff --git a/source/scripts-content/inject-native-auth-into-api.ts b/source/scripts-content/inject-native-auth-into-api.ts new file mode 100644 index 0000000..75e8189 --- /dev/null +++ b/source/scripts-content/inject-native-auth-into-api.ts @@ -0,0 +1,32 @@ +import { updateApiVariable } from "../utils/api"; +import { getConfig } from "../utils/config"; +import { getCSRFData } from "../utils/getCSRFData"; + +function injectNativeAuthIntoApi(request, _sender, _sendResponse): void { + const { authKind } = getConfig(); + + if (authKind !== "native") { + return; + } + + const { gitlabSessionToken } = request; + + if (gitlabSessionToken) { + // console.log("`gitlabSessionToken` present - creating new api"); + + const { key: gitlabCSRFTokenKey, value: gitlabCSRFTokenValue } = getCSRFData(); + + updateApiVariable({ + kind: "native", + options: { + nativeAuth: { + gitlabSessionCookieValue: gitlabSessionToken, + gitlabCSRFTokenKey, + gitlabCSRFTokenValue, + }, + }, + }); + } +} + +browser.runtime.onMessage.addListener(injectNativeAuthIntoApi); diff --git a/source/refined-gitlab.ts b/source/scripts-content/refined-gitlab.ts similarity index 55% rename from source/refined-gitlab.ts rename to source/scripts-content/refined-gitlab.ts index 6eec8ee..8493628 100644 --- a/source/refined-gitlab.ts +++ b/source/scripts-content/refined-gitlab.ts @@ -1,13 +1,15 @@ import select from "select-dom"; -import { globalInit } from "./utils/globalInit"; +// import "./utils/api"; +import { globalInit } from "../utils/globalInit"; -import "./styles/default.scss"; -import "./styles/cluster.scss"; +import "../styles/default.scss"; +import "../styles/cluster.scss"; /** leggo */ -import "./features/add-custom-label-pickers"; -import "./features/always-expand-sidebar"; +import "../features/add-custom-label-pickers"; +import "../features/always-expand-sidebar"; +import "../features/show-total-commit-count"; globalInit(); diff --git a/source/utils/api.ts b/source/utils/api.ts index 20a1853..a1884a7 100644 --- a/source/utils/api.ts +++ b/source/utils/api.ts @@ -1,13 +1,54 @@ // // import { Gitlab } from "@gitbeaker/core"; /** all imports utterly broken */ import { Gitlab } from "../../gitbeaker/packages/gitbeaker-browser/src"; -// eslint-disable-next-line import/no-cycle -import { getConfig } from "./config"; +import { NativeAuth as GitbeakerNativeAuth } from "../../gitbeaker/packages/gitbeaker-core/src/infrastructure/BaseService"; +// import { setGlobalVar } from "./setGlobalVar"; /** * https://github.com/jdalrymple/gitbeaker */ -export const api = new Gitlab({ - token: getConfig().apiToken, - host: getConfig().hostUrl, -}); +export interface NativeAuth { + kind: "native"; + options: { + nativeAuth: GitbeakerNativeAuth; + }; +} + +export interface APITokenAuth { + kind: "apiToken"; + options: { + oauthToken: string; + host: string; + }; +} + +export type Auth = NativeAuth | APITokenAuth; + +export type AuthKind = Auth["kind"]; + +// eslint-disable-next-line import/no-mutable-exports +let api: ReturnType; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const createApi = (auth: Auth) => { + console.log("createApi, auth =", auth); + + return new Gitlab({ ...auth.options }); +}; + +export const updateApiVariable = (auth: Auth) => { + console.log("updateApiVariable", api); + + api = createApi(auth); + + /** broken */ + // (window as any).api = api; + + // // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // // @ts-ignore + // (window as any).api = cloneInto(api, window, { cloneFunctions: true }); + + return api; +}; + +export { api }; diff --git a/source/utils/apiLoaded.ts b/source/utils/apiLoaded.ts new file mode 100644 index 0000000..bea7a51 --- /dev/null +++ b/source/utils/apiLoaded.ts @@ -0,0 +1,15 @@ +import { needsToWaitForApi } from "./needsToWaitForApi"; + +export const apiLoaded = new Promise((resolve) => { + if (!needsToWaitForApi()) { + resolve(); + } + + browser.runtime.onMessage.addListener((request, _sender, _sendResponse) => { + const { gitlabSessionToken } = request; + + if (gitlabSessionToken) { + resolve(); + } + }); +}); diff --git a/source/utils/config.ts b/source/utils/config.ts index 5ad41dc..3f7f8e6 100644 --- a/source/utils/config.ts +++ b/source/utils/config.ts @@ -19,6 +19,7 @@ import { FeatureDescription } from "../Features"; // eslint-disable-next-line import/no-cycle import { SidebarFeatureFromLabels } from "../features/add-custom-label-pickers"; +import { AuthKind } from "./api"; /** TODO LINT disable no-explicit-any */ // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -29,10 +30,9 @@ export interface FeatureConfig { config?: FeatureConfigConfig; } -export interface Config { +export type Config<_AuthKind extends AuthKind = AuthKind> = { configVersion: string; - hostUrl: string | RegExp; - apiToken: string; + authKind: _AuthKind; /** feature configs leggo */ sidebarFeaturesFromLabels: SidebarFeatureFromLabels[]; @@ -41,7 +41,14 @@ export interface Config { // // features: { [key: FeatureDescription["id"]]: FeatureConfig}; // // features: { [key: string]: boolean | FeatureConfig}; features: Record; -} +} & (_AuthKind extends "native" + ? {} + : _AuthKind extends "apiToken" + ? { + hostUrl: string; + apiToken: string; + } + : {}); /** Stored somewhere, probably as stringified JSON, or perhaps a js object instead? */ let config: Config = getDefaultConfig(); @@ -59,10 +66,11 @@ export const resetConfig = (): Config => { }; function getDefaultConfig(): Config { - return { + const authKind: AuthKind = "native"; + + const defaultConfig: Config = { configVersion: "0", - hostUrl: "https://gitlab.com", // "", /** TODO FIXME - why are there errors if the url is without `https?://` ? */ - apiToken: "" /** TODO provide link to get the API token @ popup */, + authKind, sidebarFeaturesFromLabels: [ { @@ -171,4 +179,6 @@ function getDefaultConfig(): Config { // }, // ], }; + + return defaultConfig; } diff --git a/source/utils/getCSRFData.ts b/source/utils/getCSRFData.ts new file mode 100644 index 0000000..4c2abde --- /dev/null +++ b/source/utils/getCSRFData.ts @@ -0,0 +1,10 @@ +export const getCSRFData = (): { key: string; value: string } => { + const key: string | undefined = document.querySelector(`meta[name="csrf-param"]`)?.content; + const value: string | undefined = document.querySelector(`meta[name="csrf-token"]`)?.content; + + if (key === undefined || value === undefined) { + throw new Error("[Refined GitLab] Cannot get GitLab's CSRF data - the selectors might be outdated"); + } + + return { key, value }; +}; diff --git a/source/utils/needsToWaitForApi.ts b/source/utils/needsToWaitForApi.ts new file mode 100644 index 0000000..4386225 --- /dev/null +++ b/source/utils/needsToWaitForApi.ts @@ -0,0 +1,3 @@ +import { getConfig } from "./config"; + +export const needsToWaitForApi = (): boolean => getConfig().authKind === "native"; diff --git a/source/utils/pageLoaded.ts b/source/utils/pageLoaded.ts new file mode 100644 index 0000000..92de055 --- /dev/null +++ b/source/utils/pageLoaded.ts @@ -0,0 +1,23 @@ +const hasLoaded = (): boolean => document.readyState === "complete"; + +export const pageLoaded = new Promise((resolve) => { + if (hasLoaded()) { + resolve(); + } else { + document.addEventListener( + "readystatechange", + (event) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + if (event?.target?.readyState === "complete") { + resolve(); + } + }, + { + capture: true, + once: true, + passive: true, + } + ); + } +}); diff --git a/tsconfig.json b/tsconfig.json index 4abd2fd..a6504e1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,40 @@ { - "extends": "@sindresorhus/tsconfig", "compilerOptions": { + // "baseUrl": ".", + // "outDir": "", "module": "esnext", "target": "es6", - "lib": ["dom", "esnext"], - // "target": "esnext", - "declaration": false, - "esModuleInterop": true + "lib": [ + "es6", // + "dom", + "esnext" + ], + "sourceMap": true, + "allowJs": true, + "jsx": "react", + "moduleResolution": "node", + "rootDir": "src", + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": false /** TODO true */, + "strictNullChecks": true, + "suppressImplicitAnyIndexErrors": true, + "noUnusedLocals": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true }, - "include": ["source"] + "include": [ + "source" // + ], + "exclude": [ + "node_modules", + "build", + "scripts", + "acceptance-tests", + "webpack", + "jest", + "src/setupTests.ts" + ] } diff --git a/webpack.config.js b/webpack.config.js index cb5636b..9adda33 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -11,9 +11,8 @@ module.exports = { devtool: "source-map", stats: "errors-only", entry: { - background: "./source/background.ts", - options: "./source/options.ts", - index: "./source/index.ts", + background: "./source/scripts-background/background.ts", + content: "./source/scripts-content/content.ts", }, output: { path: path.join(__dirname, "distribution"), @@ -28,7 +27,7 @@ module.exports = { test: /\.(js|ts|tsx)$/, loader: "ts-loader", options: { - transpileOnly: true /** TODO FUTURE `false` */, + transpileOnly: false, // compilerOptions: { // // Enables ModuleConcatenation. It must be in here to avoid conflict with ts-node when it runs this file @@ -67,7 +66,7 @@ module.exports = { from: "**/*", context: "source", globOptions: { - ignore: ["*.js"], + ignore: ["*.js", ".jsx", ".ts", ".tsx"], }, }, {