diff --git a/background/openusercss-api.js b/background/openusercss-api.js new file mode 100644 index 00000000..0ef98140 --- /dev/null +++ b/background/openusercss-api.js @@ -0,0 +1,102 @@ +'use strict'; + +(() => { + // begin:nanographql - Tiny graphQL client library + // Author: yoshuawuyts (https://github.com/yoshuawuyts) + // License: MIT + // Modified by DecentM to fit project standards + + const getOpname = /(query|mutation) ?([\w\d-_]+)? ?\(.*?\)? \{/; + const gql = str => { + str = Array.isArray(str) ? str.join('') : str; + const name = getOpname.exec(str); + + return variables => { + const data = {query: str}; + if (variables) data.variables = JSON.stringify(variables); + if (name && name.length) { + const operationName = name[2]; + if (operationName) data.operationName = name[2]; + } + return JSON.stringify(data); + }; + }; + + // end:nanographql + + const api = 'https://api.openusercss.org'; + const doQuery = ({id}, queryString) => { + const query = gql(queryString); + + return fetch(api, { + method: 'POST', + headers: new Headers({ + 'Content-Type': 'application/json' + }), + body: query({ + id + }) + }) + .then(res => res.json()); + }; + + window.API_METHODS = Object.assign(window.API_METHODS || {}, { + /** + * This function can be used to retrieve a theme object from the + * GraphQL API, set above + * + * Example: + * chrome.runtime.sendMessage({ + * 'method': 'oucThemeById', + * 'id': '5a2f819f7c57c751001b49df' + * }, console.log); + * + * @param {ID} $0.id MongoDB style ID + * @returns {Promise.<{data: object}>} The GraphQL result with the `theme` object + */ + + oucThemeById: params => doQuery(params, ` + query($id: ID!) { + theme(id: $id) { + _id + title + description + createdAt + lastUpdate + version + screenshots + user { + _id + displayname + } + } + } + `), + + /** + * This function can be used to retrieve a user object from the + * GraphQL API, set above + * + * Example: + * chrome.runtime.sendMessage({ + * 'method': 'oucUserById', + * 'id': '5a2f0361ba666f0b00b9c827' + * }, console.log); + * + * @param {ID} $0.id MongoDB style ID + * @returns {Promise.<{data: object}>} The GraphQL result with the `user` object + */ + + oucUserById: params => doQuery(params, ` + query($id: ID!) { + user(id: $id) { + _id + displayname + avatarUrl + smallAvatarUrl + bio + } + } + `), + }); +})(); diff --git a/content/install-hook-openusercss.js b/content/install-hook-openusercss.js new file mode 100644 index 00000000..e9699073 --- /dev/null +++ b/content/install-hook-openusercss.js @@ -0,0 +1,176 @@ +'use strict'; + +(() => { + const manifest = chrome.runtime.getManifest(); + const allowedOrigins = [ + 'https://openusercss.org', + 'https://openusercss.com' + ]; + + const sendPostMessage = message => { + if (allowedOrigins.includes(location.origin)) { + window.postMessage(message, location.origin); + } + }; + + const askHandshake = () => { + // Tell the page that we exist and that it should send the handshake + sendPostMessage({ + type: 'ouc-begin-handshake' + }); + }; + + // Listen for queries by the site and respond with a callback object + const sendInstalledCallback = styleData => { + sendPostMessage({ + type: 'ouc-is-installed-response', + style: styleData + }); + }; + + const installedHandler = event => { + if (event.data + && event.data.type === 'ouc-is-installed' + && allowedOrigins.includes(event.origin) + ) { + chrome.runtime.sendMessage({ + method: 'findUsercss', + name: event.data.name, + namespace: event.data.namespace + }, style => { + const data = {event}; + const callbackObject = { + installed: Boolean(style), + enabled: style.enabled, + name: data.name, + namespace: data.namespace + }; + + sendInstalledCallback(callbackObject); + }); + } + }; + + const attachInstalledListeners = () => { + window.addEventListener('message', installedHandler); + }; + + const doHandshake = () => { + // This is a representation of features that Stylus is capable of + const implementedFeatures = [ + 'install-usercss', + 'event:install-usercss', + 'event:is-installed', + 'configure-after-install', + 'builtin-editor', + 'create-usercss', + 'edit-usercss', + 'import-moz-export', + 'export-moz-export', + 'update-manual', + 'update-auto', + 'export-json-backups', + 'import-json-backups', + 'manage-local' + ]; + const reportedFeatures = []; + + // The handshake question includes a list of required and optional features + // we match them with features we have implemented, and build a union array. + event.data.featuresList.required.forEach(feature => { + if (implementedFeatures.includes(feature)) { + reportedFeatures.push(feature); + } + }); + + event.data.featuresList.optional.forEach(feature => { + if (implementedFeatures.includes(feature)) { + reportedFeatures.push(feature); + } + }); + + // We send the handshake response, which includes the key we got, plus some + // additional metadata + sendPostMessage({ + type: 'ouc-handshake-response', + key: event.data.key, + extension: { + name: manifest.name, + capabilities: reportedFeatures + } + }); + }; + + const handshakeHandler = event => { + if (event.data + && event.data.type === 'ouc-handshake-question' + && allowedOrigins.includes(event.origin) + ) { + doHandshake(); + } + }; + + const attachHandshakeListeners = () => { + // Wait for the handshake request, then start it + window.addEventListener('message', handshakeHandler); + }; + + const sendInstallCallback = data => { + // Send an install callback to the site in order to let it know + // we were able to install the theme and it may display a success message + sendPostMessage({ + type: 'ouc-install-callback', + key: data.key + }); + }; + + const installHandler = event => { + if (event.data + && event.data.type === 'ouc-install-usercss' + && allowedOrigins.includes(event.origin) + ) { + chrome.runtime.sendMessage({ + method: 'saveUsercss', + reason: 'install', + name: event.data.title, + sourceCode: event.data.code, + }, style => { + sendInstallCallback({ + enabled: style.enabled, + key: event.data.key + }); + }); + } + }; + + const attachInstallListeners = () => { + // Wait for an install event, then save the theme + window.addEventListener('message', installHandler); + }; + + const orphanCheck = () => { + const eventName = chrome.runtime.id + '-install-hook-openusercss'; + const orphanCheckRequest = () => { + // If we can't get the UI language, it means we are orphaned, and should + // remove our event handlers + if (chrome.i18n && chrome.i18n.getUILanguage()) return true; + + window.removeEventListener('message', installHandler); + window.removeEventListener('message', handshakeHandler); + window.removeEventListener('message', installedHandler); + window.removeEventListener(eventName, orphanCheckRequest, true); + }; + + // Send the event before we listen for it, for other possible + // running instances of the content script. + dispatchEvent(new Event(eventName)); + addEventListener(eventName, orphanCheckRequest, true); + }; + + orphanCheck(); + + attachHandshakeListeners(); + attachInstallListeners(); + attachInstalledListeners(); + askHandshake(); +})(); diff --git a/manifest.json b/manifest.json index 66c117e2..8cffdff2 100644 --- a/manifest.json +++ b/manifest.json @@ -34,6 +34,7 @@ "background/search-db.js", "background/update.js", "background/refresh-all-tabs.js", + "background/openusercss-api.js", "vendor/node-semver/semver.js", "vendor-overwrites/colorpicker/colorconverter.js" ] @@ -60,6 +61,12 @@ "all_frames": false, "js": ["content/install-hook-userstyles.js"] }, + { + "matches": ["https://openusercss.org/*", "https://openusercss.com/*"], + "run_at": "document_start", + "all_frames": false, + "js": ["content/install-hook-openusercss.js"] + }, { "matches": [""], "include_globs": ["*.user.css", "*.user.styl"],