diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d735ab85..c00f63e5 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1365,6 +1365,18 @@ "message": "Sections", "description": "Header for the table of contents block listing style section names in the left panel of the classic editor" }, + "integration": { + "message": "UserStyles.world integration", + "description": "Header for the section to link the style with userStyles.world" + }, + "uploadStyle": { + "message": "Publish style", + "description": "Publish the current style to userstyles.world" + }, + "revokeLink": { + "message": "Revoke link", + "description": "Revoke current link of style with userstyles.world" + }, "shortcuts": { "message": "Shortcuts", "description": "Go to shortcut configuration" diff --git a/background/style-manager.js b/background/style-manager.js index be99dccf..f390bfdc 100644 --- a/background/style-manager.js +++ b/background/style-manager.js @@ -6,6 +6,8 @@ /* global prefs */ /* global tabMan */ /* global usercssMan */ +/* global tokenMan */ +/* global retrieveStyleInformation uploadStyle */// usw-api.js 'use strict'; /* @@ -54,12 +56,14 @@ const styleMan = (() => { name: style => `ID: ${style.id}`, _id: () => uuidv4(), _rev: () => Date.now(), + _usw: () => ({}), }; const DELETE_IF_NULL = ['id', 'customName', 'md5Url', 'originalMd5']; /** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */ let ready = init(); chrome.runtime.onConnect.addListener(handleLivePreview); + chrome.runtime.onConnect.addListener(handlePublishingUSW); //#endregion //#region Exports @@ -352,6 +356,56 @@ const styleMan = (() => { }); } + function handlePublishingUSW(port) { + if (port.name !== 'link-style-usw') { + return; + } + port.onMessage.addListener(async incData => { + const {data: style, reason} = incData; + if (!style.id) { + return; + } + switch (reason) { + case 'revoke': + await tokenMan.revokeToken('userstylesworld', style.id); + style._usw = {}; + handleSave(await saveStyle(style), 'success-revoke', true); + break; + + case 'publish': + if (!style._usw || !style._usw.token) { + // Ensures just the style does have the _isUswLinked property as `true`. + for (const {style: someStyle} of dataMap.values()) { + if (someStyle._id === style._id) { + someStyle._isUswLinked = true; + someStyle.sourceCode = style.sourceCode; + const {metadata} = await API.worker.parseUsercssMeta(style.sourceCode); + someStyle.metadata = metadata; + } else { + delete someStyle._isUswLinked; + delete someStyle.sourceCode; + delete someStyle.metadata; + } + handleSave(await saveStyle(someStyle), null, null, false); + } + style._usw = { + token: await tokenMan.getToken('userstylesworld', true, style), + }; + + delete style._isUswLinked; + delete style.sourceCode; + delete style.metadata; + for (const [k, v] of Object.entries(await retrieveStyleInformation(style._usw.token))) { + style._usw[k] = v; + } + handleSave(await saveStyle(style), 'success-publishing', true); + } + uploadStyle(style); + break; + } + }); + } + async function addIncludeExclude(type, id, rule) { if (ready.then) await ready; const style = Object.assign({}, id2style(id)); @@ -427,7 +481,7 @@ const styleMan = (() => { style.id = newId; } uuidIndex.set(style._id, style.id); - API.sync.put(style._id, style._rev); + API.sync.put(style._id, style._rev, style._usw); } async function saveStyle(style) { @@ -437,7 +491,7 @@ const styleMan = (() => { return style; } - function handleSave(style, reason, codeIsUpdated) { + function handleSave(style, reason, codeIsUpdated, broadcast = true) { const data = id2data(style.id); const method = data ? 'styleUpdated' : 'styleAdded'; if (!data) { @@ -445,7 +499,7 @@ const styleMan = (() => { } else { data.style = style; } - broadcastStyleUpdated(style, reason, method, codeIsUpdated); + if (broadcast) broadcastStyleUpdated(style, reason, method, codeIsUpdated); return style; } diff --git a/background/token-manager.js b/background/token-manager.js index 67cefe51..272f431e 100644 --- a/background/token-manager.js +++ b/background/token-manager.js @@ -1,4 +1,4 @@ -/* global FIREFOX getActiveTab waitForTabUrl */// toolbox.js +/* global FIREFOX getActiveTab waitForTabUrl URLS */// toolbox.js /* global chromeLocal */// storage-util.js 'use strict'; @@ -48,6 +48,15 @@ const tokenMan = (() => { 'https://' + location.hostname + '.chromiumapp.org/', scopes: ['Files.ReadWrite.AppFolder', 'offline_access'], }, + userstylesworld: { + flow: 'code', + clientId: 'zeDmKhJIfJqULtcrGMsWaxRtWHEimKgS', + clientSecret: 'wqHsvTuThQmXmDiVvOpZxPwSIbyycNFImpAOTxjaIRqDbsXcTOqrymMJKsOMuibFaij' + + 'ZZAkVYTDbLkQuYFKqgpMsMlFlgwQOYHvHFbgxQHDTwwdOroYhOwFuekCwXUlk', + authURL: URLS.usw + 'api/oauth/authorize_style', + tokenURL: URLS.usw + 'api/oauth/access_token', + redirect_uri: 'https://gusted.xyz/callback_helper/', + }, }; const NETWORK_LATENCY = 30; // seconds @@ -55,11 +64,11 @@ const tokenMan = (() => { return { - buildKeys(name) { + buildKeys(name, styleId) { const k = { - TOKEN: `secure/token/${name}/token`, - EXPIRE: `secure/token/${name}/expire`, - REFRESH: `secure/token/${name}/refresh`, + TOKEN: `secure/token/${name}/${styleId ? `${styleId}/` : ''}token`, + EXPIRE: `secure/token/${name}/${styleId ? `${styleId}/` : ''}expire`, + REFRESH: `secure/token/${name}/${styleId ? `${styleId}/` : ''}refresh`, }; k.LIST = Object.values(k); return k; @@ -69,8 +78,8 @@ const tokenMan = (() => { return AUTH[name].clientId; }, - async getToken(name, interactive) { - const k = tokenMan.buildKeys(name); + async getToken(name, interactive, style) { + const k = tokenMan.buildKeys(name, style.id); const obj = await chromeLocal.get(k.LIST); if (obj[k.TOKEN]) { if (!obj[k.EXPIRE] || Date.now() < obj[k.EXPIRE]) { @@ -83,12 +92,13 @@ const tokenMan = (() => { if (!interactive) { throw new Error(`Invalid token: ${name}`); } - return authUser(name, k, interactive); + const accessToken = authUser(name, k, interactive); + return accessToken; }, - async revokeToken(name) { + async revokeToken(name, styleId) { const provider = AUTH[name]; - const k = tokenMan.buildKeys(name); + const k = tokenMan.buildKeys(name, styleId); if (provider.revoke) { try { const token = await chromeLocal.getValue(k.TOKEN); @@ -177,6 +187,7 @@ const tokenMan = (() => { grant_type: 'authorization_code', client_id: provider.clientId, redirect_uri: query.redirect_uri, + state, }; if (provider.clientSecret) { body.client_secret = provider.clientSecret; diff --git a/background/usw-api.js b/background/usw-api.js new file mode 100644 index 00000000..96e37aa7 --- /dev/null +++ b/background/usw-api.js @@ -0,0 +1,29 @@ +/* global URLS */ // toolbox.js + +'use strict'; + +/* exported retrieveStyleInformation */ +async function retrieveStyleInformation(token) { + return (await (await fetch(`${URLS.usw}api/style`, { + method: 'GET', + headers: new Headers({ + 'Authorization': `Bearer ${token}`, + }), + credentials: 'omit', + })).json()).data; +} + +/* exported uploadStyle */ +async function uploadStyle(style) { + return (await (await fetch(`${URLS.usw}api/style/${style._usw.id}`, { + method: 'POST', + headers: new Headers({ + 'Authorization': `Bearer ${style._usw.token}`, + 'Content-Type': 'application/json', + }), + body: JSON.stringify({ + code: style.sourceCode, + }), + credentials: 'omit', + })).json()).data; +} diff --git a/content/install-hook-userstylesworld.js b/content/install-hook-userstylesworld.js index 23f7c0d8..d7bab17c 100644 --- a/content/install-hook-userstylesworld.js +++ b/content/install-hook-userstylesworld.js @@ -1,3 +1,4 @@ +/* global API */// msg.js 'use strict'; (() => { @@ -15,6 +16,12 @@ && allowedOrigin === event.origin ) { sendPostMessage({type: 'usw-remove-stylus-button'}); + + if (location.pathname === '/api/oauth/authorize_style/new') { + API.styles.find({_isUswLinked: true}).then(style => { + sendPostMessage({type: 'usw-fill-new-style', data: style}); + }); + } } }; diff --git a/edit.html b/edit.html index cd6bac6d..a48f96f9 100644 --- a/edit.html +++ b/edit.html @@ -59,6 +59,7 @@ +